This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/ci
Adam Moussa 03b9a94881
feat(agent-team): P3-flip Phase 1 — CI trust-boundary hardening (WIP, gated) (#34)
* feat(agent-team): P3-flip Phase 1 — expand denylist vectors (§4.2) + runner-trust assertion (§4.1)

First controls of the P3-live-flip Phase-1 CI hardening (workflow stays INERT;
this only tightens the trust boundary). Whole Phase-1 surface is gated by
/sh-security-review + GPT-4.1 cross-review before any flip.

§4.2 — expand the trust-control denylist with direct code-execution / supply-chain
vectors, kept byte-identical across all three copies (ci_gate.DENYLIST_GLOBS + the
guard + post-build inline DENY_GLOBS), drift-guarded:
  .gitmodules, .husky/**, .githooks/**, .gitattributes, .npmrc, and generated/build
  artifacts (__generated__, *.generated.*, dist/**, build/**, *.min.js).
Deliberate: lockfiles are NOT wholesale denied — lockfile-postinstall RCE is already
contained by the credential-less egress-blocked build sandbox, and the Tier-3 dep-CVE
fixer rewrites lockfiles to produce its draft PRs; a blanket deny would make it
un-shippable. Flagged in-code for the security gate. Direct code-execution config
(hooks/filters/npmrc/submodules) is the actual §4.2 RCE surface.

§4.1 — runner-trust: assert no job (esp. the privileged gate-and-pr) can run on a
self-hosted/user-provided runner; all must be GitHub-hosted.

998 tests pass, ruff clean.

* feat(agent-team): P3-flip Phase 1 — gate-weakening detector (§4.5)

A diff that ADDS a lint/type/coverage/security suppression (noqa, type: ignore,
pragma: no cover, nosec, nosemgrep), a test skip/xfail, or a hook bypass
(--no-verify) could make CI pass falsely. The pure-code gate now flags these via
gate_weakening_violations() and BLOCKs in evaluate_ci_gate as a top-priority trust
violation (step 1b, alongside the denylist) — regardless of the authenticated CI
conclusion. A build cannot pass itself by disabling its own checks; flagged diffs
escalate to a human. Only ADDED lines are inspected (removing a suppression is fine).

1015 tests pass, ruff clean.

* feat(agent-team): P3-flip — diff transport (§4.3) + flip privileged apply path live

Completes the box->CI diff handoff and flips the apply/verify privileged job
live (gated behind the agent-apply environment's required reviewer).

Transport (§4.3): the read-only box (D2) emits a diff but holds no write token.
- New credential-less `materialize` job decodes the untrusted `diff_b64`
  dispatch input via env (CWE-94), fail-closed re-hashes it against
  `expected_diff_hash`, and uploads it as the named artifact so guard/build-test
  download it same-run. guard now `needs: materialize`.
- New `dispatcher.py` (the trusted apply path, operator/Mac-side — never the
  box): pushes the diff as a head branch then `gh workflow run`s the workflow.
  Pure input-assembly (sha256 == sha256sum, b64 round-trip, head ref) is
  unit-tested; git/gh are injected seams. Push-before-dispatch; fail-closed on
  empty diff/scope, unsafe task_id/owner/repo.

Flip: gate-and-pr binds `environment: agent-apply` (required reviewer
amoussa1229) + grants exactly `pull-requests: write`; the App-token + draft-PR
steps run only on `steps.gate.outputs.gate == 'pass'` (no more if:false); the
draft PR opens with an explicit `--head`; task_id/head_branch charset-validated
(§4.6). Updated the hardening tests from inert-state to live-state assertions +
added transport tests. 1039 tests, ruff clean, workflow YAML valid.

NOTE: workflow only runs on manual workflow_dispatch and the privileged job is
held at the required-reviewer gate, so nothing privileged runs unapproved.

* fix(agent-team): P3-flip — address GPT-4.1 cross-review (size bound, ref-traversal guard)

- BLOCK: cap candidate diff at 40 KB in the dispatcher (the diff rides a base64
  workflow_dispatch input; GitHub caps inputs at ~64 KB so an oversized diff
  cannot dispatch at all) + a defense-in-depth decoded-size bound in materialize.
- FIX: harden the draft-PR HEAD_BRANCH guard to reject leading/trailing slash,
  '..' segments, and '//' (CWE-88 git ref-traversal), not just bad charset.
- NIT: document the mandatory invariants on gate-and-pr (required-reviewer
  environment must stay; runs-on must stay GitHub-hosted).
- QUESTION (lockfiles): answered in-code — the build-test sandbox is
  credential-less + egress-blocked, so lockfile-postinstall RCE is contained.
Tests added for all guards. 1042 tests, ruff clean, YAML valid.

* fix(agent-team): P3-flip — resolve /sh-security-review findings (LOGIC-1/2/3)

High-recall fan-out (injection/logic/iac+secrets) + proof-or-kill on the LIVE
apply path found 3 real issues the cross-review missed; all fixed:

- LOGIC-2 (HIGH, was a live hole): build-test ran `ruff check . || echo` /
  `pytest -q || echo`, swallowing failures so the job was always 'success' and
  the gate would open draft PRs on RED builds. ruff/pytest now run
  authoritatively under set -e (pytest exit 5 'no tests' is the only non-fatal
  case); the exit code IS the build-test conclusion the gate keys on.
- LOGIC-1 (verified!=shipped): the dispatcher used `git apply` + `git add -A`,
  staging stray untracked content into the pushed PR head. Now `git apply
  --index` stages exactly the diff, so the head tree is precisely base+diff —
  bound to the bytes CI hash-verified.
- LOGIC-3 (§4.5 on the live path): gate-weakening was enforced only box-side;
  added a gate-weakening check to the guard job so the live PR-opening path
  rejects a diff that adds suppressions/skips, even on a green build.

Injection / secrets / least-privilege / flip-correctness / no-untrusted-checkout
all came back clean. 1044 tests, ruff clean, YAML valid.
2026-06-22 18:51:52 -04:00
..
agent-team-apply-verify.yml feat(agent-team): P3-flip Phase 1 — CI trust-boundary hardening (WIP, gated) (#34) 2026-06-22 18:51:52 -04:00
README.md feat(agent-team): P3-live CI apply/verify hardening + ci_fetcher (gate-passed, provisioning-gated) (#17) 2026-06-18 15:53:26 -04:00

agent-team/ci — split-job CI apply/verify workflow (Plane-2 leaf)

Pre-deployment scaffolding for the R720 agent-team SDLC pipeline. This directory holds the split-job CI apply/verify workflow that turns a builder agent's untrusted candidate diff into a verified draft PR — the §3.3.2 trust boundary, Phase P3 (§7.1) of ../../docs/r720-agent-team-design.md.

STATUS: DEPLOY-GATED. NOT ENABLED, NOT PROVISIONED. This is authored as files only. Per the design (§3.3.2, §7.1 P3) the workflow must clear BOTH /sh-security-review AND the mandatory GPT-4.1 cross-review before deployment, because it is untrusted-input handling + a CI trust boundary. The privileged draft-PR step is hard-disabled (if: ${{ false }}); the ONLY remaining step to go live is the provisioning flip (create the GitHub App, the agent-apply environment with a required reviewer, and branch protection, then flip the step's if:). Nothing here is wired to a live org repo.

AUTH MODEL (LOCKED): GitHub App installation token — ZERO cloud credentials, no token-federation. The privileged gate-and-pr job authenticates with a GitHub App installation token (pull-requests: write) minted at run time by SHA-pinned actions/create-github-app-token. There is no AWS and no cloud-OIDC anywhere in this workflow. The required human reviewer lives on the agent-apply GitHub Environment (configured at provisioning, not in YAML).

Files

File What it is
agent-team-apply-verify.yml The split-job workflow. Self-contained: the load-bearing gate logic (diff integrity, trust-control denylist, declared-scope check, pure-code pass/fail) is embedded inline as stdlib-only, type-hinted Python heredocs, so the workflow has no external script dependency.
README.md This file.

The filename is kebab-case per the handbook. Deployment target (later, after the gates): promote into Sea-Haven-Industries/.github as a reusable workflow (engineering-handbook/cicd.md); the trusted apply path (which owns the GitHub App write token) invokes it.

The trust boundary (design §3.3.2)

The builder agents are semi-trusted: an LLM that read repo content can be wrong or prompt-injected, so the candidate diff is treated as untrusted code. The threat is that executing it in CI with org credentials lets a bad diff exfiltrate secrets, assume the deploy role, or tamper with other repos. The workflow implements all five boundaries:

  1. Split CI — untrusted execution is credential-less. The job that checks out and runs the diff (build-test) runs with permissions: contents: read, no secrets, no App token, no write token, and egress blocked (harden-runner). The patch executes only there, where there is nothing to steal and nothing to assume. Every privileged action (the GitHub App token mint, the draft-PR open) runs in a separate gate-and-pr job that never checks out or executes patch-controlled code — it consumes the build/test report as data only. There is no pull_request_target + head-ref checkout (the "pwn request" anti-pattern). The build-test job also runs a post-build denied-path check: after the build/test step, it diffs the working tree against the committed patched baseline and fails if a build hook (setup.py, conftest, postinstall, Makefile) wrote into the trust-control surface or out of declared scope — closing the build-hook write vector.
  2. Trust-control-surface denylist (CI-side hard fail). The guard job rejects any diff that touches .github/workflows/**, IAM/policy IaC (CDK/SAM/Terraform), branch-protection / CODEOWNERS / Dependabot config, or files outside the task's declared scope. The match is not naive: it canonicalizes paths, rejects parent-directory traversal, and inspects rename from/to headers, so a rename into a denied path — or path indirection — cannot bypass it. Such a diff is escalated to mandatory human + GPT cross-review, never auto-built.
  3. Diff integrity, box → CI. The builder records the candidate diff's sha256 in the task ledger (foundation agent_team.state_store content-hash idiom). CI re-hashes the diff and verifies it equals the recorded hash before applying (in both guard and again pre-apply in build-test). Precisely: this is an unkeyed hash that binds the bytes CI applies to the hash the dispatcher recorded — it detects accidental corruption or substitution of the artifact in transit between the trusted dispatcher and CI. It does not prove the diff matches the approved plan, and it cannot defend a compromised box that generates both the diff and its hash (see "Threat-model honesty" below). A keyed/attested binding to the approval record is a later hardening.
  4. Pure-code pass/fail gate over authenticated results. Mirroring secrev's "one pure-code script owns the block decision," the gate-and-pr gate reads only the authenticated needs.*.result job conclusions (GitHub-controlled, patch-independent) keyed to the run id + diff hash. It never trusts a success/failure file or artifact the patch could have written. The verifier agent only reads failures to propose the next fix; it cannot declare success.
  5. Branch protection is the final enforcement. The draft PR targets a protected branch; the locked-down checks are required; merge needs them green plus the security-review, the Claude Code App review, and human approval. The agent never auto-merges (D2).

Threat-model honesty (§3.3.2). The diff hash protects box → CI transit (tamper/substitution), not a box that is itself compromised — the box is in the trusted computing base for what it proposes. The real backstops against a bad box are the credential-less build job, the trust-control-surface denylist, and above all the human review of the draft PR plus required checks (nothing merges without Adam).

Job graph

workflow_dispatch (task_id, diff_artifact_name, expected_diff_hash, declared_scope)
        │
        ▼
   guard            contents:read, no secrets — validates the diff as DATA:
   (boundaries 2,3) re-hash + denylist + scope. Never applies it.
        │ (needs)
        ▼
   build-test       contents:read, no secrets, no App token, egress blocked —
   (boundary 1)     the ONLY job that applies + runs the UNTRUSTED patch.
        │ (needs)   Emits a NON-authoritative report artifact.
        ▼
   gate-and-pr      privileged home, but NEVER checks out patch code. Pure-code
   (boundaries 1,4,5) gate over authenticated needs.*.result → DRAFT PR
                    (hard-disabled until the review gates pass).

permissions: {} at the workflow level (least privilege); each job re-declares its own grant explicitly. The trigger is workflow_dispatch only — the patch never runs in a context carrying write or secret scope.

SHA-pinned actions (handbook Pinning Principle, §3.3.2)

Every third-party action is pinned to a full commit SHA with the human-readable tag in a trailing comment:

Action SHA Tag
actions/checkout 11bd71901bbe5b1630ceea73d27597364c9af683 v4.2.2
actions/download-artifact fa0a91b85d4f404e444e00e005971372dc801d16 v4.1.8
actions/upload-artifact b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 v4.4.3
actions/setup-python 0b93645e9fea7318ecaed2b359559ac225c90a2b v5.3.0
actions/create-github-app-token 5d869da34e18e7287c1daad50e0b8ea0f506ce69 v1.11.0
step-security/harden-runner 0080882f6c36860b6ba35c610c98ce87d4e2f26f v2.10.2

Relationship to the foundation

This leaf imports the committed Plane-2 foundation contracts verbatim (it does not redefine them):

  • The diff-hash recorded in the ledger and re-checked in CI is the same content-hash idiom as agent_team.state_store.compute_content_hash (§6.7).
  • The task this workflow verifies is an agent_team.task_model.TaskRecord; its candidate_diff + diff_hash fields (§3.3) are exactly the expected_diff_hash this workflow consumes, and ci_results is what the verifier writes back from the authenticated gate (boundary 4).
  • The ledger that records provenance (diff hash, run id, gate decision) is the agent_team.db schema (pending_questions / budget_ledger live there; per-task CI provenance is recorded against the task thread).

Tests

The workflow's embedded gate logic (diff integrity, the trust-control denylist with path-canonicalization + rename/copy/delete detection, the symlink-escape reject, and declared-scope enforcement) is stdlib-only, type-hinted, and ruff-clean, and is covered by a committed, runnable suite: ../tests/test_ci_gate_workflow.py extracts the inline guard script from this YAML and executes it against good and adversarial diffs — clean in-scope, hash mismatch, workflow delete, copy-into-denied, symlink addition, non-UTF-8, out-of-scope, unscoped, and escaping-scope. Run it with the rest of the suite: python3 -m pytest agent-team/tests/ -q from the repo root. (The claim that the gate is "verified" is therefore backed by that test, not by authoring alone.)

Deploy gating (do NOT skip)

Before this ships (§3.3.2, §7.1 P3):

  1. /sh-security-review over this workflow (untrusted-input handling + CI trust boundary).
  2. Mandatory GPT-4.1 cross-review of the workflow. (No IAM/cloud role is involved — the auth model is a GitHub App installation token, not OIDC/AWS.)
  3. Provisioning (the single remaining step to go live):
    • Create the GitHub App with a single permission (pull-requests: write), install it on the target repo, and store its id + private key as the AGENT_APPLY_APP_ID / AGENT_APPLY_APP_PRIVATE_KEY secrets.
    • Create the agent-apply GitHub Environment with a required reviewer (and optional wait timer) — this is the human gate, configured on the Environment, not in YAML.
    • Configure branch protection on the target branch (required checks + human approval).
    • Issue the read-only token for the CI-result fetcher (AGENT_TEAM_CI_READ_TOKEN, falling back to GITHUB_TOKEN).
  4. A documented, exercised rollback (uninstall the App, remove the environment, revert the workflow).
  5. Only then: flip the App-token + draft-PR steps' if: ${{ false }} to the live condition documented in the workflow (always() && needs.guard.result=='success' && needs.build-test.result=='success' && steps.gate.outputs.gate=='pass'), and promote to Sea-Haven-Industries/.github. Draft PRs only; never auto-merge.