Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1

# Build stage with required Node + Go
FROM node:22.6.0-alpine3.22 AS build-env
FROM node:22-alpine AS build-env

ARG GOPROXY=direct
ARG GITEA_VERSION
Expand Down Expand Up @@ -41,7 +41,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/tmp/local/etc/s6/gitea/* \
/tmp/local/etc/s6/openssh/* \
/tmp/local/etc/s6/.s6-svscan/* \
/src/gitea \
/src/processgit \
/src/processgit-seed

# Runtime stage
Expand All @@ -67,7 +67,7 @@ RUN addgroup -S -g 1000 git && \
echo "git:*" | chpasswd -e

COPY --from=build-env /tmp/local /
COPY --from=build-env /src/gitea /app/gitea/gitea
COPY --from=build-env /src/processgit /app/gitea/gitea
COPY --from=build-env /src/processgit-seed /app/processgit-seed
COPY --from=build-env /src/templates /app/gitea/templates
COPY --from=build-env /src/public /app/gitea/public
Expand Down
66 changes: 66 additions & 0 deletions deploy/docker-compose.uapf.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# UAPF execution MCP scope — release-bundle additions
#
# Wires the per-repo /{owner}/{repo}/uapf-mcp execution scope (added in
# routers/web/repo/uapfmcp.go) to a UAPF engine and, optionally, a capability
# host (LLM gateway). Apply alongside the base compose:
#
# # Fresh install (ProcessGit ships its own engine):
# docker compose -f docker-compose.yml -f docker-compose.uapf.example.yml \
# --profile bundled-engine up -d
#
# # Host where an engine already runs on the same network (e.g. Kojusalas:
# # the opendms stack already provides 'uapf-engine' on deploy_default):
# docker compose -f docker-compose.yml -f docker-compose.uapf.example.yml up -d
#
# Backward compatible: the env below is inert unless a repo has a uapf.yaml and
# an agent connects to /uapf-mcp. A processgit image built from this branch is
# required for the endpoint to exist.

services:
processgit:
environment:
# Engine the /uapf-mcp scope delegates execution to (internal network).
UAPF_ENGINE_URL: "http://uapf-engine:4000"
# Where the engine fetches package archives from — internal ProcessGit, so
# no public egress and no dependence on the external hostname.
UAPF_ARCHIVE_BASE: "http://processgit:3000"
# Host identity advertised to the engine in the session hostManifest.
UAPF_HOST_DID: "did:web:processgit.local"
# Capability host implementing /uapf/host/capability/* for ai.* operations
# (ai.redact@1, ai.extract@1, ...). REQUIRED only for packages that declare
# ai.* in requires_capabilities; pure DMN/BPMN packages run without it.
# Point at your LLM gateway (see llm-gateway note below).
UAPF_HOST_BASE_URL: "${UAPF_HOST_BASE_URL:-}"

# --- Bundled UAPF engine (fresh installs) -----------------------------------
# Enabled only with `--profile bundled-engine`. On hosts where an engine is
# already present on the same docker network (Kojusalas: the opendms stack
# provides 'uapf-engine' on deploy_default), DO NOT enable this profile — the
# processgit env above already reaches the existing engine.
uapf-engine:
profiles: ["bundled-engine"]
image: "${UAPF_ENGINE_IMAGE:-opendms-uapf-engine:1.3.0}"
container_name: uapf-engine
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:4000/health >/dev/null 2>&1 || exit 1"]
interval: 10s
timeout: 3s
retries: 12

# --- Capability host (LLM gateway) ------------------------------------------
# No image ships with ProcessGit: the capability host is a UAPF host adapter
# that implements POST /uapf/host/capability/{namespace}/{operation} and backs
# ai.* operations with your LLM, kept inside the perimeter. Provide it and set
# UAPF_HOST_BASE_URL (e.g. http://llm-gateway:8080). Skeleton:
#
# llm-gateway:
# profiles: ["bundled-gateway"]
# image: <your-uapf-capability-host-image>
# container_name: llm-gateway
# restart: unless-stopped
# healthcheck:
# test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health >/dev/null 2>&1 || exit 1"]
# interval: 10s
# timeout: 3s
# retries: 12
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
github.com/ProtonMail/go-crypto v1.3.0
github.com/PuerkitoBio/goquery v1.10.3
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
github.com/UAPFormat/uapf-mcp-go v0.1.1
github.com/alecthomas/chroma/v2 v2.21.1
github.com/aws/aws-sdk-go-v2/credentials v1.18.10
github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 h1:tgjwQrDH5m6jIYB7kac5IQZmfUzQNseac/e3H4VoCNE=
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R1QWahSeMJkjSkq6CYAZu1aIbYSpfJ4o=
github.com/UAPFormat/uapf-mcp-go v0.1.1 h1:L6WpUzAMXhVcRdC3Fj8jZZ8rT8SYkmTNBua7H/PFsYg=
github.com/UAPFormat/uapf-mcp-go v0.1.1/go.mod h1:paBr1tXTrfc7WdJ3Oas9zf+WOEjjBfDhc8z5XupAYAo=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
Expand Down Expand Up @@ -456,8 +458,6 @@ github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY=
Expand Down
9 changes: 8 additions & 1 deletion modules/diagrams/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
DiagramDMN DiagramType = "dmn"
DiagramNGraph DiagramType = "ngraph"
DiagramRuleset DiagramType = "ruleset"
DiagramCard DiagramType = "card" // UAPF v2.5.0+ algorithm card
DiagramNone DiagramType = "none"
)

Expand All @@ -33,7 +34,7 @@ type rulesetMetadata struct {

func (d DiagramType) Editable() bool {
switch d {
case DiagramBPMN, DiagramCMMN, DiagramDMN:
case DiagramBPMN, DiagramCMMN, DiagramDMN, DiagramCard:
return true
default:
return false
Expand Down Expand Up @@ -73,6 +74,10 @@ func detectByExtension(pathLower string) (DiagramType, string) {
return DiagramRuleset, "xml"
case strings.HasSuffix(pathLower, ".ruleset"):
return DiagramRuleset, ""
case strings.HasSuffix(pathLower, ".card.yaml"), strings.HasSuffix(pathLower, ".card.yml"):
return DiagramCard, "yaml"
case strings.HasSuffix(pathLower, ".card.json"):
return DiagramCard, "json"
default:
return DiagramNone, ""
}
Expand Down Expand Up @@ -162,6 +167,8 @@ func defaultFormatForType(diagramType DiagramType) string {
if diagramType != DiagramNone {
return "json"
}
case DiagramCard:
return "yaml"
}
return ""
}
Expand Down
80 changes: 30 additions & 50 deletions modules/uapf/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"fmt"
"io"
"os"
"slices"
"strings"

repo_model "code.gitea.io/gitea/models/repo"
Expand All @@ -36,49 +35,32 @@ func ExportUAPF(ctx context.Context, repo *repo_model.Repository, ref string) (i
return nil, "", err
}

manifestEntry, err := commit.GetTreeEntryByPath("manifest.json")
manifestName, manifestEntry, err := findManifestEntry(commit)
if err != nil {
if git.IsErrNotExist(err) {
return nil, "", fmt.Errorf("manifest.json not found at ref %s", ref)
}
return nil, "", err
}

manifestData, err := readTreeEntry(manifestEntry)
if err != nil {
return nil, "", fmt.Errorf("read manifest.json: %w", err)
return nil, "", fmt.Errorf("read %s: %w", manifestName, err)
}

if err := ValidateManifest(manifestData); err != nil {
if err := ValidateManifest(manifestName, manifestData); err != nil {
return nil, "", err
}

var manifest spec.Manifest
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return nil, "", fmt.Errorf("manifest.json is not valid JSON: %w", err)
}

refPaths, err := spec.ValidateManifest(&manifest)
manifestJSON, err := manifestToJSON(manifestName, manifestData)
if err != nil {
return nil, "", err
}

requiredPaths := make(map[string]struct{}, len(refPaths))
for _, rel := range refPaths {
if rel == "" {
continue
}
entry, err := commit.GetTreeEntryByPath(rel)
if err != nil {
if git.IsErrNotExist(err) {
return nil, "", fmt.Errorf("referenced path missing at ref %s: %s", ref, rel)
}
return nil, "", err
}
if entry.IsDir() {
return nil, "", fmt.Errorf("referenced path must be a file: %s", rel)
}
requiredPaths[rel] = struct{}{}
var manifest spec.Manifest
if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
return nil, "", fmt.Errorf("manifest is not valid: %w", err)
}

if err := spec.ValidateManifest(&manifest); err != nil {
return nil, "", err
}

entries, err := commit.Tree.ListEntriesRecursiveFast()
Expand All @@ -89,7 +71,7 @@ func ExportUAPF(ctx context.Context, repo *repo_model.Repository, ref string) (i
pr, pw := io.Pipe()
go func() {
zw := zip.NewWriter(pw)
if err := writeBytesEntry(zw, "manifest.json", manifestData); err != nil {
if err := writeBytesEntry(zw, manifestName, manifestData); err != nil {
_ = pw.CloseWithError(err)
return
}
Expand All @@ -99,8 +81,7 @@ func ExportUAPF(ctx context.Context, repo *repo_model.Repository, ref string) (i
continue
}
name := entry.Name()
if name == "" || name == "manifest.json" {
delete(requiredPaths, name)
if name == "" || name == manifestName {
continue
}
if entry.IsSubModule() {
Expand All @@ -111,17 +92,6 @@ func ExportUAPF(ctx context.Context, repo *repo_model.Repository, ref string) (i
_ = pw.CloseWithError(err)
return
}
delete(requiredPaths, name)
}

if len(requiredPaths) > 0 {
missing := make([]string, 0, len(requiredPaths))
for path := range requiredPaths {
missing = append(missing, path)
}
slices.Sort(missing)
_ = pw.CloseWithError(fmt.Errorf("referenced path missing at ref %s: %s", ref, strings.Join(missing, ", ")))
return
}

if err := zw.Close(); err != nil {
Expand All @@ -135,16 +105,26 @@ func ExportUAPF(ctx context.Context, repo *repo_model.Repository, ref string) (i
return pr, filename, nil
}

// findManifestEntry locates the UAPF manifest in a commit tree, returning the
// highest-priority accepted manifest file name and its tree entry.
func findManifestEntry(commit *git.Commit) (string, *git.TreeEntry, error) {
for _, name := range ManifestNames {
entry, err := commit.GetTreeEntryByPath(name)
if err == nil {
return name, entry, nil
}
if !git.IsErrNotExist(err) {
return "", nil, err
}
}
return "", nil, fmt.Errorf("a UAPF manifest (uapf.yaml) is required in the repository")
}

func buildExportFilename(repo *repo_model.Repository, manifest spec.Manifest) string {
name := manifest.Name
version := manifest.Version
if manifest.Package != nil {
if manifest.Package.Name != "" {
name = manifest.Package.Name
}
if manifest.Package.Version != "" {
version = manifest.Package.Version
}
if name == "" {
name = manifest.ID
}
if name == "" {
name = repo.Name
Expand Down
Loading
Loading