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
4 changes: 2 additions & 2 deletions docs/architecture/agent-factory.drawio

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/architecture/ai-platform.drawio

Large diffs are not rendered by default.

102 changes: 95 additions & 7 deletions docs/superpowers/plans/2026-09-27-agent-dark-factory-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -11794,10 +11794,16 @@ git commit -m "docs(adr): ADR-0045 merge policy gate"
*External review R01:* it also exits 1 when such a workflow grants any `write` permission at
workflow level, or at job level for a job not in the script's own allowlist (`sarif-upload:
security-events`, `render-diff-comment: pull-requests`, `build-and-push: packages,
security-events`). An allowlisted job must contain no `actions/checkout` of the PR head and no
`run:` step. Fixtures: a workflow-level write fails; an unlisted job with write fails. The
allowlist is a gate path (R17). `build-and-push` fails the last clause today: split the push out
of the PR path, or record it as an exception. The `ci.yaml` job split is its own `fix(ci)` PR.
security-events`, `notify-main-broken: issues` — a recorded exception: push-gated by its `if:`,
deliberately checkout-free, its `run:` steps open the tracking issue only after a broken push
to `main`). An allowlisted job must contain no `actions/checkout` of the PR head, and no
`run:` step that executes on a `pull_request` event — a job whose `if:` pins
`github.event_name == 'push'` satisfies this by construction and is listed as push-gated in
the script. Fixtures: a workflow-level write fails; an unlisted job with write fails; a
push-gated allowlisted job with a `run:` step passes; the same job without the push gate
fails. The allowlist is a gate path (R17). `build-and-push` fails the last clause today:
split the push out of the PR path, or record it as an exception. The `ci.yaml` job split is
its own `fix(ci)` PR.
- Tasks `ci:policy-gates`, `ci:workflow-secrets`.

The canonical gate list is `no_changed_files.paths` of the rule `agent change approved by a
Expand Down Expand Up @@ -11878,6 +11884,8 @@ echo PASS
#
# T8: a pull_request workflow may reference GITHUB_TOKEN and no other secret; agent branches
# live in this repo, so their PRs run with its secrets.
# External review R01: such a workflow also grants no write permission, at workflow level
# or in a job outside the script's allowlist.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SUBJECT="$HERE/../check-workflow-secrets.sh"
Expand All @@ -11894,13 +11902,38 @@ on: {push: {branches: [main]}}
jobs: {a: {runs-on: x, steps: [{run: "echo ${{ secrets.DEPLOY_KEY }}"}]}}
EOF
WORKFLOWS_DIR="$d" bash "$SUBJECT" >/dev/null 2>&1 || fail "GITHUB_TOKEN, and secrets on push-only workflows, pass"
cat >"$d/push-gated-run.yml" <<'EOF'
on: {pull_request: {}}
jobs: {notify-main-broken: {if: "github.event_name == 'push'", runs-on: x, permissions: {issues: write}, steps: [{run: "echo ok"}]}}
EOF
WORKFLOWS_DIR="$d" bash "$SUBJECT" >/dev/null 2>&1 || fail "a push-gated allowlisted job with a run: step passes"
cat >"$d/bad.yml" <<'EOF'
on:
pull_request_target:
jobs: {a: {runs-on: x, steps: [{run: "echo ${{ secrets.SLACK_WEBHOOK }}"}]}}
EOF
out="$(WORKFLOWS_DIR="$d" bash "$SUBJECT" 2>&1)" && fail "a pull_request_target workflow with a secret fails"
grep -q 'bad.yml.*SLACK_WEBHOOK' <<<"$out" || fail "the failure names the file and the secret"
cat >"$d/wf-write.yml" <<'EOF'
on: {pull_request: {}}
permissions: {contents: write}
jobs: {a: {runs-on: x, steps: [{run: "echo ok"}]}}
EOF
out="$(WORKFLOWS_DIR="$d" bash "$SUBJECT" 2>&1)" && fail "a pull_request workflow with a workflow-level write permission fails"
grep -q 'wf-write.yml.*contents' <<<"$out" || fail "the failure names the file and the permission"
cat >"$d/job-write.yml" <<'EOF'
on: {pull_request: {}}
jobs:
upload: {runs-on: x, permissions: {security-events: write}, steps: [{run: "echo ok"}]}
EOF
out="$(WORKFLOWS_DIR="$d" bash "$SUBJECT" 2>&1)" && fail "a pull_request workflow with an unlisted job holding write fails"
grep -q 'job-write.yml.*upload' <<<"$out" || fail "the failure names the file and the job"
cat >"$d/ungated-run.yml" <<'EOF'
on: {pull_request: {}}
jobs: {notify-main-broken: {runs-on: x, permissions: {issues: write}, steps: [{run: "echo ok"}]}}
EOF
out="$(WORKFLOWS_DIR="$d" bash "$SUBJECT" 2>&1)" && fail "an allowlisted job with a run: step and no push gate fails"
grep -q 'ungated-run.yml.*notify-main-broken' <<<"$out" || fail "the failure names the file and the job"
[ "$fails" -eq 0 ] || exit 1
echo PASS
```
Expand Down Expand Up @@ -11973,21 +12006,76 @@ PY
# T8 (SP3 §8): agent branches live in this repository, so their PRs run pull_request workflows
# with its secrets. Only GITHUB_TOKEN may appear in such a workflow; a new secret-bearing
# workflow must fence agent heads first.
# External review R01: such a workflow also grants no write permission — write scopes live
# only in allowlisted jobs that run no PR code. The allowlist below is a gate path (R17).
set -euo pipefail
DIR="${WORKFLOWS_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/.github/workflows}"
DIR="$DIR" python3 - <<'PY'
import glob, os, re, sys, yaml

