Commit graph

6 commits

Author SHA1 Message Date
265e32d9c9 fix(agent-team): authenticate git push with Basic auth, not Bearer
The box-side dispatch smoke test failed at `git clone` (exit 128, "could not
read Username"): GitHub's git smart-HTTP transport authenticates an installation
token via BASIC auth (username x-access-token), not Bearer. Bearer is the REST
API form (minting + workflow_dispatch + run-list all use it correctly) but git
rejects it.

app_branch_pusher now sets the http.extraHeader to
`Authorization: Basic <base64("x-access-token:" + token)>`. Verified on the box:
Bearer -> exit 128, Basic -> clone OK. _scrub now also redacts the base64
credential blob (it decodes to the token). Test updated to assert the Basic form
and that the raw token never appears literally in the header.
2026-06-24 18:03:59 -04:00
183789a239 harden(agent-team): scrub token from HTTP transport errors; fail closed on bad expiry
Two defense-in-depth fixes surfaced by /sh-security-review (both were
unverified — no exploit — but cheaply strengthen the credential contract):

- dispatcher: wrap the requests.post/get in app_workflow_dispatcher and
  app_run_locator in try/except that re-raises DispatcherError with the
  exception TYPE only (`from None`). The no-token-in-a-propagating-exception
  guarantee is now enforced by code, not by requests' incidental behavior.
- github_app: parse expires_at BEFORE caching the token and raise GitHubAppError
  (scrubbed) on a malformed value, so a parse failure fails closed without
  leaving a half-written cache (token set, expiry None) behind a bare ValueError.

Tests: +3 (transport-error scrub for both HTTP seams; malformed-expiry fail-closed
with no half-written cache). Full suite 1526 passing; ruff clean.
2026-06-24 17:14:24 -04:00
61ab1f0cd5 fix(agent-team): dispatch via GitHub App so P3 reaches CI (run_id resolves)
The P3 dispatcher's default seams shell out to gh/git, but the R720 box has
no gh and a read-only PAT with no Actions scope — so dispatch_apply_verify
returned no run_id and every task parked at verify ("dispatch unresolved").

Add a GitHub-App auth path: the box mints short-lived (~1h) installation
access tokens from the App private key and uses them for the three dispatch
seams, removing the gh dependency.

- agent_team/github_app.py (new): mint_installation_token (RS256 App JWT,
  iss=app_id, iat backdated 60s, exp 9 min; POST /access_tokens) + a lazy
  TokenProvider that caches and re-mints near expiry. Secret-safe: the JWT
  and token are never logged, never in an exception message, never persisted.
- dispatcher.py: app_branch_pusher / app_workflow_dispatcher / app_run_locator
  (additive; gh/git _default_* left untouched). Push auth rides a host-scoped
  http.extraHeader via GIT_CONFIG_* env (token never in argv/ps); the REST
  run locator maps id->databaseId / created_at->createdAt into select_run_id
  and surfaces 4xx promptly instead of silently exhausting the poll window.
- coordinator.py: default_dispatch_node_factory binds the App seams when
  AGENT_TEAM_GH_APP_ID / _INSTALLATION_ID / _PRIVATE_KEY are all set; partial
  or unreadable config logs one warning and falls back to gh-default (never
  raises at serve-start).
- requirements.txt: pin PyJWT, cryptography, requests (App seams + CI fetcher).
- DEPLOY-R720.md / README.md: App dispatch config, permission/scope audit,
  env-precedence check, key rotation/revocation + incident response.

Tests: +18 (test_github_app.py new; dispatcher/coordinator additions) covering
JWT claims, cache/re-mint, token-scrub-on-error, REST field mapping + run-name
correlation, and the partial-env inert fallback. Full suite 1523 passing.
2026-06-24 17:07:01 -04:00
1f8c7e1ee3 fix(agent-team): close P3 async-resume BLOCKs (durable CI-watcher wiring)
Remediates the Phase-0 adversarial BLOCKs:
- Durable ci_pending_provider (_enumerate_ci_pending) walks the LangGraph
  SQLite checkpointer to enumerate threads suspended at VERIFY awaiting CI;
  re-derives across restart. Excludes human-clarify gates + advanced threads.
- run-team serve wires ci_pending_provider + ci_poller + ci_timeout ONLY on a
  configured box; inert path unchanged. Closes the 'VERIFY suspended forever'
  defect: tick()->_ci_watch resumes on terminal CI or timeout-parks.
- CI resume routes through the single-flight, turn-guarded ResumeWorker.
- FIXes: run-locator skips cancelled/stale runs on rapid re-dispatch; inert-mode
  wording matches behavior; added node-level fail-closed + spurious-resume tests.
- end-to-end async-resume proof (test_p3_async_resume.py, real checkpointer).

Suite: 1270 passed, ruff clean. Branch only; not merged/deployed.
2026-06-23 19:52:04 -04:00
c3e935c904 feat(agent-team): capture dispatched run_id for P3 box-side verify
Wire the box-side build->dispatch->verify run identity so the verifier gate
can bind to the CI run the dispatcher triggered:

- task_model: add run_id / ci_correlation_tag / dispatched_at to TaskRecord +
  PipelineState (+ dict round-trip).
- dispatcher: RunLocator seam + DispatchResult; dispatch_apply_verify stamps a
  dispatched-at watermark, fires, then resolves the run via the workflow
  run-name (gh run list; the per-task_id concurrency group makes it
  unambiguous). Fails closed to run_id=None.
- dispatch_invoker: persist run_id/dispatched_at/ci_correlation_tag into state.
- workflow: additive run-name surfacing inputs.task_id as the correlation key
  (flagged for the C1 /sh-security-review + GPT-4.1 cross-review re-run).
- docs: P3-PHASE0-DESIGN.md records the async-resume design decision.

Part of Phase 0 (feat/agent-team-p3-box-integration). No behavior change on the
default path: P3 wiring is still opt-in/inert.
2026-06-23 19:52:04 -04:00
Adam Moussa
03b9a94881
feat(agent-team): P3-flip Phase 1 — CI trust-boundary hardening (WIP, gated) (#34)
* feat(agent-team): P3-flip Phase 1 — expand denylist vectors (§4.2) + runner-trust assertion (§4.1)

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

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

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

998 tests pass, ruff clean.

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

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

1015 tests pass, ruff clean.

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

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

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

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

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

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

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

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

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

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

Injection / secrets / least-privilege / flip-correctness / no-untrusted-checkout
all came back clean. 1044 tests, ruff clean, YAML valid.
2026-06-22 18:51:52 -04:00