fix(agent-team): dispatch via GitHub App so P3 reaches CI (run_id resolves) #63

Merged
amoussa1229 merged 8 commits from fix/agent-team-dispatch-run-id into main 2026-06-24 22:22:19 +00:00

8 commits

Author SHA1 Message Date
ed6520ccfe fix(deps): bump cryptography 46.0.7 -> 48.0.1 (GHSA-537c-gmf6-5ccf)
pip-audit flagged the 46.0.7 pin: wheels before 48.0.1 statically link a
vulnerable OpenSSL. 48.0.1 is the fix version.
2026-06-24 18:16:47 -04:00
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
7c2303a76c fix(agent-team): derive declared_scope from the diff when no plan scope is set
End-to-end validation surfaced that auto-dispatch parked every task at
"empty declared_scope": dispatch_node required plan["scope"], but the planner
emits only summary+phases (never a scope) and config.allowed_scope defaults to
None, so no node ever populated it. (The earlier dispatch test was
operator-initiated with an explicit scope; the auto planner->build->dispatch
path was never exercised until box-side App dispatch went live.)

Fix: when no planner-/operator-declared scope is present, dispatch_node derives
declared_scope from the candidate diff's own touched paths (ci_gate.diff_touched_paths).
This supplies the missing scope without relaxing any CI trust control — the
apply/verify workflow still INDEPENDENTLY re-checks the materialized diff against
the denylist + '..'-escape + this scope + the diff-hash binding, and the
agent-apply environment's required reviewer remains the human gate. A non-empty
diff that parses to zero touched paths still parks (fail closed).

Tests: the three old park-on-missing-scope cases now assert scope-from-diff
dispatch; added a park case for a diff with no parseable paths. 1527 pass.
2026-06-24 17:56:19 -04:00
4eb245e83b docs(agent-team): key lives at ~/.ssh/agent-team-apply.pem on the box
The App private key is placed under ~/.ssh (already mode 700) rather than
~/.sea-haven (which holds the synced engineering-handbook). Update the env path,
the secure-copy block, and the rotation/incident-response rm to ~/.ssh.
2026-06-24 17:37:07 -04:00
52e8e4bf63 docs(agent-team): concretize App dispatch runbook with verified app/install IDs
App agent-team-apply: app_id 4119505, installation 141992144 (not secrets — only
the .pem is). A 2026-06-24 test mint confirmed contents:write + actions:write +
repository_selection:selected. The box gets its own freshly-generated private key
(the CI-side AGENT_APPLY_APP_PRIVATE_KEY Actions secret is write-only and cannot
be re-exported); secure-copy it to the box and delete the Mac copy after.
2026-06-24 17:34:08 -04:00
78104e8934 refactor(agent-team): apply /code-review findings on the App dispatch seams
- app_run_locator: default a missing status_code to None (fail closed -> raise)
  rather than 200, so a malformed response object can never be treated as a
  successful run list.
- app_run_locator: drop the unused `_now` parameter (dead/misleading — the floor
  is derived solely from since_iso) and simplify the redundant two-step `_sleep`
  indirection to a single resolution.
- github_app._parse_expires_at: normalise a naive parsed datetime to aware UTC so
  an offset-less expires_at cannot raise a bare TypeError in TokenProvider.token
  (bypassing the fail-closed GitHubAppError contract).
- coordinator._app_dispatch_seams: use the module-level Path import instead of a
  redundant inline one.

Follow-ups (left to respect the plan's "leave the gh _default_* seams untouched,
additive only"): the floor/skew/poll scaffold is duplicated between
app_run_locator and _default_run_locator, and the GitHub REST header dict is
rebuilt in several places — both worth a later shared helper.

Full suite 1526 passing; ruff clean.
2026-06-24 17:19:58 -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