Identity separation: per-role and per-mix-bucket session env (#304)#308
Identity separation: per-role and per-mix-bucket session env (#304)#308polymath-orchestrator wants to merge 18 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Implements per-session GitHub identity separation by introducing env layering at the project-config level (project env < per-role env < per-workerMix-bucket env keyed by resolved harness), plus adds ops tooling to provision/check per-bot gh/git identity directories so workers can author PRs under a login distinct from the reviewing orchestrator.
Changes:
- Backend: add
envtoRoleOverrideandWorkerMixEntry, implementProjectConfig.ResolveEnv(kind,harness)and wiresession_managerruntime env resolution to the resolved harness (including switch/relaunch paths), while strippingPOLYPOWERS_AUTOMERGEpost-merge. - Surfaces: update CLI JSON mirror structs, OpenAPI + generated frontend schema, and frontend project settings form to round-trip bucket
envwithout wiping it. - Ops: expand
assertNoSecretEnvto cover role/bucket env maps and addops/identity.mjs(+ core + tests) for identity directory creation and validation.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| ops/project-config-core.mjs | Extends secret-shaped env key guard to role/bucket env maps. |
| ops/project-config-core.test.mjs | Adds regression tests ensuring the secret guard reaches role/bucket env and allows identity path keys. |
| ops/identity.mjs | New CLI to create/check/list per-bot identity directories and validate separation from orchestrator login. |
| ops/identity.test.mjs | End-to-end tests for ops/identity.mjs using a fake gh and temp HOME. |
| ops/identity-core.mjs | Pure helpers for identity path layout, gitconfig rendering, and validation rules. |
| ops/identity-core.test.mjs | Unit tests for identity-core helpers and validation failure modes. |
| backend/internal/domain/projectconfig.go | Adds per-role env + env resolution (ResolveEnv) and validates role/bucket env against secret-shaped keys. |
| backend/internal/domain/workermix.go | Adds env to worker-mix entries and helper to pick env by harness (first match). |
| backend/internal/domain/projectconfig_env_test.go | Domain-level tests for env layering precedence and worker-only bucket application. |
| backend/internal/session_manager/manager.go | Threads harness into runtime env resolution; uses resolved harness on switch paths; strips automerge key post-merge. |
| backend/internal/session_manager/provision_test.go | Updates tests for new projectRuntimeEnv(kind,harness,cfg) signature. |
| backend/internal/session_manager/identity_env_test.go | Session-manager seam tests ensuring identity env follows resolved harness (including switch trap). |
| backend/internal/service/project/service_test.go | Verifies config writes reject secret-shaped keys in role/bucket env. |
| backend/internal/cli/project.go | CLI JSON mirror updated to round-trip role/bucket env fields. |
| backend/internal/httpd/apispec/openapi.yaml | OpenAPI schema updated to include env on role override + worker mix entries. |
| frontend/src/api/schema.ts | Regenerated client schema reflecting OpenAPI env additions. |
| frontend/src/renderer/components/ProjectSettingsForm.tsx | Preserves workerMix[i].env through load/edit/save so settings edits don’t wipe identity env. |
| frontend/src/renderer/components/ProjectSettingsForm.test.tsx | Regression test ensuring role/bucket env survives a save even though the form doesn’t expose it. |
| docs/design/304-identity-separation.md | Design doc capturing rationale, env layering rules, sequencing, and residual risk. |
Comments suppressed due to low confidence (1)
ops/project-config-core.mjs:98
- The error message still says “Move secrets out of project env…”, but
assertNoSecretEnvnow scans multiple env maps (env,worker.env,orchestrator.env,prime.env,workerMix[i].env). When the offender is under a role/bucket env path, the current message is misleading for operators trying to fix the config.
throw new Error(
`refusing to write secret-like env key(s) to a committed spec: ${offenders.join(", ")}. ` +
`Move secrets out of project env, or exempt specific non-secret keys via ` +
`AO_PROJECT_CONFIG_ALLOW_ENV_KEYS=<comma-separated key names>.`,
);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
final-review — verdict: clean · PARKED for human mergeHead reviewed: Reviewer's closing verdict: "READY. No CRITICAL or HIGH issue remains that makes merging unsafe. ResolveEnv layering, token shadowing, AO-owned precedence, spawn/switch/restore selection, and reviewer-pane isolation are sound." ⛔ PARKED — sensitive path, human merge requiredThe diff touches The review earned its keep — 3 bugs that would have shipped a silently broken featureEach looks correctly configured and fails silently, which is the exact failure mode #304 exists to eliminate.
Also caught and fixed: a regression I introduced ( Where I pushed back
|
Independent review (codex) confirmed the stale-SHA logic is correct, and found
five ways the gate could wave a merge through. Every one is the same shape as the
bug it exists to fix: an ambiguity treated as "fine".
HIGH — PR resolution could pick the WRONG pull request. `commits/{sha}/pulls`
returns every PR associated with a commit, merged and closed ones included, so
taking the first exact-head match could select a stale PR and mask a
CHANGES_REQUESTED on the one actually entering the merge queue — a direct bypass.
Now: only OPEN PRs whose head is this SHA, and exactly one, or fail closed.
- A non-array reviews response (null, an error body, a truncated read) meant "no
reviews" and certified the merge. It means "we do not know". Fails closed; the
old test had locked the unsafe behavior in.
- An unknown PR author passed when the review list was empty. Without the author
we cannot tell a self-review from an independent one — author == reviewer is the
exact collapse this prevents — so it fails closed with zero reviews too.
- A current-head verdict whose author could not be read silently VANISHED rather
than blocking.
- Ordering used submitted_at, so a missing or malformed timestamp parsed to 0 and
a LATER requested change could lose to an EARLIER approval, silently unblocking
the merge. Now uses the API's documented response order with the review id as a
deterministic tie-breaker.
- `--paginate` without `--slurp` emits one JSON document per page, so the parse
threw past 100 reviews: fail-closed, but UNCLEARABLE — and an unclearable gate
gets routed around, which is the failure mode we are trying to end.
Re-verified live: #308 still parks on human-required (not on reviews), human mode
is unchanged, and a SHA with no open PR fails closed.
Refs #304
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
The independent reviewer caught that my previous commit's message claimed three fixes the committed code did not contain. It was right: a patch to fetchHeadPullRequestReviews silently failed to apply (prettier had reflowed the target), so only the call site changed — the file destructured `ambiguous` from a function that never returned it, still took the first matching PR of any state, and still paginated without --slurp. My own live check passed anyway, because #308 happens to have exactly one open PR. That is precisely the hole an independent reviewer reading the committed diff closes, and precisely why "it worked when I ran it" is not verification. Now applied, and the caller's logic is extracted into pure, tested seams — selectHeadPullRequest() and flattenReviewPages() — because the bug lived in the one part of this gate that had no tests while the evaluator's own tests were green: - Only OPEN pulls whose head is exactly this SHA count, and exactly one must remain; ambiguity fails closed instead of guessing which PR is real. - --slurp + flatten, so a PR with more than 100 reviews stays evaluable. - A non-array reviews body reaches the evaluator UNCHANGED (not `?? []`) so it fails closed on an unread response rather than certifying the merge. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
Records the resolved design decisions for #304 and the three findings that reshape the issue: the nhod-claude/nhod-codex bots already exist with write access, mainprotect carries no pull_request rule (so a blocking review would not block), and the org is at 5/5 seats. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
…#304) Adds Env to RoleOverride and WorkerMixEntry, plus ProjectConfig.ResolveEnv, which layers project < role < mix-bucket env. The bucket is selected by the session's RESOLVED harness, so an explicit --agent still lands on that harness's identity; mix buckets are worker-only, so an orchestrator keeps its own credentials even when its harness matches a worker bucket. Validate refuses secret-shaped keys in role and bucket env: those carry identity paths only. The storage CDC triggers redact $.env out of the change log but cannot strip workerMix[i].env generically (SQLite json_remove has no array wildcard), so the no-secrets invariant is enforced at the source. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
…ness (#304) projectRuntimeEnv now takes the session's resolved harness and layers project < role < mix-bucket env through ProjectConfig.ResolveEnv, so a worker carries a GitHub identity distinct from the orchestrator that reviews its PR. The harness is a parameter, not a lookup: switchLiveHarness and relaunchTerminatedWithHarness carry the NEW harness while the session record still holds the old one, so resolving from the record would compile, pass every spawn test, and silently leave a switched session pushing under the previous bot's login. Covered by a test that fails against exactly that mistake. POLYPOWERS_AUTOMERGE is stripped from the merged map rather than only from project env: without that, worker.env could self-grant autonomous merge and walk straight around the merge gate. Reviewer panes deliberately keep the daemon's credentials — review/launcher.go never calls projectRuntimeEnv — which is what keeps author and reviewer distinct. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
) The load-bearing surface is the READ path: `ao project get --json` re-marshals through the CLI's own mirror structs, and a field missing there is silently dropped — which is exactly what ops/project-config check and capture consume. Left unfixed, worker.env would show as permanent phantom drift and capture would erase the identity config apply had just installed. The frontend had the same hazard in the other writer: ProjectSettingsForm rebuilt mix rows as {agent, model, weight}, so any settings save wiped a bucket's env. Both are regression-tested against the lossy versions. assertNoSecretEnv now walks role and bucket env too: unlike top-level env those are not redacted out of the change log by the CDC triggers (SQLite json_remove has no array wildcard for workerMix[i]), so a secret there would be both committed and journaled. Drift output deliberately does NOT blanket-redact role/bucket env: those are guaranteed non-secret at the source, and redacting them would make identity drift undiagnosable. Secret-named leaves are still redacted by the existing heuristic. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
…he write path (#304) SetConfig -> validateProjectConfig -> Validate is the complete guard for the new identity env maps, so pin it with a test. Deliberately NOT extending the CDC redaction triggers, which is where the plan pointed: SQLite json_remove has no array wildcard, so a trigger can redact the three role paths but never workerMix[i].env — partial coverage that reads as complete. The source guard covers every path, so redacting on the way to the change log is moot, and we avoid burning a migration version (a recurring collision point between parallel PRs) on a strictly weaker guarantee. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
#304) Two decisions worth writing down: The config cutover CANNOT precede the credentials. GH_CONFIG_DIR pointing at a directory that does not exist yet makes gh unauthenticated, so landing identity env in the committed spec before the bots are authenticated would break gh for every worker on the next `project-config apply`. The cutover is therefore not part of the mechanism change. The identity check lives in ops, not `ao doctor`. "An identity that collapses to the reviewing login is a failure" is Polymath review policy, not generic daemon behavior, and this tree is under the vanilla rule — the backend diff stays minimal and upstream-shaped (per-role env + resolution), while the policy check goes in ops/identity.mjs where it belongs. Also scores the acceptance criteria honestly: AC2 is not met by decision (branch protection stays as-is), so the PR does not claim a gate it does not have. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
…304) ops/identity.mjs create|check|list, over a pure ops/identity-core.mjs. `check --all` carries the guard that would have CAUGHT #304: it resolves each configured identity's login via GH_CONFIG_DIR and fails when that login is not distinct from the orchestrator's. It also fails when the resolved login is a third account — the likeliest misprovisioning is running `gh auth login` in the right directory as the wrong user, and nothing in "non-empty and != orchestrator" catches that, yet the directory name IS the bot login by construction. Deliberately in ops rather than `ao doctor`: "an identity that collapses to the reviewing login is a failure" is Polymath review policy, not generic daemon behavior, and this tree is under the vanilla rule. create never writes, reads, or prints a token — it makes dirs (0700) and a gitconfig; the credential arrives only from the operator's own gh auth login. The url.insteadOf rewrite in that gitconfig is load-bearing: delete it and the pane falls back to the shared SSH key, i.e. back to pushing as the orchestrator. Documents the noreply-email caveat: the id-prefixed form needs an authenticated API call, which create cannot make before the bot has credentials. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
Codex review cycle 1: 2 high, 8 medium, 1 low. Two were real bugs. SECURITY — AO_RUN_FILE could be forged by config env. Every other AO-owned key is unconditionally assigned in spawnEnv, but AO_RUN_FILE was only set when the daemon HAS a run file; otherwise a project-supplied value simply survived, so config env could forge the path `ao hooks` callbacks resolve against. Now deleted when absent. Found by the table-test the reviewer asked for (1j) — the original test only proved AO_SESSION_ID, which is exactly why this hid. BUG — the settings form carried a mix bucket's identity env across an agent change. env is HARNESS-KEYED: repointing a row from claude-code to codex kept the claude bot's GH_CONFIG_DIR, silently handing one bot's identity to another harness's workers. Dropped on agent change; mutation-verified. Also: - ops: fail CLOSED when the orchestrator's own login cannot be resolved (1b). Distinctness from the reviewer is the ONE thing the check exists to prove; passing when it is unverifiable is how #304 went unnoticed to begin with. - domain: align the Go secret heuristic with looksSecret in ops/project-config-core.mjs (1f). The narrower Go list let GH_PAT and NPM_AUTH through while the JS guard rejected them — a key only one side catches is a hole. - domain: reject same-harness mix buckets that disagree on env (1g), which is what makes first-match-by-harness unambiguous instead of order-dependent. - ops: parse the gitconfig into sections instead of regexing the whole file (1h) — a whole-file regex was satisfied by a directive in an unrelated section. - ops: `check` takes explicit bot names (1d); `--all` only sweeps what is already provisioned, so it cannot notice a configured-but-unprovisioned bot. - review: pin reviewer-pane isolation with a named test (1i). It holds by construction today; the test exists so a refactor that "unifies" the env paths cannot silently re-collapse author and reviewer. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
From the PR-integrated reviewer. `check --all <bot>` silently ignored the bot, and unknown flags were swallowed — and accepting multiple bot names (needed so a cutover can name its identities explicitly) made that sharper. A silently-ignored argument is the worst failure mode for a tool that gates which GitHub account workers push as: it would report "separated" for a set the operator never actually asked about. Reject rather than guess. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
…304) Codex review cycle 2 found two HIGHs. Both are the same shape as the original bug — the identity looks correctly configured and the worker authenticates as the orchestrator anyway — and both are verified against the real tools, not just asserted. 1. gh reads GH_TOKEN > GITHUB_TOKEN > GH_CONFIG_DIR/hosts.yml, and a pane inherits the daemon's whole environment. An inherited token therefore OVERRIDES the bot's stored credentials. This is reachable in supported config: ProjectConfig.Env may legitimately carry a GITHUB_TOKEN (that is why the CDC triggers redact $.env at all). Verified live: with GH_TOKEN set, gh ignores hosts.yml entirely; with it shadowed to empty, gh falls back to the identity. projectRuntimeEnv now shadows both keys (empty, not deleted — the pane must override an INHERITED value) whenever the session selects an identity by path. Done in the daemon, not config: an operator who forgot the shadow keys would silently re-collapse author and reviewer. 2. git collects credential helpers into a LIST and asks them in order. The identity gitconfig [include]s the real ~/.gitconfig ABOVE its own settings, so a helper configured there (store/cache/keychain holding the orchestrator's credentials) is asked FIRST. Proven with `git credential fill`: without a reset the ORCHESTRATOR's helper answers; with an empty `helper =` reset, the bot's does. The reset is now emitted and REQUIRED by validation — checking only that our helper is present would pass the broken config. Also from the cycle: - Allowlist identity/transport keys the widened secret heuristic wrongly rejected (SSH_AUTH_SOCK, GIT_AUTHOR_NAME, SSL_CERT_FILE all contain "auth"/ "cert" and are exactly what this field exists to hold). Go and ops allowlists are kept in sync and cross-tested. - Same-harness bucket consistency compares EFFECTIVE env (role under bucket), so a bucket that inherits the role identity and one that spells it out are no longer a false positive. - gitconfig parsing uses git's --get-all list semantics and treats an unparseable header as ending the section. - `check --all` now covers every CONFIGURED identity (union of committed specs and disk), so a spec'd-but-unprovisioned bot FAILS instead of being skipped. - orchestratorLogin() documents why it strips ambient identity env, and takes an AO_ORCHESTRATOR_LOGIN override. - Cover the mix-SELECTED spawn path; every prior spawn test passed an explicit harness and so never exercised the mix. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
Codex cycle 3: two HIGHs, both on validation that looked right and wasn't. 1. The helper list must be EXACTLY ["", "!gh auth git-credential"]. "Reset is present AND ours is present" passes ["", "!orchestrator", "!gh ..."], where the orchestrator's helper is still asked first — git takes the first helper that answers. 2. Validating the outer file is not validating the config. An [include] can inject a helper AFTER our reset, so a file that reads perfectly still resolves to the orchestrator's credentials. ops now reads git's own resolution (`git config --file <f> --includes --get-all`) and applies git's reset semantics in a pure, unit-tested helper — `--get-all` reports raw values and does NOT collapse resets, so an empty value clears the list accumulated so far. Proven against real git: the include-after ordering is now rejected and the safe ordering accepted. Also from the cycle: - The check now REPRODUCES the runtime: its gh probe shadows GH_TOKEN/ GITHUB_TOKEN exactly as the daemon does. It previously left an ambient token standing, so gh would answer with the AMBIENT account and the check could certify a bot it never actually consulted. A check that does not fail the way production fails is not a check. - Neutralization keys on a NON-EMPTY GH_CONFIG_DIR: presence-only would treat GH_CONFIG_DIR="" as an identity and blank a project's tokens, destroying auth instead of redirecting it. - Validate rejects a half-configured identity (GH_CONFIG_DIR without GIT_CONFIG_GLOBAL or vice versa) — gh acts as the bot while git authors as the orchestrator. Looks configured; is not. - Validate rejects a copilot identity with no COPILOT_GITHUB_TOKEN: the identity shadows GH_TOKEN/GITHUB_TOKEN, which are exactly the adapter's own auth fallbacks, so such a worker would lose authentication at runtime with no hint why. Fail at config time instead. - `check --all` checks the CONFIGURED paths, not a canonical path rebuilt from the bot name — a spec may point elsewhere, and rebuilding would green-light a directory the daemon never uses. - GIT_SSH_COMMAND is not allowlisted: its value is an arbitrary command that can carry an inline credential. (The name heuristic cannot catch a secret in an arbitrary VALUE; that limit is now stated rather than papered over.) Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
…blind spot (#304) Codex cycle 4. The first finding is a REGRESSION I introduced last cycle. REGRESSION — `check --all` crashed with "invalid bot name [object Object]" whenever a spec actually configured an identity: configuredIdentities() returns objects and knownIdentities() returns strings, and cmdCheck treated both as names. It broke precisely at cutover, the one moment the gate exists for, and my earlier "it works" run only passed because no spec carried identity env yet. Reproduced by temporarily adding identity env to the spec, then fixed and re-verified the same way. Every target is now normalized to {bot, paths}. HIGH — git applies the GENERIC `credential.helper` as well as the URL-scoped one, and an [include] can set it. Querying only credential.https://github.com.helper missed it entirely, so an injected generic helper produced a false green. ops now reads the fully-resolved config with `--list` (which preserves the file ORDER that decides which helper answers first) and keeps every helper applying to github.com. Verified against real git: a generic helper injected via an include is now caught. Also: - Validation resolves the EFFECTIVE env per surface (the same ResolveEnv the runtime uses) instead of inspecting raw layers, which missed an identity supplied via ProjectConfig.Env and a copilot bucket inheriting the worker identity while carrying unrelated env of its own. - The half-pair rule is relaxed correctly: GIT_CONFIG_GLOBAL alone is legitimate (a role may want its own author/signing config without changing gh). Only a GH_CONFIG_DIR without its GIT_CONFIG_GLOBAL is the split identity. - Identity paths must be ABSOLUTE: a relative one resolves against each process's working directory, so the identity validated is not the identity used. - GIT_SSH_COMMAND is now DENIED outright. Removing it from the allowlist was inert — the name heuristic never matched it — so the previous "fix" changed nothing and the JS test still asserted acceptance. - `check --all` keys targets by the configured ghDir+gitconfig PAIR and never fabricates a missing GIT_CONFIG_GLOBAL; it reports the half-configured identity the daemon would also reject. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
) Codex cycle 5 confirms the CORE is sound — ResolveEnv layering, token shadowing, AO-owned precedence, spawn/switch/restore selection and reviewer-pane isolation. What remained was one real gap in the ops checker plus small edges. HIGH — git honors PATH-scoped credential sections too (credential.https://github.com/org/repo.helper), not just generic and host-scoped. An include could inject a repo-scoped orchestrator helper and `check` reported green while git consulted it first. Verified against real git: the path-scoped helper appears in git's resolved config and is now caught and rejected. Also: - Same-harness bucket consistency layers PROJECT env too, matching ResolveEnv. Omitting it made "one bucket inherits X from project env, another spells the same X out" a false positive though both resolve identically. - The absolute-path rule applies to a standalone GIT_CONFIG_GLOBAL as well. - allowKeys can no longer re-admit GIT_SSH_COMMAND: that escape hatch exists for a non-secret NAME that trips the heuristic, not for a key refused on the strength of its VALUE. Deliberately NOT done: validating every harness that exists. The reviewer wanted the copilot rule extended to explicit `--agent copilot` on projects with no copilot bucket, but that would force COPILOT_GITHUB_TOKEN on every identity-using project on the account. That override fails LOUDLY at runtime (copilot cannot authenticate once its fallbacks are shadowed) — an error, not the silent identity collapse this validation exists to prevent. Scoped to the harnesses a project is actually configured to spawn, and said so in the code. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
…ment (#304) Final review nits. The denial now matches the Go validator exactly: role and bucket env only. Top-level project env is a different contract — it may legitimately carry secrets (the storage CDC triggers redact $.env out of the change log precisely because it can), so denying it there would have rejected configs the daemon accepts. Refs #304 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
cfe1b39 to
e0901fa
Compare
review requested (SDLC marker)\n\nReviewer: Claude (independent local reviewer)\nHead:
|
review verdict (SDLC marker)\n\nCycle: 1\nHead reviewed:
|
final-review cycle 2 verdict2a. Reviewer/model family: Claude independent foreground review. 2b. Reviewed head: 785e116. 2c. Verdict: fixes required; 2 findings, both fixed. 2d. Fix head: 9b0744b. 2e. Fixes: removed the retired nopool helper/test restored during rebase, and updated docs/design/304-identity-separation.md so the phase and acceptance-status notes match the shipped PR #311/autonomous-merge path. 2f. Local verification after fixes: npm run ci:backend, npm run test:ops, npm run format:check, npm test --prefix frontend, npm run typecheck --prefix frontend, npm run build:web --prefix frontend. |
final-review cycle 3 verdict3a. Reviewer/model family: Claude independent foreground recheck. 3b. Reviewed head: 9b0744b. 3c. Verdict: clean for the cycle-2 fixes. 3d. Findings: none. The retired nopool helper/symbols are absent from backend/internal/domain/workermix.go, remaining nopool occurrences are tests/docs asserting the legacy label is ignored, and docs/design/304-identity-separation.md now marks phase 4 shipped via #311 and AC2 met on the autonomous-merge path. 3e. Residual risk noted by reviewer: envForHarness depends on validateMixEnvConsistentPerHarness preserving same-harness env consistency; existing implementation/tests cover that invariant. |
final-review — verdict: clean · PARKED for human merge1a. Head reviewed: 9b0744b. 1b. Independent reviewer: Claude foreground recheck after rebase and review fixes. 1c. Review result: clean. Cycle 1 findings fixed at 785e116; cycle 2 findings fixed at 9b0744b; cycle 3 recheck found no remaining findings. 1d. Review threads: 0 unresolved current-head threads. The only Copilot thread is outdated and resolved. 1e. Local CI: npm run ci:backend; npm run test:ops; npm run format:check; npm test --prefix frontend; npm run typecheck --prefix frontend; npm run build:web --prefix frontend. 1f. Remote CI: all GitHub checks are green on this SHA, including Go, Frontend, Prettier, gitleaks, and CLI E2E. 1g. Statuses set on this SHA: final-review=success, review-passed=success, merge-park=success with reason=human-required. 1h. Park reason: the diff touches backend/internal/session_manager/**, which is a sensitive path under this repo's policy, so autonomous merge must park for a human even with a clean gate. |
|
Closing without merge, per the #313 harmony plan (operator-approved implementation direction, 2026-07-13/14): separate GitHub bot identities, credential routing, and per-role/mix-bucket environment layering add a large subsystem that does not solve a current need of this single-operator local product. This is a rejection on product-scope grounds, not a deferral — see the 'Close without implementing' section of the plan comment on #313. The identity mechanism here is not security isolation; cross-family independent review plus the current-head final-review status remains the review control. |
Refs #304 — deliberately does NOT close it. The mechanism ships here; acceptance criterion 2 is unmet by operator decision and the config cutover is blocked on credentials (both scored honestly below). An independent reviewer flagged auto-closing this as its top finding, and it was right: merging must not mark #304 done.
Workers push and call
ghunder the orchestrator's own login, so author and reviewer collapse to one identity and GitHub refuses a blocking review (Can not request changes on your own pull request). This gives a session a GitHub identity resolved from its role and its resolved harness, so a worker can author a PR under a login the reviewing orchestrator can actually block.Design:
docs/design/304-identity-separation.md.Three findings that reshaped the issue
nhod-claudeandnhod-codexare already org members withwriteon this repo and on agent-vault, and already author PRs elsewhere (nhod-codex→ Consolidate runtime architecture and delete dead generations #271 here). The provisioning half was largely done — it had just never reached the orchestrator box, where oneghconfig and one SSH key collapse every pane topolymath-orchestrator.mainprotecthas nopull_requestrule — onlydeletion,non_fast_forward,required_status_checks,merge_queue. GitHub enforcesCHANGES_REQUESTEDas a merge blocker only when a review rule exists. So identity separation alone satisfies AC1 but not AC2 — see the honest scoring below.What this ships
EnvonRoleOverrideand on eachWorkerMixbucket, resolved byProjectConfig.ResolveEnv:Identity is a path, never a secret. The committed spec carries only
GH_CONFIG_DIRandGIT_CONFIG_GLOBALpaths into~/.ao/identities/<bot>/; tokens live on disk, outside git. Aurl.insteadOfrewrite (SSH → HTTPS) routes push auth throughgh auth git-credential, which resolves against each pane's ownGH_CONFIG_DIR— so one global credential helper yields a different identity per pane, and zero SSH keys need provisioning.Two things worth reviewing closely
The
POLYPOWERS_AUTOMERGEescalation.projectRuntimeEnvstrips that key from project env precisely so a project cannot self-grant autonomous merge. A naive role-env merge would have letworker.env: {POLYPOWERS_AUTOMERGE: "1"}walk straight around the merge gate. The strip now applies to the merged map. Tested.The switch trap.
switchLiveHarnessandrelaunchTerminatedWithHarnessreceive the new harness as a parameter while the session record still holds the old one. Resolving identity from the record compiles, passes every spawn test, and silently leaves a switched session pushing under the previous bot's login. The harness is therefore a parameter, not a lookup — andTestSwitchHarness_IdentityEnvFollowsTheNewHarnesswas verified to fail against exactly that mistake.Reviewer panes are deliberately not given a worker identity (
review/launcher.gonever callsprojectRuntimeEnv), which is what keeps author and reviewer distinct. A future refactor that "unifies" the two env paths would re-collapse them — commented in code.Two deliberate departures from the plan
json_removehas no array wildcard, so a trigger could redact the three role paths but neverworkerMix[i].env— partial coverage that reads as complete. InsteadValidate()refuses credential-shaped keys in any role/bucket env andSetConfigcalls it on every write, which is total. Also avoids burning a migration version (a recurring collision point between parallel PRs) on a weaker guard.ao doctorcheck. "An identity that collapses to the reviewing login is a failure" is Polymath review policy, not generic daemon behavior, and this tree is under the vanilla rule. The backend diff stays minimal and upstream-shaped; the policy check lives inops/identity.mjs check --all.The silent-corruption class this closes
The load-bearing surface was the read path, not
set-config:ao project get --jsonre-marshals through the CLI's own mirror structs, and a field missing there is silently dropped — and that read path is whatops/project-config check/captureconsume. Unfixed,worker.envwould have been permanent phantom drift andcapturewould have erased the identity configapplyhad just installed. The frontend had the mirror-image bug:ProjectSettingsFormrebuilt mix rows as{agent, model, weight}, wiping bucketenvon any settings save. Both are now regression-tested against the lossy versions (both tests verified to fail against them).Acceptance criteria, scored honestly
--request-changesactually blocks the mergeops/final-review-status.mjs check --mode autonomousto refuse a current-headCHANGES_REQUESTEDfrom a non-author, which closes it on the path these PRs actually take.BEADS_TO_GH_ASSIGNEE_MAP/ claim+mirror still resolve.beads/here, and skills resolve the login fromgh api user, which now returns the bot.GH_CONFIG_DIRpointing at a directory that does not exist makesghunauthenticated. Landing identity env in the committed spec before the bots are authenticated on the box would breakghfor every worker on the nextproject-config apply. Order is fixed:identity.mjs create→ operator runsgh auth loginper bot (the one step an agent cannot do) →identity.mjs check --allpasses → then cut the spec over.Testing
npm run ci:backendgreen (build, vet,-race, golangci-lint).npm run format:checkgreen.Notes for the reviewer
backend/internal/session_manager/**(sensitive path) → autonomous merge parks for a human.$HOME, so a worker that deliberately unsetGH_CONFIG_DIRcould still act as the orchestrator. Strictly weaker threat model than today's structural single-identity, but not isolation.🤖 Generated with Claude Code
https://claude.ai/code/session_01RP5jQC1fNo5ak4oyogD3ay
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.