# job -> the write scopes it may hold. build-and-push's split out of the PR path is its
# own fix(ci) PR; until it lands the lint flags the job's run steps, and the entry stays
# so the split cannot quietly widen it again.
ALLOWLIST = {
"sarif-upload": {"security-events"},
"render-diff-comment": {"pull-requests"},
"build-and-push": {"packages", "security-events"},
# notify-main-broken: push-gated if:, no checkout by design, run: only opens the tracking issue
"notify-main-broken": {"issues"},
}

def write_scopes(perms):
# `permissions: write` (a bare string) is write on every scope
if perms == "write":
return None
if isinstance(perms, dict):
return {k for k, v in perms.items() if v == "write"}
return set()

bad = []
for f in sorted(glob.glob(os.path.join(os.environ["DIR"], "*.y*ml"))):
text = open(f).read()
doc = yaml.safe_load(text) or {}
on = doc.get("on", doc.get(True, {})) # PyYAML reads the key `on` as True
events = on if isinstance(on, (dict, list)) else [on]
if not any(e in ("pull_request", "pull_request_target") for e in events):
triggers = {e for e in events if e in ("pull_request", "pull_request_target")}
if not triggers:
continue
for name in sorted(set(re.findall(r"secrets\.([A-Za-z0-9_]+)", text)) - {"GITHUB_TOKEN"}):
bad.append(f"{os.path.basename(f)}: secrets.{name} in a pull_request workflow")
name = os.path.basename(f)
for secret in sorted(set(re.findall(r"secrets\.([A-Za-z0-9_]+)", text)) - {"GITHUB_TOKEN"}):
bad.append(f"{name}: secrets.{secret} in a pull_request workflow")
scopes = write_scopes(doc.get("permissions"))
if scopes is None:
bad.append(f"{name}: workflow-level permissions: write (every scope)")
else:
for scope in sorted(scopes):
bad.append(f"{name}: workflow-level {scope}: write")
for job, spec in (doc.get("jobs") or {}).items():
if not isinstance(spec, dict):
continue
held = write_scopes(spec.get("permissions"))
if held is None:
bad.append(f"{name}: job '{job}' holds write on every scope")
continue
for scope in sorted(held - ALLOWLIST.get(job, set())):
bad.append(f"{name}: job '{job}' holds {scope}: write and is not in the allowlist")
if job in ALLOWLIST:
# An allowlisted job runs no PR code. pull_request's default checkout ref is
# the PR merge commit; pull_request_target's is the base, so only an explicit
# pull_request ref counts there.
steps = [s for s in (spec.get("steps") or []) if isinstance(s, dict)]
# a push-gated if: means the job's run: steps never execute on a pull_request event
job_if = str(spec.get("if") or "")
push_gated = "github.event_name == 'push'" in job_if or "!= 'pull_request'" in job_if
if any("run" in s for s in steps) and not push_gated:
bad.append(f"{name}: allowlisted job '{job}' has a run: step")
for s in steps:
if str(s.get("uses") or "").split("@")[0] != "actions/checkout":
continue
ref = str((s.get("with") or {}).get("ref") or "")
if "github.event.pull_request" in ref or "github.head_ref" in ref \
or (not ref and "pull_request" in triggers):
bad.append(f"{name}: allowlisted job '{job}' checks out the PR head")
for b in bad:
print("FAIL:", b, file=sys.stderr)
sys.exit(1 if bad else 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,4 +509,4 @@ deciding risk · agents deploying anything · auto-reverting human merges · a m
| 3 | `Smana/agent-platform` | `Task` CRD and controller (intake, run-request API, scheduler, triage, templates, run meter, CI/policy watch, auto-merge arming, revert watcher, metrics), strict config, envtest suite, signed chart | Unit and envtest suites green |
| 4 | this repo | ADR-0048; HelmRelease, config, factory App key via `agents-secrets`, CNPs, Kyverno `AgentRun` creator rule, Kueue queues under `clusters/aws-0-agent-platform/`; RunLore `notify.templated` block; VMRule + dashboard inside the umbrella | `validate-manifests.sh`, `validate-vmrules.sh` exit 0 |
| 5 | live trial | `link-rot` schedule, one review-class issue, the kill-switch drill, all `public`; budgets in shadow for the first week (OD-10) | SC-1…SC-8, SC-10…SC-14 → `/verify-spec` |
| 6 | after SP4's Bedrock backend (OD-12/13) | A RunLore replay: `internal` tasks have no backend before it | SC-9 |
| 6 | after the Anthropic API backend lands (agentgateway migration phase I, ADR-0054; OD-12/13) | A RunLore replay: `internal` tasks have no backend before it | SC-9 |
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ secrets; `id-token: write` only on push and schedule workflows.
| T10 | Unauthorised claims | After SP3, only the factory SA creates `AgentRun`s (SP3's Kyverno rule, admins included), and the factory derives the principal. A repo opts in three times: the branch ruleset, trust policies and App install | Before SP3, the owner creates runs directly. Break-glass is suspending the rule through Flux, which is visible in Git |
| T11 | Harness supply chain | Profiles pinned by digest; Trivy; no image field in the claim | Lands with the next reviewed bump |
| T12 | MCP data exposure | Read-only, no `secrets`, per-role tools | Logs and ConfigMaps may hold secrets. Cluster-wide `get pods` also exposes pod specs (env `value`, args) and, under `FallbackToLogsOnError`, `status...terminated.message` (log tail) — reachable by every `internal` run, not only reviewer/tester/triager (review M3) |
| T13 | CI tampering | No `workflows` permission; PR CI holds no secrets | `contents: read` in every job that runs PR code. Write scopes live only in jobs that run no PR code (`sarif-upload`, `render-diff-comment`), enforced by the T8 lint (SP3 plan Task 6.3). *External review R01: until that `ci.yaml` fix lands, `security-events: write` is workflow-level, not only on the SARIF upload* |
| T13 | CI tampering | No `workflows` permission; PR CI holds no secrets | `contents: read` in every job that runs PR code. Write scopes live only in jobs that run no PR code (`sarif-upload`, `render-diff-comment`), enforced by the T8 lint (SP3 plan Task 6.3) |
| T14 | **Pre-existing:** the `openbao-platform` ClusterSecretStore has no namespace `conditions`, so any namespace can read any `platform/` path | SP1 never uses it (S9). `agents-no-secret-import` blocks ESO objects in `agents` | Any *other* namespace with ExternalSecret rights can read `platform/agents/*`. Fixing the cluster store is out of scope (O1) | **Addressed by SP2 plan P38 (2026-09-27):** agent credentials move to a dedicated `agents` mount that the `external-secrets` policy does not cover.
| T15 | `internal` data reaching a SaaS model | `dataClass` is required at creation. The audience binds the class. Z.ai routes only on `public`. Cluster-read MCP tools only on `internal` | A human creating a run can misclassify internal content as `public`. Once SP3 ships it sets the class from the task source |
| T16 | Reserved-audience minting from an excluded namespace | Kyverno's global config excludes `kube-system` and `security` (its own namespace) from admission, so a pod there (ESO, cert-manager) can still mint the reserved audiences with a live `TokenRequest` call; `agent-audience-token-request` covers every other namespace | **Accepted:** needs a compromised platform controller in `kube-system` or `security` |
Expand Down
2 changes: 1 addition & 1 deletion website/content/docs/platform/ai-platform/agents/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ The diagram below shows the **target** architecture: the whole programme once bu
each box as deployed on `gcp-0` (noting where its live gate is pending), built but not yet
deployed, or planned.

![The Agent Factory's target architecture. Triggers: a GitHub repository (the factory/ready and factory/stop labels; a PR review asking for changes, built but not deployed), RunLore findings (planned), the task agent:run CLI, a developer in a browser (approving is planned), and roomctl (planned). The factory turns a labelled issue into a task: intake and narration from a fixed template, then the Task controller, which starts one implementer per task, opens a room and runs the run meter, with a kill switch beside it; all deployed on gcp-0, live gate pending. The reviewer pair and revise flow are built but not deployed; teams with a tester, Kueue admission and the merge gate (policy-bot and a merger App, auto-merge and rollback in shadow) are planned. Rooms: a web UI behind oauth2-proxy and ZITADEL SSO, the room-broker and its append-only CNPG log are deployed, with the steering, room tools and verdicts still awaiting their live gate; approval cards and fork are planned. The runtime turns an AgentRun claim, through Crossplane, into a default-deny CiliumNetworkPolicy, projected tokens and a gVisor Sandbox pod holding the room-bridge sidecar, the OpenHands harness and an Envoy identity-proxy, on a GKE Sandbox pool on gcp-0 (deployed) or a Karpenter AL2023 pool on aws-0 (built). The proxy sends every call with a per-run JWT to Agent Router (Envoy AI Gateway 1.1.0, deployed and being replaced by agentgateway, selected on 2026-10-01, with a PoC instance on gcp-0), which routes to Z.ai GLM-5.3 (deployed), Claude on Bedrock for aws-0 and on Vertex AI for gcp-0 (planned), the MCP servers and octo-sts, which mints a token for the agents' GitHub App, confined by rulesets to agent/** branches and no tags. Agent Router also carries the agents' room_* tools to the broker; token budgets and tiers are planned. The room-bridge streams events to the broker over TLS with a room token, and the broker posts verdicts on the PR. Spans go through the agent-traces-collector to VictoriaTraces, step logs to VictoriaLogs, and access logs, gen_ai metrics and AgentRun state to VictoriaMetrics, all shown on the agent-run and agent-fleet Grafana dashboards. The same manifests deploy to gcp-0, the live cluster, and aws-0, destroyed and rebuilt on demand](/images/diagrams/agent-factory.svg)
![The Agent Factory's target architecture. Triggers: a GitHub repository (the factory/ready and factory/stop labels; a PR review asking for changes, built but not deployed), RunLore findings (planned), the task agent:run CLI, a developer in a browser (approving is planned), and roomctl (planned). The factory turns a labelled issue into a task: intake and narration from a fixed template, then the Task controller, which starts one implementer per task, opens a room and runs the run meter, with a kill switch beside it; all deployed on gcp-0, live gate pending. The reviewer pair and revise flow are built but not deployed; teams with a tester, Kueue admission and the merge gate (policy-bot and a merger App, auto-merge and rollback in shadow) are planned. Rooms: a web UI behind oauth2-proxy and ZITADEL SSO, the room-broker and its append-only CNPG log are deployed, with the steering, room tools and verdicts still awaiting their live gate; approval cards and fork are planned. The runtime turns an AgentRun claim, through Crossplane, into a default-deny CiliumNetworkPolicy, projected tokens and a gVisor Sandbox pod holding the room-bridge sidecar, the OpenHands harness and an Envoy identity-proxy, on a GKE Sandbox pool on gcp-0 (deployed) or a Karpenter AL2023 pool on aws-0 (built). The proxy sends every call with a per-run JWT to Agent Router (Envoy AI Gateway 1.1.0, deployed and being replaced by agentgateway, selected on 2026-10-01, with a PoC instance on gcp-0), which routes to Z.ai GLM-5.3 (deployed), the Anthropic API direct for internal data with a gateway-held key (planned), Bedrock and Vertex AI optional per cloud (planned), the MCP servers and octo-sts, which mints a token for the agents' GitHub App, confined by rulesets to agent/** branches and no tags. Agent Router also carries the agents' room_* tools to the broker; token budgets and tiers are planned. The room-bridge streams events to the broker over TLS with a room token, and the broker posts verdicts on the PR. Spans go through the agent-traces-collector to VictoriaTraces, step logs to VictoriaLogs, and access logs, gen_ai metrics and AgentRun state to VictoriaMetrics, all shown on the agent-run and agent-fleet Grafana dashboards. The same manifests deploy to gcp-0, the live cluster, and aws-0, destroyed and rebuilt on demand](/images/diagrams/agent-factory.svg)

*Source: [`docs/architecture/agent-factory.drawio`](https://github.com/Smana/cloud-native-ref/blob/main/docs/architecture/agent-factory.drawio).*

Expand Down
2 changes: 1 addition & 1 deletion website/static/images/diagrams/agent-factory.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion website/static/images/diagrams/ai-platform-2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading