Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
"""Pure-code CI pass/fail gate — the deterministic block decision (design §3.3.2).
|
|
|
|
|
|
|
|
|
|
This module is the §3.3.2 boundary #4: *"Pass/fail is a pure-code gate over
|
|
|
|
|
authenticated CI results, not the LLM verifier."* Mirroring secrev's "one
|
|
|
|
|
pure-code script owns the block decision", the gate reads the CI run
|
|
|
|
|
**conclusion** (already fetched, authenticated as the box read-only PAT via the
|
|
|
|
|
GitHub Checks/Actions API) keyed to a specific ``run_id`` + ``diff_hash`` and
|
|
|
|
|
makes a deterministic ``pass | fail | block`` decision. It consumes **only**
|
|
|
|
|
that authenticated, patch-independent conclusion; it never trusts a
|
|
|
|
|
success/failure file or artifact the patch could have written.
|
|
|
|
|
|
|
|
|
|
The gate also enforces the two box-side/CI defences that must hold before a
|
|
|
|
|
diff is even allowed to build (defence in depth — the CI side enforces the same
|
|
|
|
|
checks as a hard fail):
|
|
|
|
|
|
|
|
|
|
* **boundary #2** — the **trust-control-surface denylist**: a candidate diff
|
|
|
|
|
may not touch ``.github/workflows/**``, IAM/policy IaC, branch-protection /
|
|
|
|
|
``CODEOWNERS`` / Dependabot config, or paths outside the task's declared
|
|
|
|
|
scope. A match is canonicalized (symlink/``..`` resolution) and rejects
|
|
|
|
|
renames into denied paths, so a path match cannot be bypassed by indirection.
|
|
|
|
|
* **boundary #3** — **diff integrity**: the gate recomputes the candidate diff
|
|
|
|
|
hash and compares it to the ledger-recorded hash and to the hash CI verified,
|
|
|
|
|
so a tampered or substituted diff fails closed.
|
|
|
|
|
|
|
|
|
|
This module is pure stdlib and imports the committed foundation contracts
|
|
|
|
|
verbatim (it redefines none of them). It performs **no** network I/O: the
|
|
|
|
|
authenticated CI conclusion is passed in as data (the caller fetches it via the
|
|
|
|
|
read-only PAT). Keeping the gate I/O-free is what makes the block decision
|
|
|
|
|
deterministic and unit-testable.
|
|
|
|
|
|
|
|
|
|
Decision semantics (a diff "ships" only on an unambiguous authenticated pass):
|
|
|
|
|
|
|
|
|
|
* :data:`GateDecision.PASS` — the run concluded ``success`` for the exact
|
|
|
|
|
``run_id``/``diff_hash`` and every guard held; the verifier may open a draft
|
|
|
|
|
PR.
|
|
|
|
|
* :data:`GateDecision.FAIL` — the run concluded a recognised failure
|
|
|
|
|
(``failure``/``timed_out``/``cancelled``/...); the verifier loops back to the
|
|
|
|
|
builders.
|
|
|
|
|
* :data:`GateDecision.BLOCK` — a trust violation (denylist hit, hash mismatch,
|
|
|
|
|
run-id mismatch, missing/ambiguous authenticated conclusion). This is an
|
|
|
|
|
ALARM-worthy refuse-to-proceed, never a silent pass.
|
feat(agent-team): P3-live CI apply/verify hardening + ci_fetcher (gate-passed, provisioning-gated) (#17)
* feat(agent-team): read-only CI-result fetcher for P3 verify gate (opt-in, inert)
ci_fetcher.py: fail-closed CiResultFetcher reading the GitHub Actions run
conclusion via a read-only PAT (AGENT_TEAM_CI_READ_TOKEN→GITHUB_TOKEN), returns
{run_id,conclusion,diff_hash} or None on any error. Data-fetcher only — ci_gate
owns the verdict; never writes, no OIDC/AWS, never reads patch artifacts.
coordinator gains opt-in gated_build_verify_wiring() composing it via
bind_ci_result_fetcher; NOT wired into the default run-team.py path. 20 tests.
* harden(agent-team): P3 apply/verify workflow — GitHub App token, CWE-94, fail-closed
Decision-1 auth model: gate-and-pr uses a GitHub App installation token
(pull-requests:write) behind the agent-apply environment; ALL OIDC/id-token/AWS
removed. Hardening: task_id env-indirection (CWE-94 — GitHub expands ${{ }} into
the run shell before exec, so %s/quoting is insufficient); run-id pinning on both
download-artifact; post-build denied-path check (build-hook writes into denied
paths fail the job); empty-hash fail-closed in BOTH the embedded gate (fixed a
real ''=='' pass bug) and ci_gate.py. App-token + draft-PR steps stay if:${{ false }}
until provisioning (App + environment + branch protection). +17 tests.
* harden(agent-team): apply P3-live security-gate fixes (GPT-4.1 xreview + sh-security-review)
BLOCK-1/FIX-4: gate-and-pr re-comments pull-requests:write + environment:agent-apply
(provisioning-time uncomment) and gains needs.guard/build-test=='success' job guard —
zero privilege until provisioning. BLOCK-2/3+FIX-5: ci_fetcher validates run_id (^[0-9]{1,20}$),
owner/repo (^[A-Za-z0-9_.-]{1,100}$), and fetched_id (int) — fail closed, no SSRF/path
injection. FIX-1: conclusion allowlist. FIX-3: api_root removed from public builder (no
injectable endpoint). INJ-02: post-build denied-path check uses NUL-delimited git output +
explicit rename parsing, no backslash mangling, non-UTF8=violation. INJ-03: all three trust-
control denylists unified to one 22-entry union + drift-guard test. Q1: documented run_id/
diff_hash trust source (dispatcher/ledger only). 884 tests, ruff clean. Privileged steps stay
if:${{ false }} until provisioning.
* build(security-review): prune .claude worktrees from deterministic scanners
Agent worktrees under .claude/worktrees/ are full repo copies; the cfn-lint
find|xargs template scan overflowed ('command line cannot be assembled') and the
pre-push hook fail-closed to BLOCK whenever a worktree was present. Prune .claude
in the cfn-lint find + semgrep/checkov excludes, and gitignore .claude/ so it is
never scanned or committed. Unblocks main-tree pushes during parallel agent work.
2026-06-18 15:53:26 -04:00
|
|
|
|
|
|
|
|
TRUST SOURCE (§3.3.2, QUESTION-1). ``state["run_id"]`` (the run id the fetcher
|
|
|
|
|
reads and the value compared against ``expected_run_id`` here) and
|
|
|
|
|
``state["diff_hash"]`` MUST be written ONLY by the trusted dispatcher / ledger at
|
|
|
|
|
dispatch time — NEVER by an LLM / builder / verifier node or by anything a
|
|
|
|
|
candidate diff can influence. The dispatcher records the run id it dispatched the
|
|
|
|
|
apply/verify workflow under (and the ledger-recorded diff hash); the LLM nodes
|
|
|
|
|
only read them. The gate's run-id EQUALITY check below (``actual_run_id`` vs the
|
|
|
|
|
dispatcher-supplied ``expected_run_id``) plus the fetcher's run_id format
|
|
|
|
|
validation are the defense if that assumption is ever broken: a tampered
|
|
|
|
|
``state["run_id"]`` would still have to equal the dispatcher's expected run id to
|
|
|
|
|
pass, and a malformed value fails closed.
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from enum import Enum
|
|
|
|
|
from pathlib import PurePosixPath
|
|
|
|
|
from typing import Any, Mapping, Sequence
|
|
|
|
|
|
|
|
|
|
from agent_team.state_store import compute_content_hash
|
|
|
|
|
|
|
|
|
|
__all__ = [
|
|
|
|
|
"DENYLIST_GLOBS",
|
|
|
|
|
"CiGateError",
|
|
|
|
|
"GateDecision",
|
|
|
|
|
"GateResult",
|
|
|
|
|
"denylist_violations",
|
|
|
|
|
"diff_touched_paths",
|
|
|
|
|
"evaluate_ci_gate",
|
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
|
|
|
"gate_weakening_violations",
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
"verify_diff_hash",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CiGateError(Exception):
|
|
|
|
|
"""Raised when the gate is called with structurally invalid inputs.
|
|
|
|
|
|
|
|
|
|
Distinct from a :data:`GateDecision.BLOCK`: a ``BLOCK`` is a *valid* gate
|
|
|
|
|
run that found a trust violation, whereas this exception means the caller
|
|
|
|
|
handed the gate malformed data (e.g. a non-string diff). Fails loud rather
|
|
|
|
|
than guessing.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GateDecision(Enum):
|
|
|
|
|
"""The deterministic gate outcome (§3.3.2 boundary #4)."""
|
|
|
|
|
|
|
|
|
|
PASS = "pass"
|
|
|
|
|
FAIL = "fail"
|
|
|
|
|
BLOCK = "block"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Trust-control-surface denylist (§3.3.2 boundary #2). A candidate diff that
|
|
|
|
|
# touches any of these is escalated to mandatory human + GPT cross-review, never
|
|
|
|
|
# auto-built — they are the mandatory-cross-review surface regardless. Globs are
|
|
|
|
|
# matched against POSIX-canonicalized repo-relative paths.
|
feat(agent-team): P3-live CI apply/verify hardening + ci_fetcher (gate-passed, provisioning-gated) (#17)
* feat(agent-team): read-only CI-result fetcher for P3 verify gate (opt-in, inert)
ci_fetcher.py: fail-closed CiResultFetcher reading the GitHub Actions run
conclusion via a read-only PAT (AGENT_TEAM_CI_READ_TOKEN→GITHUB_TOKEN), returns
{run_id,conclusion,diff_hash} or None on any error. Data-fetcher only — ci_gate
owns the verdict; never writes, no OIDC/AWS, never reads patch artifacts.
coordinator gains opt-in gated_build_verify_wiring() composing it via
bind_ci_result_fetcher; NOT wired into the default run-team.py path. 20 tests.
* harden(agent-team): P3 apply/verify workflow — GitHub App token, CWE-94, fail-closed
Decision-1 auth model: gate-and-pr uses a GitHub App installation token
(pull-requests:write) behind the agent-apply environment; ALL OIDC/id-token/AWS
removed. Hardening: task_id env-indirection (CWE-94 — GitHub expands ${{ }} into
the run shell before exec, so %s/quoting is insufficient); run-id pinning on both
download-artifact; post-build denied-path check (build-hook writes into denied
paths fail the job); empty-hash fail-closed in BOTH the embedded gate (fixed a
real ''=='' pass bug) and ci_gate.py. App-token + draft-PR steps stay if:${{ false }}
until provisioning (App + environment + branch protection). +17 tests.
* harden(agent-team): apply P3-live security-gate fixes (GPT-4.1 xreview + sh-security-review)
BLOCK-1/FIX-4: gate-and-pr re-comments pull-requests:write + environment:agent-apply
(provisioning-time uncomment) and gains needs.guard/build-test=='success' job guard —
zero privilege until provisioning. BLOCK-2/3+FIX-5: ci_fetcher validates run_id (^[0-9]{1,20}$),
owner/repo (^[A-Za-z0-9_.-]{1,100}$), and fetched_id (int) — fail closed, no SSRF/path
injection. FIX-1: conclusion allowlist. FIX-3: api_root removed from public builder (no
injectable endpoint). INJ-02: post-build denied-path check uses NUL-delimited git output +
explicit rename parsing, no backslash mangling, non-UTF8=violation. INJ-03: all three trust-
control denylists unified to one 22-entry union + drift-guard test. Q1: documented run_id/
diff_hash trust source (dispatcher/ledger only). 884 tests, ruff clean. Privileged steps stay
if:${{ false }} until provisioning.
* build(security-review): prune .claude worktrees from deterministic scanners
Agent worktrees under .claude/worktrees/ are full repo copies; the cfn-lint
find|xargs template scan overflowed ('command line cannot be assembled') and the
pre-push hook fail-closed to BLOCK whenever a worktree was present. Prune .claude
in the cfn-lint find + semgrep/checkov excludes, and gitignore .claude/ so it is
never scanned or committed. Unblocks main-tree pushes during parallel agent work.
2026-06-18 15:53:26 -04:00
|
|
|
# INJ-03: this denylist is the UNION SUPERSET shared verbatim across all three
|
|
|
|
|
# trust-control copies — this tuple, the guard-job inline DENY_GLOBS, and the
|
|
|
|
|
# post-build inline DENY_GLOBS in ci/agent-team-apply-verify.yml. The three had
|
|
|
|
|
# drifted in BOTH directions (each carried entries the others lacked); they are
|
|
|
|
|
# now identical, and tests/test_apply_verify_workflow_hardening.py asserts the
|
|
|
|
|
# identity so any future drift fails CI. When editing one, edit all three.
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
DENYLIST_GLOBS: tuple[str, ...] = (
|
|
|
|
|
# CI workflow definitions — the "pwn request" surface.
|
|
|
|
|
".github/workflows/**",
|
|
|
|
|
".github/actions/**",
|
|
|
|
|
# Branch protection / ownership / dependency automation config.
|
|
|
|
|
".github/CODEOWNERS",
|
feat(agent-team): P3-live CI apply/verify hardening + ci_fetcher (gate-passed, provisioning-gated) (#17)
* feat(agent-team): read-only CI-result fetcher for P3 verify gate (opt-in, inert)
ci_fetcher.py: fail-closed CiResultFetcher reading the GitHub Actions run
conclusion via a read-only PAT (AGENT_TEAM_CI_READ_TOKEN→GITHUB_TOKEN), returns
{run_id,conclusion,diff_hash} or None on any error. Data-fetcher only — ci_gate
owns the verdict; never writes, no OIDC/AWS, never reads patch artifacts.
coordinator gains opt-in gated_build_verify_wiring() composing it via
bind_ci_result_fetcher; NOT wired into the default run-team.py path. 20 tests.
* harden(agent-team): P3 apply/verify workflow — GitHub App token, CWE-94, fail-closed
Decision-1 auth model: gate-and-pr uses a GitHub App installation token
(pull-requests:write) behind the agent-apply environment; ALL OIDC/id-token/AWS
removed. Hardening: task_id env-indirection (CWE-94 — GitHub expands ${{ }} into
the run shell before exec, so %s/quoting is insufficient); run-id pinning on both
download-artifact; post-build denied-path check (build-hook writes into denied
paths fail the job); empty-hash fail-closed in BOTH the embedded gate (fixed a
real ''=='' pass bug) and ci_gate.py. App-token + draft-PR steps stay if:${{ false }}
until provisioning (App + environment + branch protection). +17 tests.
* harden(agent-team): apply P3-live security-gate fixes (GPT-4.1 xreview + sh-security-review)
BLOCK-1/FIX-4: gate-and-pr re-comments pull-requests:write + environment:agent-apply
(provisioning-time uncomment) and gains needs.guard/build-test=='success' job guard —
zero privilege until provisioning. BLOCK-2/3+FIX-5: ci_fetcher validates run_id (^[0-9]{1,20}$),
owner/repo (^[A-Za-z0-9_.-]{1,100}$), and fetched_id (int) — fail closed, no SSRF/path
injection. FIX-1: conclusion allowlist. FIX-3: api_root removed from public builder (no
injectable endpoint). INJ-02: post-build denied-path check uses NUL-delimited git output +
explicit rename parsing, no backslash mangling, non-UTF8=violation. INJ-03: all three trust-
control denylists unified to one 22-entry union + drift-guard test. Q1: documented run_id/
diff_hash trust source (dispatcher/ledger only). 884 tests, ruff clean. Privileged steps stay
if:${{ false }} until provisioning.
* build(security-review): prune .claude worktrees from deterministic scanners
Agent worktrees under .claude/worktrees/ are full repo copies; the cfn-lint
find|xargs template scan overflowed ('command line cannot be assembled') and the
pre-push hook fail-closed to BLOCK whenever a worktree was present. Prune .claude
in the cfn-lint find + semgrep/checkov excludes, and gitignore .claude/ so it is
never scanned or committed. Unblocks main-tree pushes during parallel agent work.
2026-06-18 15:53:26 -04:00
|
|
|
"**/CODEOWNERS",
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
"CODEOWNERS",
|
|
|
|
|
".github/dependabot.yml",
|
|
|
|
|
".github/dependabot.yaml",
|
|
|
|
|
".github/settings.yml",
|
|
|
|
|
# IAM / policy IaC (CDK / SAM / Terraform / CloudFormation).
|
|
|
|
|
"**/cdk.json",
|
|
|
|
|
"**/template.yml",
|
|
|
|
|
"**/template.yaml",
|
|
|
|
|
"**/samconfig.toml",
|
|
|
|
|
"**/*.tf",
|
feat(agent-team): P3-live CI apply/verify hardening + ci_fetcher (gate-passed, provisioning-gated) (#17)
* feat(agent-team): read-only CI-result fetcher for P3 verify gate (opt-in, inert)
ci_fetcher.py: fail-closed CiResultFetcher reading the GitHub Actions run
conclusion via a read-only PAT (AGENT_TEAM_CI_READ_TOKEN→GITHUB_TOKEN), returns
{run_id,conclusion,diff_hash} or None on any error. Data-fetcher only — ci_gate
owns the verdict; never writes, no OIDC/AWS, never reads patch artifacts.
coordinator gains opt-in gated_build_verify_wiring() composing it via
bind_ci_result_fetcher; NOT wired into the default run-team.py path. 20 tests.
* harden(agent-team): P3 apply/verify workflow — GitHub App token, CWE-94, fail-closed
Decision-1 auth model: gate-and-pr uses a GitHub App installation token
(pull-requests:write) behind the agent-apply environment; ALL OIDC/id-token/AWS
removed. Hardening: task_id env-indirection (CWE-94 — GitHub expands ${{ }} into
the run shell before exec, so %s/quoting is insufficient); run-id pinning on both
download-artifact; post-build denied-path check (build-hook writes into denied
paths fail the job); empty-hash fail-closed in BOTH the embedded gate (fixed a
real ''=='' pass bug) and ci_gate.py. App-token + draft-PR steps stay if:${{ false }}
until provisioning (App + environment + branch protection). +17 tests.
* harden(agent-team): apply P3-live security-gate fixes (GPT-4.1 xreview + sh-security-review)
BLOCK-1/FIX-4: gate-and-pr re-comments pull-requests:write + environment:agent-apply
(provisioning-time uncomment) and gains needs.guard/build-test=='success' job guard —
zero privilege until provisioning. BLOCK-2/3+FIX-5: ci_fetcher validates run_id (^[0-9]{1,20}$),
owner/repo (^[A-Za-z0-9_.-]{1,100}$), and fetched_id (int) — fail closed, no SSRF/path
injection. FIX-1: conclusion allowlist. FIX-3: api_root removed from public builder (no
injectable endpoint). INJ-02: post-build denied-path check uses NUL-delimited git output +
explicit rename parsing, no backslash mangling, non-UTF8=violation. INJ-03: all three trust-
control denylists unified to one 22-entry union + drift-guard test. Q1: documented run_id/
diff_hash trust source (dispatcher/ledger only). 884 tests, ruff clean. Privileged steps stay
if:${{ false }} until provisioning.
* build(security-review): prune .claude worktrees from deterministic scanners
Agent worktrees under .claude/worktrees/ are full repo copies; the cfn-lint
find|xargs template scan overflowed ('command line cannot be assembled') and the
pre-push hook fail-closed to BLOCK whenever a worktree was present. Prune .claude
in the cfn-lint find + semgrep/checkov excludes, and gitignore .claude/ so it is
never scanned or committed. Unblocks main-tree pushes during parallel agent work.
2026-06-18 15:53:26 -04:00
|
|
|
"**/*-stack.ts",
|
|
|
|
|
"**/*_stack.py",
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
"**/iam/**",
|
|
|
|
|
"**/policies/**",
|
feat(agent-team): P3-live CI apply/verify hardening + ci_fetcher (gate-passed, provisioning-gated) (#17)
* feat(agent-team): read-only CI-result fetcher for P3 verify gate (opt-in, inert)
ci_fetcher.py: fail-closed CiResultFetcher reading the GitHub Actions run
conclusion via a read-only PAT (AGENT_TEAM_CI_READ_TOKEN→GITHUB_TOKEN), returns
{run_id,conclusion,diff_hash} or None on any error. Data-fetcher only — ci_gate
owns the verdict; never writes, no OIDC/AWS, never reads patch artifacts.
coordinator gains opt-in gated_build_verify_wiring() composing it via
bind_ci_result_fetcher; NOT wired into the default run-team.py path. 20 tests.
* harden(agent-team): P3 apply/verify workflow — GitHub App token, CWE-94, fail-closed
Decision-1 auth model: gate-and-pr uses a GitHub App installation token
(pull-requests:write) behind the agent-apply environment; ALL OIDC/id-token/AWS
removed. Hardening: task_id env-indirection (CWE-94 — GitHub expands ${{ }} into
the run shell before exec, so %s/quoting is insufficient); run-id pinning on both
download-artifact; post-build denied-path check (build-hook writes into denied
paths fail the job); empty-hash fail-closed in BOTH the embedded gate (fixed a
real ''=='' pass bug) and ci_gate.py. App-token + draft-PR steps stay if:${{ false }}
until provisioning (App + environment + branch protection). +17 tests.
* harden(agent-team): apply P3-live security-gate fixes (GPT-4.1 xreview + sh-security-review)
BLOCK-1/FIX-4: gate-and-pr re-comments pull-requests:write + environment:agent-apply
(provisioning-time uncomment) and gains needs.guard/build-test=='success' job guard —
zero privilege until provisioning. BLOCK-2/3+FIX-5: ci_fetcher validates run_id (^[0-9]{1,20}$),
owner/repo (^[A-Za-z0-9_.-]{1,100}$), and fetched_id (int) — fail closed, no SSRF/path
injection. FIX-1: conclusion allowlist. FIX-3: api_root removed from public builder (no
injectable endpoint). INJ-02: post-build denied-path check uses NUL-delimited git output +
explicit rename parsing, no backslash mangling, non-UTF8=violation. INJ-03: all three trust-
control denylists unified to one 22-entry union + drift-guard test. Q1: documented run_id/
diff_hash trust source (dispatcher/ledger only). 884 tests, ruff clean. Privileged steps stay
if:${{ false }} until provisioning.
* build(security-review): prune .claude worktrees from deterministic scanners
Agent worktrees under .claude/worktrees/ are full repo copies; the cfn-lint
find|xargs template scan overflowed ('command line cannot be assembled') and the
pre-push hook fail-closed to BLOCK whenever a worktree was present. Prune .claude
in the cfn-lint find + semgrep/checkov excludes, and gitignore .claude/ so it is
never scanned or committed. Unblocks main-tree pushes during parallel agent work.
2026-06-18 15:53:26 -04:00
|
|
|
"**/*iam*",
|
|
|
|
|
"**/policy*.json",
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
"**/*policy*.json",
|
feat(agent-team): P3-live CI apply/verify hardening + ci_fetcher (gate-passed, provisioning-gated) (#17)
* feat(agent-team): read-only CI-result fetcher for P3 verify gate (opt-in, inert)
ci_fetcher.py: fail-closed CiResultFetcher reading the GitHub Actions run
conclusion via a read-only PAT (AGENT_TEAM_CI_READ_TOKEN→GITHUB_TOKEN), returns
{run_id,conclusion,diff_hash} or None on any error. Data-fetcher only — ci_gate
owns the verdict; never writes, no OIDC/AWS, never reads patch artifacts.
coordinator gains opt-in gated_build_verify_wiring() composing it via
bind_ci_result_fetcher; NOT wired into the default run-team.py path. 20 tests.
* harden(agent-team): P3 apply/verify workflow — GitHub App token, CWE-94, fail-closed
Decision-1 auth model: gate-and-pr uses a GitHub App installation token
(pull-requests:write) behind the agent-apply environment; ALL OIDC/id-token/AWS
removed. Hardening: task_id env-indirection (CWE-94 — GitHub expands ${{ }} into
the run shell before exec, so %s/quoting is insufficient); run-id pinning on both
download-artifact; post-build denied-path check (build-hook writes into denied
paths fail the job); empty-hash fail-closed in BOTH the embedded gate (fixed a
real ''=='' pass bug) and ci_gate.py. App-token + draft-PR steps stay if:${{ false }}
until provisioning (App + environment + branch protection). +17 tests.
* harden(agent-team): apply P3-live security-gate fixes (GPT-4.1 xreview + sh-security-review)
BLOCK-1/FIX-4: gate-and-pr re-comments pull-requests:write + environment:agent-apply
(provisioning-time uncomment) and gains needs.guard/build-test=='success' job guard —
zero privilege until provisioning. BLOCK-2/3+FIX-5: ci_fetcher validates run_id (^[0-9]{1,20}$),
owner/repo (^[A-Za-z0-9_.-]{1,100}$), and fetched_id (int) — fail closed, no SSRF/path
injection. FIX-1: conclusion allowlist. FIX-3: api_root removed from public builder (no
injectable endpoint). INJ-02: post-build denied-path check uses NUL-delimited git output +
explicit rename parsing, no backslash mangling, non-UTF8=violation. INJ-03: all three trust-
control denylists unified to one 22-entry union + drift-guard test. Q1: documented run_id/
diff_hash trust source (dispatcher/ledger only). 884 tests, ruff clean. Privileged steps stay
if:${{ false }} until provisioning.
* build(security-review): prune .claude worktrees from deterministic scanners
Agent worktrees under .claude/worktrees/ are full repo copies; the cfn-lint
find|xargs template scan overflowed ('command line cannot be assembled') and the
pre-push hook fail-closed to BLOCK whenever a worktree was present. Prune .claude
in the cfn-lint find + semgrep/checkov excludes, and gitignore .claude/ so it is
never scanned or committed. Unblocks main-tree pushes during parallel agent work.
2026-06-18 15:53:26 -04:00
|
|
|
"**/*.pem",
|
|
|
|
|
"**/*.key",
|
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
|
|
|
# --- P3-flip §4.2: direct code-execution / supply-chain vectors ---
|
|
|
|
|
# Submodule pointers / config — a changed submodule pulls in arbitrary
|
|
|
|
|
# external code at the pinned commit.
|
|
|
|
|
".gitmodules",
|
|
|
|
|
"**/.gitmodules",
|
|
|
|
|
# Git hook directories / hook-path redirection — code that runs on git ops.
|
|
|
|
|
# (`.git/hooks` itself is never in a checkout; `.husky` / `.githooks` are the
|
|
|
|
|
# in-tree hook dirs a repo points `core.hooksPath` at.)
|
|
|
|
|
".husky/**",
|
|
|
|
|
"**/.husky/**",
|
|
|
|
|
".githooks/**",
|
|
|
|
|
"**/.githooks/**",
|
|
|
|
|
# .gitattributes — clean/smudge filters execute arbitrary processes on checkout.
|
|
|
|
|
".gitattributes",
|
|
|
|
|
"**/.gitattributes",
|
|
|
|
|
# npm/registry config — can inject install scripts, a hostile registry, or auth.
|
|
|
|
|
".npmrc",
|
|
|
|
|
"**/.npmrc",
|
|
|
|
|
# Generated / build artifacts — codegen output is not reviewable as source, so
|
|
|
|
|
# a diff that writes into it is escalated to a human rather than auto-built.
|
|
|
|
|
"**/__generated__/**",
|
|
|
|
|
"**/*.generated.*",
|
|
|
|
|
"**/dist/**",
|
|
|
|
|
"**/build/**",
|
|
|
|
|
"**/*.min.js",
|
|
|
|
|
# NOTE (deliberate, for the security gate): lockfiles are NOT wholesale denied
|
|
|
|
|
# here. Lockfile-postinstall RCE is already contained by the credential-less,
|
|
|
|
|
# egress-blocked build-test sandbox, and the Tier-3 dep-CVE fixer legitimately
|
|
|
|
|
# rewrites lockfiles to produce its draft PRs — a wholesale lockfile deny would
|
|
|
|
|
# make the fixer un-shippable. The direct code-execution config above (hooks,
|
|
|
|
|
# filters, .npmrc, submodules) is the actual §4.2 RCE surface. Revisit if the
|
|
|
|
|
# fixer's lockfile writes ever need a scoped allow vs a general deny.
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Authenticated GitHub run conclusions that count as a recognised failure (the
|
|
|
|
|
# verifier loops back). Anything not in PASS/this set is ambiguous -> BLOCK.
|
|
|
|
|
_FAILURE_CONCLUSIONS: frozenset[str] = frozenset(
|
|
|
|
|
{"failure", "timed_out", "cancelled", "action_required", "stale", "startup_failure"}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# The single authenticated conclusion that means "ship-able".
|
|
|
|
|
_SUCCESS_CONCLUSION = "success"
|
|
|
|
|
|
|
|
|
|
# ``git diff`` file-header line, e.g. ``diff --git a/foo.py b/foo.py``. We read
|
|
|
|
|
# the post-image (``b/``) path as the touched path and also surface the pre-image
|
|
|
|
|
# (``a/``) so a *rename into* a denied path is caught (boundary #2).
|
|
|
|
|
_DIFF_GIT_RE = re.compile(r"^diff --git a/(?P<a>.+?) b/(?P<b>.+?)\s*$")
|
|
|
|
|
# ``rename from``/``rename to`` lines carry the rename source/target explicitly.
|
|
|
|
|
_RENAME_FROM_RE = re.compile(r"^rename from (?P<path>.+?)\s*$")
|
|
|
|
|
_RENAME_TO_RE = re.compile(r"^rename to (?P<path>.+?)\s*$")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class GateResult:
|
|
|
|
|
"""The gate's deterministic verdict plus the evidence behind it.
|
|
|
|
|
|
|
|
|
|
``decision`` is the block decision the verifier node acts on. ``reasons``
|
|
|
|
|
enumerates every concrete trigger (denylist hits, hash mismatch, the CI
|
|
|
|
|
conclusion consumed) so the decision is auditable and an ALARM can quote it.
|
|
|
|
|
``run_id``/``diff_hash`` echo the keys the gate was bound to (provenance,
|
|
|
|
|
§3.3.2). ``ci_conclusion`` is the authenticated conclusion actually
|
|
|
|
|
consumed.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
decision: GateDecision
|
|
|
|
|
reasons: list[str] = field(default_factory=list)
|
|
|
|
|
run_id: str | None = None
|
|
|
|
|
diff_hash: str | None = None
|
|
|
|
|
ci_conclusion: str | None = None
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def passed(self) -> bool:
|
|
|
|
|
"""``True`` only on an unambiguous authenticated pass."""
|
|
|
|
|
return self.decision is GateDecision.PASS
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def blocked(self) -> bool:
|
|
|
|
|
"""``True`` on a trust violation (ALARM-worthy refuse-to-proceed)."""
|
|
|
|
|
return self.decision is GateDecision.BLOCK
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _canonical_repo_path(raw: str) -> str:
|
|
|
|
|
"""Canonicalize a repo-relative path for denylist matching (§3.3.2).
|
|
|
|
|
|
|
|
|
|
Strips a leading ``a/``/``b/`` git prefix, normalizes separators, resolves
|
|
|
|
|
``.``/``..`` segments without touching the filesystem (the diff describes
|
|
|
|
|
paths that may not exist locally), and drops a leading ``/`` so the result
|
|
|
|
|
is always repo-relative. A path that escapes the repo root via ``..`` is
|
|
|
|
|
returned with a sentinel ``..`` prefix preserved so it cannot silently match
|
|
|
|
|
*nothing* — the caller treats an escaping path as a denylist hit.
|
|
|
|
|
"""
|
|
|
|
|
text = raw.strip().strip('"')
|
|
|
|
|
# Drop a single git a//b/ prefix if present.
|
|
|
|
|
if text.startswith(("a/", "b/")):
|
|
|
|
|
text = text[2:]
|
|
|
|
|
# PurePosixPath normalizes separators; resolve . and .. logically.
|
|
|
|
|
parts: list[str] = []
|
|
|
|
|
for segment in PurePosixPath(text).parts:
|
|
|
|
|
if segment in ("", "."):
|
|
|
|
|
continue
|
|
|
|
|
if segment == "..":
|
|
|
|
|
# Escaping the repo root — keep the marker so it never matches a
|
|
|
|
|
# benign glob and is treated as suspicious by the caller.
|
|
|
|
|
parts.append("..")
|
|
|
|
|
continue
|
|
|
|
|
parts.append(segment)
|
|
|
|
|
return "/".join(parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def diff_touched_paths(unified_diff: str) -> list[str]:
|
|
|
|
|
"""Extract the canonicalized repo-relative paths a unified diff touches.
|
|
|
|
|
|
|
|
|
|
Reads ``diff --git`` headers (both the ``a/`` pre-image and ``b/``
|
|
|
|
|
post-image) plus explicit ``rename from``/``rename to`` lines, so a rename
|
|
|
|
|
*into* a denied path is surfaced (§3.3.2 boundary #2 — "rejects renames into
|
|
|
|
|
denied paths"). Returns a de-duplicated, sorted list of canonical paths.
|
|
|
|
|
|
|
|
|
|
Raises :class:`CiGateError` if ``unified_diff`` is not a string.
|
|
|
|
|
"""
|
|
|
|
|
if not isinstance(unified_diff, str):
|
|
|
|
|
raise CiGateError(f"diff must be str, got {type(unified_diff).__name__}")
|
|
|
|
|
|
|
|
|
|
touched: set[str] = set()
|
|
|
|
|
for line in unified_diff.splitlines():
|
|
|
|
|
m = _DIFF_GIT_RE.match(line)
|
|
|
|
|
if m is not None:
|
|
|
|
|
touched.add(_canonical_repo_path(m.group("a")))
|
|
|
|
|
touched.add(_canonical_repo_path(m.group("b")))
|
|
|
|
|
continue
|
|
|
|
|
m = _RENAME_FROM_RE.match(line)
|
|
|
|
|
if m is not None:
|
|
|
|
|
touched.add(_canonical_repo_path(m.group("path")))
|
|
|
|
|
continue
|
|
|
|
|
m = _RENAME_TO_RE.match(line)
|
|
|
|
|
if m is not None:
|
|
|
|
|
touched.add(_canonical_repo_path(m.group("path")))
|
|
|
|
|
touched.discard("")
|
|
|
|
|
return sorted(touched)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _glob_to_regex(glob: str) -> re.Pattern[str]:
|
|
|
|
|
"""Compile a denylist glob to an anchored regex.
|
|
|
|
|
|
|
|
|
|
Supports ``**`` (any number of path segments, including zero), ``*`` (within
|
|
|
|
|
a single segment), and ``?``. Everything else is matched literally. Matching
|
|
|
|
|
is done on canonical POSIX repo-relative paths.
|
|
|
|
|
"""
|
|
|
|
|
out: list[str] = ["^"]
|
|
|
|
|
i = 0
|
|
|
|
|
n = len(glob)
|
|
|
|
|
while i < n:
|
|
|
|
|
ch = glob[i]
|
|
|
|
|
if ch == "*":
|
|
|
|
|
if i + 1 < n and glob[i + 1] == "*":
|
|
|
|
|
# ``**`` — any chars incl. ``/``. Swallow an optional trailing
|
|
|
|
|
# ``/`` so ``dir/**`` also matches ``dir`` itself's children
|
|
|
|
|
# without requiring a separator artifact.
|
|
|
|
|
out.append(".*")
|
|
|
|
|
i += 2
|
|
|
|
|
if i < n and glob[i] == "/":
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
# single ``*`` — anything but a path separator.
|
|
|
|
|
out.append("[^/]*")
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
if ch == "?":
|
|
|
|
|
out.append("[^/]")
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
out.append(re.escape(ch))
|
|
|
|
|
i += 1
|
|
|
|
|
out.append("$")
|
|
|
|
|
return re.compile("".join(out))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_DENYLIST_RES: tuple[tuple[str, re.Pattern[str]], ...] = tuple(
|
|
|
|
|
(g, _glob_to_regex(g)) for g in DENYLIST_GLOBS
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def denylist_violations(
|
|
|
|
|
unified_diff: str,
|
|
|
|
|
*,
|
|
|
|
|
allowed_scope: Sequence[str] | None = None,
|
|
|
|
|
) -> list[str]:
|
|
|
|
|
"""Return the trust-control-surface violations in ``unified_diff`` (§3.3.2).
|
|
|
|
|
|
|
|
|
|
A violation is any touched path that (a) matches a :data:`DENYLIST_GLOBS`
|
|
|
|
|
entry, (b) escapes the repo root via ``..`` (path-indirection attempt), or
|
|
|
|
|
(c) — when ``allowed_scope`` is given — falls outside the task's declared
|
|
|
|
|
scope. ``allowed_scope`` is a sequence of canonical path prefixes
|
|
|
|
|
(directories or exact files) the task is allowed to modify; a touched path
|
|
|
|
|
outside every prefix is a violation ("files outside the task's declared
|
|
|
|
|
scope").
|
|
|
|
|
|
|
|
|
|
Returns a sorted list of human-readable reason strings; an empty list means
|
|
|
|
|
the diff is clean for the denylist boundary.
|
|
|
|
|
"""
|
|
|
|
|
violations: list[str] = []
|
|
|
|
|
normalized_scope = (
|
|
|
|
|
[_canonical_repo_path(p) for p in allowed_scope]
|
|
|
|
|
if allowed_scope is not None
|
|
|
|
|
else None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for path in diff_touched_paths(unified_diff):
|
|
|
|
|
if ".." in PurePosixPath(path).parts:
|
|
|
|
|
violations.append(f"path escapes repo root via '..': {path!r}")
|
|
|
|
|
continue
|
|
|
|
|
for glob, pattern in _DENYLIST_RES:
|
|
|
|
|
if pattern.match(path):
|
|
|
|
|
violations.append(f"denylisted path {path!r} matches glob {glob!r}")
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
if normalized_scope is not None and not _within_scope(
|
|
|
|
|
path, normalized_scope
|
|
|
|
|
):
|
|
|
|
|
violations.append(f"path {path!r} is outside the task's declared scope")
|
|
|
|
|
return sorted(violations)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _within_scope(path: str, scope: Sequence[str]) -> bool:
|
|
|
|
|
"""Return ``True`` if ``path`` is within one declared-scope prefix."""
|
|
|
|
|
candidate = PurePosixPath(path)
|
|
|
|
|
for prefix in scope:
|
|
|
|
|
if not prefix:
|
|
|
|
|
continue
|
|
|
|
|
if path == prefix:
|
|
|
|
|
return True
|
|
|
|
|
prefix_path = PurePosixPath(prefix)
|
|
|
|
|
try:
|
|
|
|
|
candidate.relative_to(prefix_path)
|
|
|
|
|
return True
|
|
|
|
|
except ValueError:
|
|
|
|
|
continue
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
# §4.5 gate-weakening: substrings whose appearance on an ADDED diff line means the
|
|
|
|
|
# change is suppressing a quality/security gate (a self-passing move). Matched
|
|
|
|
|
# case-insensitively against added-line content.
|
|
|
|
|
_GATE_WEAKENING_MARKERS: tuple[tuple[str, str], ...] = (
|
|
|
|
|
("noqa", "ruff/flake8 lint suppression (noqa)"),
|
|
|
|
|
("type: ignore", "type-check suppression (type: ignore)"),
|
|
|
|
|
("type:ignore", "type-check suppression (type:ignore)"),
|
|
|
|
|
("pragma: no cover", "coverage suppression (pragma: no cover)"),
|
|
|
|
|
("pragma: no-cover", "coverage suppression (pragma: no-cover)"),
|
|
|
|
|
("nosec", "bandit security suppression (nosec)"),
|
|
|
|
|
("nosemgrep", "semgrep suppression (nosemgrep)"),
|
|
|
|
|
("--no-verify", "git/pre-commit hook bypass (--no-verify)"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Test skip/xfail decorators and calls (carry args, so regex not substring).
|
|
|
|
|
_TEST_SKIP_RE: re.Pattern[str] = re.compile(
|
|
|
|
|
r"@(?:pytest\.mark\.(?:skip|xfail)|unittest\.skip\w*)\b"
|
|
|
|
|
r"|\bpytest\.(?:skip|xfail)\s*\("
|
|
|
|
|
r"|\.skipTest\s*\("
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _added_diff_lines(unified_diff: str) -> list[str]:
|
|
|
|
|
"""Return the content of lines ADDED by a unified diff (excluding ``+++`` headers)."""
|
|
|
|
|
if not isinstance(unified_diff, str):
|
|
|
|
|
raise CiGateError(
|
|
|
|
|
f"unified_diff must be str, got {type(unified_diff).__name__}"
|
|
|
|
|
)
|
|
|
|
|
return [
|
|
|
|
|
line[1:]
|
|
|
|
|
for line in unified_diff.splitlines()
|
|
|
|
|
if line.startswith("+") and not line.startswith("+++")
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def gate_weakening_violations(unified_diff: str) -> list[str]:
|
|
|
|
|
"""Return reasons the diff WEAKENS a quality/security gate (empty if none).
|
|
|
|
|
|
|
|
|
|
A diff that ADDS a lint/type/coverage/security suppression, a test
|
|
|
|
|
skip/xfail, or a hook bypass could make CI pass falsely — so the pure-code
|
|
|
|
|
gate flags it even when the authenticated CI conclusion is ``success``
|
|
|
|
|
(§4.5). Such diffs are escalated to a human, never auto-built. Only ADDED
|
|
|
|
|
lines are inspected (removing a suppression is fine); matching is
|
|
|
|
|
case-insensitive on the marker text.
|
|
|
|
|
"""
|
|
|
|
|
violations: list[str] = []
|
|
|
|
|
for content in _added_diff_lines(unified_diff):
|
|
|
|
|
low = content.lower()
|
|
|
|
|
snippet = content.strip()[:120]
|
|
|
|
|
for marker, desc in _GATE_WEAKENING_MARKERS:
|
|
|
|
|
if marker in low:
|
|
|
|
|
violations.append(f"gate-weakening: added {desc}: {snippet!r}")
|
|
|
|
|
if _TEST_SKIP_RE.search(content):
|
|
|
|
|
violations.append(f"gate-weakening: added a test skip/xfail: {snippet!r}")
|
|
|
|
|
return violations
|
|
|
|
|
|
|
|
|
|
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
def verify_diff_hash(
|
|
|
|
|
candidate_diff: str,
|
|
|
|
|
*,
|
|
|
|
|
ledger_hash: str | None,
|
|
|
|
|
ci_verified_hash: str | None = None,
|
|
|
|
|
) -> bool:
|
|
|
|
|
"""Verify the candidate diff hash matches the ledger (and CI) (§3.3.2 #3).
|
|
|
|
|
|
|
|
|
|
Recomputes the content hash of ``candidate_diff`` (sha256, via the committed
|
|
|
|
|
:func:`agent_team.state_store.compute_content_hash`) and compares it to the
|
|
|
|
|
``ledger_hash`` recorded by the builder and, when supplied, to the
|
|
|
|
|
``ci_verified_hash`` CI checked before applying. Comparison is
|
|
|
|
|
constant-time. Returns ``True`` only when all supplied hashes agree; a
|
|
|
|
|
``None`` ledger hash is treated as a failure (there is nothing to bind to).
|
|
|
|
|
|
|
|
|
|
Raises :class:`CiGateError` if ``candidate_diff`` is not a string.
|
|
|
|
|
"""
|
|
|
|
|
if not isinstance(candidate_diff, str):
|
|
|
|
|
raise CiGateError(
|
|
|
|
|
f"candidate_diff must be str, got {type(candidate_diff).__name__}"
|
|
|
|
|
)
|
|
|
|
|
if not ledger_hash:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
actual = compute_content_hash(candidate_diff.encode("utf-8"))
|
|
|
|
|
if not _consteq(actual, ledger_hash):
|
|
|
|
|
return False
|
|
|
|
|
if ci_verified_hash is not None and not _consteq(actual, ci_verified_hash):
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _consteq(a: str, b: str) -> bool:
|
|
|
|
|
"""Constant-time string compare (hashes are not secret, but be tidy)."""
|
|
|
|
|
if len(a) != len(b):
|
|
|
|
|
return False
|
|
|
|
|
result = 0
|
|
|
|
|
for x, y in zip(a, b):
|
|
|
|
|
result |= ord(x) ^ ord(y)
|
|
|
|
|
return result == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def evaluate_ci_gate(
|
|
|
|
|
*,
|
|
|
|
|
candidate_diff: str,
|
|
|
|
|
ledger_hash: str | None,
|
|
|
|
|
ci_result: Mapping[str, Any] | None,
|
2026-06-23 17:51:58 -04:00
|
|
|
expected_run_id: str | None,
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
allowed_scope: Sequence[str] | None = None,
|
|
|
|
|
) -> GateResult:
|
|
|
|
|
"""Make the deterministic pass/fail/block decision (§3.3.2 boundary #4).
|
|
|
|
|
|
|
|
|
|
Inputs (all authenticated/patch-independent — the gate does no I/O):
|
|
|
|
|
|
|
|
|
|
* ``candidate_diff`` — the builder's diff text (re-hashed here).
|
|
|
|
|
* ``ledger_hash`` — the hash the builder recorded in the task ledger.
|
|
|
|
|
* ``ci_result`` — the authenticated CI conclusion the caller fetched via the
|
|
|
|
|
read-only PAT (GitHub Checks/Actions API). The gate reads only
|
|
|
|
|
``run_id``, ``conclusion``, and (optionally) ``diff_hash`` from it; it
|
|
|
|
|
**never** reads a patch-written success file/artifact.
|
2026-06-23 17:51:58 -04:00
|
|
|
* ``expected_run_id`` — the run id THIS task dispatched the apply/verify
|
|
|
|
|
workflow under (per-task, sourced from ``state["run_id"]`` at node-run
|
|
|
|
|
time, NOT a static wiring-time constant). The conclusion must be keyed to
|
|
|
|
|
it (a stale/substituted run id is a BLOCK). A ``None``/empty value means
|
|
|
|
|
the dispatcher captured no run id to bind to (e.g. the run-id poll fell
|
|
|
|
|
through) — there is nothing to anchor the verdict against, so the gate
|
|
|
|
|
BLOCKs (refuse-to-proceed, never a vacuous pass).
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
* ``allowed_scope`` — optional declared-scope prefixes for the task.
|
|
|
|
|
|
|
|
|
|
Decision order (a trust violation always wins over a CI verdict):
|
|
|
|
|
|
2026-06-23 17:51:58 -04:00
|
|
|
1. **Missing binding** — a ``None``/empty ``expected_run_id`` ->
|
|
|
|
|
:data:`GateDecision.BLOCK` (no per-task run to bind the verdict to).
|
|
|
|
|
2. **Denylist / scope** — any violation -> :data:`GateDecision.BLOCK`.
|
|
|
|
|
3. **Diff-hash integrity** — hash mismatch (ledger or CI-verified) ->
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
``BLOCK``.
|
2026-06-23 17:51:58 -04:00
|
|
|
4. **Authenticated conclusion** — missing result, a ``run_id`` that does not
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
match ``expected_run_id``, or an unrecognised/ambiguous conclusion ->
|
|
|
|
|
``BLOCK``; a recognised failure -> :data:`GateDecision.FAIL`; ``success``
|
|
|
|
|
-> :data:`GateDecision.PASS`.
|
|
|
|
|
|
|
|
|
|
Returns a :class:`GateResult` with the decision and the reasons behind it.
|
2026-06-23 17:51:58 -04:00
|
|
|
Raises :class:`CiGateError` on structurally invalid inputs (a non-string
|
|
|
|
|
``expected_run_id`` is structurally invalid; ``None``/empty is a BLOCK, not
|
|
|
|
|
an exception, because it is the legitimate "no run captured" runtime state).
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
"""
|
2026-06-23 17:51:58 -04:00
|
|
|
if expected_run_id is not None and not isinstance(expected_run_id, str):
|
|
|
|
|
raise CiGateError("expected_run_id must be a string or None")
|
|
|
|
|
|
|
|
|
|
# (0) Missing per-task binding: the dispatcher captured no run id for this
|
|
|
|
|
# task (None/empty). There is nothing to anchor the verdict to, so refuse to
|
|
|
|
|
# proceed rather than gating against an empty string (which a substituted
|
|
|
|
|
# ``ci_result`` with no/empty run_id could otherwise vacuously satisfy).
|
|
|
|
|
if not expected_run_id:
|
|
|
|
|
return GateResult(
|
|
|
|
|
decision=GateDecision.BLOCK,
|
|
|
|
|
reasons=["no per-task run_id to bind the verdict to (dispatch unresolved)"],
|
|
|
|
|
run_id=None,
|
|
|
|
|
diff_hash=ledger_hash,
|
|
|
|
|
ci_conclusion=None,
|
|
|
|
|
)
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
|
|
|
|
|
reasons: list[str] = []
|
|
|
|
|
|
|
|
|
|
# (1) Trust-control-surface denylist + declared scope. Highest priority:
|
|
|
|
|
# these files are the mandatory-cross-review surface, never auto-built.
|
|
|
|
|
violations = denylist_violations(candidate_diff, allowed_scope=allowed_scope)
|
|
|
|
|
if violations:
|
|
|
|
|
reasons.extend(violations)
|
|
|
|
|
return GateResult(
|
|
|
|
|
decision=GateDecision.BLOCK,
|
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
|
|
|
reasons=reasons,
|
|
|
|
|
run_id=expected_run_id,
|
|
|
|
|
diff_hash=ledger_hash,
|
|
|
|
|
ci_conclusion=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# (1b) Gate-weakening (§4.5): a diff that suppresses a lint/type/coverage/
|
|
|
|
|
# security check or skips tests could make CI pass falsely -> BLOCK regardless
|
|
|
|
|
# of the authenticated conclusion. A build cannot pass itself by disabling the
|
|
|
|
|
# checks; these escalate to a human.
|
|
|
|
|
weakening = gate_weakening_violations(candidate_diff)
|
|
|
|
|
if weakening:
|
|
|
|
|
reasons.extend(weakening)
|
|
|
|
|
return GateResult(
|
|
|
|
|
decision=GateDecision.BLOCK,
|
Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.
Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled
KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven
Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 14:17:44 -04:00
|
|
|
reasons=reasons,
|
|
|
|
|
run_id=expected_run_id,
|
|
|
|
|
diff_hash=ledger_hash,
|
|
|
|
|
ci_conclusion=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# (2) Diff-hash integrity: bind the decision to the exact bytes the ledger
|
|
|
|
|
# recorded (and, if present, what CI verified before applying).
|
|
|
|
|
ci_verified_hash = (
|
|
|
|
|
str(ci_result.get("diff_hash"))
|
|
|
|
|
if ci_result is not None and ci_result.get("diff_hash") is not None
|
|
|
|
|
else None
|
|
|
|
|
)
|
|
|
|
|
if not verify_diff_hash(
|
|
|
|
|
candidate_diff,
|
|
|
|
|
ledger_hash=ledger_hash,
|
|
|
|
|
ci_verified_hash=ci_verified_hash,
|
|
|
|
|
):
|
|
|
|
|
reasons.append(
|
|
|
|
|
"diff-hash mismatch: candidate diff does not match the ledger"
|
|
|
|
|
+ (" / CI-verified" if ci_verified_hash is not None else "")
|
|
|
|
|
+ " hash"
|
|
|
|
|
)
|
|
|
|
|
return GateResult(
|
|
|
|
|
decision=GateDecision.BLOCK,
|
|
|
|
|
reasons=reasons,
|
|
|
|
|
run_id=expected_run_id,
|
|
|
|
|
diff_hash=ledger_hash,
|
|
|
|
|
ci_conclusion=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# (3) Authenticated, patch-independent CI conclusion.
|
|
|
|
|
if ci_result is None:
|
|
|
|
|
reasons.append("no authenticated CI result supplied")
|
|
|
|
|
return GateResult(
|
|
|
|
|
decision=GateDecision.BLOCK,
|
|
|
|
|
reasons=reasons,
|
|
|
|
|
run_id=expected_run_id,
|
|
|
|
|
diff_hash=ledger_hash,
|
|
|
|
|
ci_conclusion=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
actual_run_id = ci_result.get("run_id")
|
|
|
|
|
if str(actual_run_id) != expected_run_id:
|
|
|
|
|
reasons.append(
|
|
|
|
|
f"CI run-id mismatch: expected {expected_run_id!r}, "
|
|
|
|
|
f"conclusion is keyed to {actual_run_id!r}"
|
|
|
|
|
)
|
|
|
|
|
return GateResult(
|
|
|
|
|
decision=GateDecision.BLOCK,
|
|
|
|
|
reasons=reasons,
|
|
|
|
|
run_id=expected_run_id,
|
|
|
|
|
diff_hash=ledger_hash,
|
|
|
|
|
ci_conclusion=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
conclusion = ci_result.get("conclusion")
|
|
|
|
|
normalized = str(conclusion).strip().lower() if conclusion is not None else None
|
|
|
|
|
|
|
|
|
|
if normalized == _SUCCESS_CONCLUSION:
|
|
|
|
|
reasons.append("authenticated CI conclusion: success")
|
|
|
|
|
return GateResult(
|
|
|
|
|
decision=GateDecision.PASS,
|
|
|
|
|
reasons=reasons,
|
|
|
|
|
run_id=expected_run_id,
|
|
|
|
|
diff_hash=ledger_hash,
|
|
|
|
|
ci_conclusion=normalized,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if normalized in _FAILURE_CONCLUSIONS:
|
|
|
|
|
reasons.append(f"authenticated CI conclusion: {normalized}")
|
|
|
|
|
return GateResult(
|
|
|
|
|
decision=GateDecision.FAIL,
|
|
|
|
|
reasons=reasons,
|
|
|
|
|
run_id=expected_run_id,
|
|
|
|
|
diff_hash=ledger_hash,
|
|
|
|
|
ci_conclusion=normalized,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Unknown / null / still-running conclusion: refuse to proceed (never a
|
|
|
|
|
# silent pass). e.g. ``None`` (in-progress), ``"neutral"``, ``"skipped"``.
|
|
|
|
|
reasons.append(
|
|
|
|
|
f"unrecognised/ambiguous CI conclusion {conclusion!r}; refusing to proceed"
|
|
|
|
|
)
|
|
|
|
|
return GateResult(
|
|
|
|
|
decision=GateDecision.BLOCK,
|
|
|
|
|
reasons=reasons,
|
|
|
|
|
run_id=expected_run_id,
|
|
|
|
|
diff_hash=ledger_hash,
|
|
|
|
|
ci_conclusion=normalized,
|
|
|
|
|
)
|