feat(acp): enforce deterministic agent git commit identity - #6177
Open
wpfleger96 wants to merge 20 commits into
Open
feat(acp): enforce deterministic agent git commit identity#6177wpfleger96 wants to merge 20 commits into
wpfleger96 wants to merge 20 commits into
Conversation
Agent sessions could commit under the human operator's ambient git identity: only buzz-dev-mcp's shim applied the nostr author/signing GIT_CONFIG_* env, and only to its own shell-tool children. The native shells of claude-code, codex, and goose never saw it, so a bare `git commit` there resolved to whatever the repo/global config carried — erasing the AI-attribution signal (e.g. block/buzz #3140). Make the identity machine-managed across every harness: - New `buzz-git-identity` crate holds the pure author/email/signing/ keyfile logic as the single source of truth, consumed by both the shim and the harness so an agent commits under a byte-identical identity regardless of which surface applied it. - A `git` enforcement wrapper (installed on PATH ahead of the real binary) scrubs GIT_AUTHOR_*/GIT_COMMITTER_* from the child env, rejects `-c user.*`, `--config-env=user.*`, `--author`, and `--reset-author`, and on push refuses any outgoing commit not authored by the agent identity, then execs real git. - The harness lifts the identity + NIP-GS signing config onto the agent-runtime child and installs the wrapper plus the nostr signer/credential helpers via buzz-acp's own multicall, so native shells of all runtimes inherit both. Composed over the desktop's per-URL credential helper; skipped when no nostr key is present. - Prompt guidance (base_prompt.md, nest_agents.md) updated: identity is machine-managed; credit the operator via Co-authored-by/ Signed-off-by trailers, never user.name/email/-c/--author. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Round-1 review fixes on the deterministic agent-commit-identity work: - L3 push gate resolved the effective command through git aliases (config-defined and inline -c alias.x=push) so a push disguised as a custom alias can no longer skip outgoing-author verification. - Verification subprocesses now carry repository context (-C, --git-dir, --work-tree, --namespace); an outgoing tip that resolves to a real ref but whose range cannot be computed fails closed instead of being skipped as nothing-to-check. - AcpClient::shutdown deletes the git-identity keyfile tempdir explicitly via TempDir::close before the process-group kill. Relying on Drop right before std::process::exit leaked the 0600 nostr keyfile ~80% of runs; all client-owning error/timeout exits funnel through shutdown_and_exit, which takes the client by value. Checkpoint commit: the review round continues on this branch with the identity-authority and push-boundary rework. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The enforcement wrapper trusted the same caller-mutable GIT_CONFIG_* environment it was meant to constrain, so `env -u GIT_CONFIG_COUNT git commit` fell back to repo-local human identity and the push gate derived its expected identity from that same mutable config and failed open. - Authority: harness/shim write a 0600 identity manifest beside the keyfile; the wrapper locates its own install dir by PATH canonicalization, re-applies identity+signing GIT_CONFIG_* at the highest index before exec, and reads L3's expected author from the manifest. Manifest present = enforce; absent = passthrough. - L1b eligibility sources the key from BUZZ_PRIVATE_KEY then NOSTR_PRIVATE_KEY, decoupled from credential-helper discovery, and fails the managed session closed when identity cannot be installed. - Push gate uses git's own resolved plan (push --dry-run --porcelain --no-verify) instead of predicting argv, covering aliases, -C, --all/--mirror/--tags, config refspecs; unresolvable = fail closed. - Author preflight rejects commit -C/-c/--amend that would leave a non-agent author; rebase/cherry-pick of upstream history pass. - Signing-disable via -c/--no-gpg-sign rejected at argv. - Process-level mutation tests spawn the real multicall and go red when each enforcement layer is removed. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: Polish mobile timeline navigation (#5874) chore(release): release Buzz Desktop version 0.5.17 (#6234) fix(prompt): simplify pickup follow-through (#6186) fix(mcp): scope todo usage (#6216) fix(desktop): bound remote agent mention authorization (#6224) fix: bump h2 for RUSTSEC-2026-0258 (#6222) fix(desktop): bind presence retry timers (#6213) ci: make file-size policy a first-class gate (#6187) fix(desktop): eliminate mounted-view CPU burn — compositor-safe shimmer, observer append fast path, poll-tick disk reads (#6198) chore(release): release Buzz Desktop version 0.5.16 (#6191) fix(desktop): restore release agent mentions (#6182) test(desktop): cover exact workflow batch limit (#6168) chore(release): release Buzz Desktop version 0.5.15 (#6173) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…c-agent-commit-identity * origin/main: fix(desktop-chrome): preserve balanced layout when sidebar collapses (#6000) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…c-agent-commit-identity * origin/main: feat(managed-agents): close five Claude Code agent-config gaps (#4557) chore(hooks): keep mobile analysis out of pre-commit (#6236) fix(shared-ui): delay hover disclosures by default (#5821) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Address the reconciled Thufir/Gurney round-2 findings on the deterministic agent git-identity gate: - Injected identity args now outrank caller `-c`/GIT_CONFIG_PARAMETERS/ include.path/repo config, and a tampered manifest fails closed (C1). - Shell (`!`) push aliases are treated as opaque and rejected before any probe, so an alias cannot transmit before verification (C2). - Author-preserving commit modes gain a patch-id exemption so legitimate rebases of upstream human commits pass while new human-authored commits are still refused (I3). - BUZZ_PRIVATE_KEY outranks NOSTR_PRIVATE_KEY at both layers and the canonical key is restaged unconditionally as the child NOSTR_PRIVATE_KEY, so the harness and dev-mcp shim can never install split identities (I5). - The push predictor's subprocess probe is bounded by a hard timeout and fails closed on expiry (I6). Adds a real-binary integration test driving the buzz-acp spawn path so the install_git_identity wiring is regression-covered, plus mutation-sensitive unit tests for the key precedence and timeout paths. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
An ordinary (non-shell) git alias whose body carries global `-c`/`--config-env` config is expanded by git in-process after the wrapper's injected identity/signing `-c` options, so the alias-added config outranks the authority and could silently re-author or unsign a commit. enforce() inspects only the literal argv and never the alias body, so it could not catch this — a repo-local alias recreated the original human-attribution leak through the managed wrapper. Add a pre-exec verify_alias_safety pass that resolves the effective alias chain and refuses any alias whose expansion introduces global configuration, per the favor-rejection principle rather than modeling git's full alias grammar. Plain-subcommand aliases keep working. Shell (`!`) aliases stay out of scope here: their git invocations re-enter the wrapper on PATH and push-bearing ones are already rejected as opaque. Also gate the unconditional `nostr::ToBech32` import in git_identity_enforcement.rs behind #[cfg(unix)] (its only use is a unix-only test) so the Windows clippy gate stops failing on unused-import under -D warnings. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…shell aliases The prior blocklist scanned an alias body for known-bad config tokens, but git's quote-aware alias parser dequotes `'-c' 'user.email=…'` into a live config channel that a naive whitespace scan never sees — a parser-parity bypass. And `!` shell aliases were treated as safe on the commit path on the false premise that their inner git re-enters this wrapper; git prepends its own exec-path to PATH, so the inner git is the real binary and its -c outranks the inherited env authority, committing as an arbitrary human, unsigned. Invert verify_alias_safety to an allowlist: a non-shell alias is admitted only when every body token is a trivially-safe bare word (no quote/backslash, no -c/--config-env in any spelling, no =-valued option); anything else is refused without modeling git's grammar. Reject all shell aliases outright in a managed session, commit path included. This makes the whole config-injection class end by construction. Gurney's certified shapes (ci=commit, st=status, lg=log --oneline, pub=push origin main) stay allowed. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: Revert "fix(acp): gate relay-signed workflow messages on their attributed author" (#6311) fix(desktop): morph the drawer panel icon instead of sliding it (#6306) feat(desktop): refine repository-aware project workspaces (#6003) Fix mobile Activity thread navigation (#5850) perf(desktop): parallelize relay agent directory rebuild (#6258) Refine the mobile emoji picker (#5853) fix(desktop): exclude archived agents from nest, order regeneration (#5905) Add font size and conversation density preferences (#5644) fix(desktop): emit camelCase config-write payload fields (#6062) fix(desktop): downscale large avatars for agent-share PNG body (#6260) fix(desktop): preserve early relay auth challenges (#3320) Polish mobile message actions (#5873) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
… policy A bare-word alias whose body carried identity/signing flags (`commit --author … --no-gpg-sign`) passed the allowlist because every token is a plain bare word, and enforce()/verify_commit_author() keyed on the literal typed subcommand (the alias name, never the expanded `commit`) — so the flag preflights never fired. A repo-local alias through the managed wrapper could author as a human and disable signing. verify_alias_safety now returns the alias's fully-resolved expansion (typed globals + recursively-expanded command with accumulated body tokens and the user's trailing argv). run() holds that expansion to the same enforce() and verify_commit_author() preflight as a directly-typed command, keyed on the expanded subcommand. An alias can no longer do more than its expansion could typed directly, so there is no alias-specific flag list to maintain. Shell-alias and unclassifiable-syntax refusals are unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: fix(buzz-acp): loosen workspace-scan guardrail to allow named paths (#6261) fix(buzz-dev-mcp): expand leading ~ in read_file/str_replace paths (#6271) perf(desktop): move five hot renderer paths from JS into Rust (#6024) fix(media): accept portrait video resolutions (#6058) fix(desktop): hide archived channels from #/Tab autocomplete (#6156) Unify mobile channel details (#6113) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Git continues resolving aliases after the wrapper reaches its bounded expansion limit. Refuse when the next command word remains an alias so a partial expansion cannot bypass managed identity and signing policy. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…tity The deterministic-agent-commit-identity enforcement is unconditional: an agent always commits under its nostr identity. Sovereignty (VISION_SOVEREIGN.md) says the operator, not the platform, decides whose identity their agent's commits carry on their own machine. Add BUZZ_GIT_IDENTITY=agent|user (default agent, settable per-agent via persona env). `user` reuses the existing review-hardened unconfigured-session path — no git wrapper on PATH, no manifest, no injected authorship/signing config — so vanilla git resolves the operator's own identity. The nostr credential helper (relay git-over-HTTP auth) keeps installing in user mode: auth is not attribution. The mode is read once at spawn by the harness and shim, never by the wrapper per-invocation, so an agent cannot disable enforcement mid-session. An unrecognized value fails the spawn loudly rather than silently choosing a mode. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: (40 commits) chore(release): release Buzz Desktop version 0.5.18 (#6489) fix(desktop): simplify duplicate agent provenance (#6401) test(benchmarks): expand Buzz-native dataset (#6448) fix(desktop): sender names in notifications + macOS click-through routing (#6427) docs: clarify two-layer moderation ownership (#6481) Fix mobile thread tail and iOS channel header (#6399) chore(deps): pin earshot below 1.2.0 pending a VAD threshold re-pick (#6392) polish(desktop): finish Projects navigation and context chrome (#6429) fix(desktop): clarify add agents channel action (#6374) Repair stale large channel roster snapshots (#6251) feat(desktop-messages): show compact Buzz link metadata (#6252) feat(workflows): reply in-thread from send_message action (#6178) perf(desktop): split discover_acp_providers into cheap and forced paths (#6330) fix(desktop): restore recent channel sorting (#6402) fix(desktop): isolate main timeline stacking context from focus drawer (#6398) fix(desktop): make reconnect repair lossless (#6415) fix(hooks): scope pre-push lanes to branch merge-base diff (#6423) Enforce a three-day dependency cooldown (#6426) perf(desktop): resolve references without directory scans (#6328) feat(llm): stamp thinking effort on call-completed log line (#6424) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…lper In BUZZ_GIT_IDENTITY=user mode the harness returned Ok(None) before resolving the agent key, so a headless BUZZ_PRIVATE_KEY-only launch left the dev-mcp shim's credential-helper-only branch with no key — the toggle silently removed relay git AUTH, not just attribution. auth != attribution must hold on every launch path, and an operator who set user mode on a configured session has not unconfigured their auth. Resolve the canonical key (same BUZZ_PRIVATE_KEY-over-NOSTR_PRIVATE_KEY, child-over-process precedence) and stage it as the child's NOSTR_PRIVATE_KEY BEFORE the user-mode early return. user mode still installs no wrapper, manifest, keyfile, or injected config on the harness side; no key at all is still Ok(None). Wrapper unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The extra_env merge in AcpClient::spawn only staged a persona value when the parent process env lacked that key, so a global BUZZ_GIT_IDENTITY silently defeated every per-agent override in both directions — the child-over-process lookup in install_git_identity never saw the dropped value. Treat the var as an operator-controlled exception and always stage the persona value; the general parent-wins semantics for other keys are unchanged. Also derive Debug on Shim so the shim seam test's expect_err compiles under clippy -D warnings (--workspace --all-targets). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: fix(desktop): restore human barge-in over agent TTS in huddles (#6431) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The test mutated process-global env across .await points and unconditionally removed the variable, clobbering any caller-supplied value instead of restoring it. Add an RAII guard that captures the prior value and restores-or-removes on drop (including on assertion panic). It is the only env-mutating spawn test in buzz-acp, so no cross-test serialization is required. Both-direction real-merge-loop assertions are unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Agent sessions could commit under the human operator's ambient git identity. Only
buzz-dev-mcp's shim applied the nostr author/signingGIT_CONFIG_*env, and only to its own shell-tool children — the native shells of claude-code, codex, and goose never saw it. A baregit commitin those shells resolved to whateveruser.name/user.emailthe repo or global config carried, erasing the AI-attribution signal (e.g. #3140, which landed under a human identity).Change
Makes the agent commit author identity machine-managed and deterministic across every harness. Trailers still credit the human operator (
Co-authored-by+Signed-off-by); that policy is unchanged.buzz-git-identity(new crate) — the single source of truth for the author/email, NIP-GS signing, and keyfile logic, consumed by both the dev-mcp shim and the harness so an agent commits under a byte-identical identity regardless of which surface applied it. The identity email is<64-hex-pubkey>@<relay-host>; the author name is the sanitized Buzz display name (falls back to thenpub).0600identity manifest. The enforcement wrapper locates its own install directory by canonicalizing thegitonPATHagainst its own executable — the same trust channel it uses to find the realgit, immune to environment rewriting — reads the expected identity from the manifest, and injects it directly as the highest-precedence config, never trustinggit config user.email. A manifest that is missing or fails to parse in an otherwise-managed session fails the invocation closed rather than silently dropping enforcement. Manifest present = enforce; genuinely absent (keyless/unconfigured session) = passthrough.gitenforcement wrapper (git_wrapper.rs) — installed onPATHahead of the real binary.-cargs at the front of the resolved command, so it outranks caller-supplied-c,GIT_CONFIG_PARAMETERS,include.path, and repo/global config alike. ScrubsGIT_AUTHOR_*/GIT_COMMITTER_*; rejects-c user.*,--config-env=user.*,--author, and--reset-author(scoped tocommit/amsogit log --authorstill works).commit -C/-c <sha>and--amendcreate new commits that reuse another commit's author; injected config cannot override a reused author, so the wrapper inspects the resulting author and rejects any commit-creating mode that would leave a non-agent author. Ordinary commits and amends of the agent's own commits pass.git push --dry-run --porcelain --no-verify <original args>to obtain the exact resolved update set — covering config-defined and inline-c alias.*git aliases,-C/--git-dir,--all/--mirror/--tags, configremote.*.push, and wildcard refspecs — then refuses any outgoing commit not authored by the agent identity, naming the offending sha and email. A human-authored commit is exempted only when it is a patch-id-identical replay of a commit already upstream (a legitimately cherry-picked/rebased human commit — correct attribution, not new agent work masquerading as someone else); any other non-agent author is refused. The dry-run probe is bounded by a hard timeout; a timeout, an unresolvable update set, or any dry-run failure fails closed. The--no-verifyon the internal dry-run keeps it from double-running the repo's own pre-push hooks; the real push keeps its hooks, and enforcement runs unconditionally so--no-verifyon the real push cannot bypass it.-coptions, so an alias could otherwise plant higher-precedence config that re-authors or unsigns the commit — and git's quote-aware alias parser sees tokens differently from a naive whitespace scan ('-c' 'user.email=…'dequotes to real config). Rather than model that grammar, the wrapper admits a non-shell alias only when every token of its resolved body is a trivially-safe bare word: no quote or backslash characters, no-c/--config-envchannel, and no=-valued option. Anything else is refused. Shell (!) aliases are refused outright in a managed session — git runs their body with the real git ahead of the wrapper onPATH, so an inner-coutranks the inherited authority and can commit or push under an arbitrary identity, unsigned; there is no safe subset to allow. A bare-word alias can still carry identity/signing flags (commit --author … --no-gpg-sign), so on success the alias is expanded and its resolved command — accumulated body tokens across up to ten alias substitutions plus the caller's trailing argv; if another alias remains at that bound, the wrapper refuses rather than treating a partial expansion as resolved — is held to the identical identity/signing policy as the same command typed directly (enforce()and the commit-author preflight, keyed on the expanded subcommand). An alias can therefore never do more than its expansion could typed directly, and there is no alias-specific flag list to keep in sync. Bare-word aliases whose expansion is clean keep working (alias.ci = commit,alias.st = status,alias.lg = log --oneline,alias.pub = push origin main).-c commit.gpgSign=falseand--no-gpg-signare rejected at argv; env-based signing-disable is defeated by the injected highest-precedence config.buzz-acp) —AcpClient::spawnwrites the keyfile and manifest, installs the wrapper plus the nostr signer/credential helpers viabuzz-acp's own multicall personalities, prepends the wrapper dir to the childPATH, and applies the identity + signingGIT_CONFIG_*composed over the desktop's per-URL credential helper. The key is sourcedBUZZ_PRIVATE_KEY(the documented required secret) beforeNOSTR_PRIVATE_KEYat both the command and process-env layers, and the canonical key is restaged unconditionally as the child'sNOSTR_PRIVATE_KEYso the harness and dev-mcp shim can never install split identities. A managed session fails closed when deterministic identity cannot be installed; sessions with no nostr key are skipped entirely, so test spawns and unconfigured sessions are unchanged. Unix-only.base_prompt.md,nest_agents.md) — identity is machine-managed; credit the operator viaCo-authored-by/Signed-off-bytrailers, neveruser.name/user.email/-c/--author.BUZZ_GIT_IDENTITY=agent|user, defaultagent, settable per-agent via persona env).agentis everything above.userreuses the existing review-hardened unconfigured-session path — the harness installs no wrapper onPATH, no manifest, no keyfile, and no injected authorship/signing config, so vanilla git resolves the operator's own repo/global identity and signing. This matchesVISION_SOVEREIGN.md: the operator, not the platform, decides whose identity their agent's commits carry on their own machine. The nostr credential helper (relay git-over-HTTP auth) keeps installing inusermode, and the harness still stages the canonical key as the child'sNOSTR_PRIVATE_KEYbefore itsuser-mode early return so the helper can authenticate on every launch path (headless as well as desktop) — auth is not attribution, and settinguseron a configured session must not silently drop relay git auth. The mode is read once at spawn by the harness and dev-mcp shim, never by the wrapper per-invocation, so an agent cannotexport BUZZ_GIT_IDENTITY=usermid-session to disable enforcement. An unrecognized value fails the spawn loudly, naming the variable and its two values, rather than silently choosing a mode.usermode drops the operator out of commit-level AI attribution — deliberate, their claim to make; in that mode theCo-authored-by/Signed-off-bytrailers are redundant since the commit already is the operator's identity.Scope and ceiling
This is a deterministic best-effort local control, not an adversarial sandbox. Enforcement is a
PATHwrapper sharing the OS user with the agent, so it closes accidental identity leakage — the entire class behind #3140 — but does not stop a deliberate bypass: invoking/usr/bin/gitby absolute path,env -i, replacing the wrapper onPATH, deleting the manifest, or committing via libgit2/jj all sidestep it by design. A hard guarantee that holds regardless of what the agent runs would require a receive-side gate on the git host; that remains a possible separate follow-up. This PR intentionally targets the default-path commit/push surface, which is where the missed-attribution signal originates.Verification
Beyond unit tests (
buzz-git-identity66,buzz-acp817 unit + integration suites), the wrapper mechanism is exercised end-to-end by process-level tests (git_identity_enforcement.rs) that spawn the realbuzz-acp-as-gitmulticall against a real repo with a manifest present:-c user.email=…,--author=…) is rejected;<display-name> <hex@relay>;alias.quoted = '-c' 'user.email=…' commit) and a shell (!) commit alias are each refused before git runs and leaveHEADunchanged, while a plain-subcommand alias still resolves and commits agent-authored; a bare-word alias carrying identity/signing flags (alias.human = commit --author … --no-gpg-sign,--no-gpg-signalone, and a two-hop chain) is expanded and refused by the same policy as the typed command,HEADunchanged; a chain of exactly ten aliases reachingcommitremains usable, while an eleventh alias is refused before git runs with unbornHEADand zero commit objects;Each process-level test is mutation-verified: nulling the harness's
install_git_identitywiring, or dropping theenforce/verify_pushdispatch, turns the corresponding test red — confirming the layer is wired into the process boundary, not merely unit-covered. The key-precedence and bounded-probe paths carry their own mutation-sensitive unit tests. The keyfile-lifecycle test (keyfile_lifecycle.rs) spawns the real binary on its error-exit path and confirms the0600keyfile is deleted on every exit.The
BUZZ_GIT_IDENTITYtoggle is covered at three layers:GitIdentityModeparsing unit tests (unset →agent, exactagent/userwith whitespace tolerance, and loud rejection of typos/case-variants/empty naming the variable and both values); harness gate tests (userinstalls no identity dir orGIT_CONFIG_*, a persona-staged value outranks process env, explicitagentstill writes the manifest, an invalid value errors at spawn); and shim seam tests (agentinstalls thegitwrapper + signer + manifest + authorship config;userkeeps only thegit-credential-nostrhelper and itsnostr.keyfilepointer with no wrapper, signer, manifest, or authorship/signing config; unset defaults toagent; invalid fails install).Notes
Co-authored-bytrailer email.git-sign-nostr.