feat(acp): enforce deterministic agent git commit identity - #6177
feat(acp): enforce deterministic agent git commit identity#6177wpfleger96 wants to merge 68 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>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
I found three blocking contract failures at c64e62e599ffaaf223a111c34ea600d341967f57:
-
P1: managed git can create and push unsigned agent-authored commits.
enforce()rejects--no-gpg-signforcommit|am|tag|rebase|cherry-pick|revert, but notmerge/pull; the outgoing gate then checks only author email and never verifies signature presence or the expected key. A focused real-wrapper reproduction created an unsigned merge withgit merge --no-gpg-sign --no-ff, observed%G? = N, and pushed it successfully.git commit-treeproduces the same systemic bypass through the installed wrapper. This contradicts the PR’s “every commit is automatically signed” contract. At minimum cover every signing-capable porcelain path; the robust boundary is to require a valid expected-key NIP-GS signature on each new agent-authored outgoing commit, with merge/plumbing push regressions. -
P1:
BUZZ_GIT_IDENTITY=usercan erase relay git authentication through descriptor env. Desktop stages its credential-helperGIT_CONFIG_*entries and then writesdescriptor.envafterward (runtime.rs:781-811). A descriptor withBUZZ_GIT_IDENTITY=userplusGIT_CONFIG_COUNT=0reaches the harness; user mode intentionally returns before installing identity config, leaving the clobbered count in place. That violates the explicit auth-is-not-attribution guarantee. Preserve/reapply the helper after descriptor env or reserve the completeGIT_CONFIG_*family, and test the actual Desktop-to-harness layering rather than a freshCommand. -
P2: the changed Nest guidance is not delivered to existing installs and conflicts with #6707. This PR rewrites
nest_agents.mdbut leavesNEST_AGENTS_VERSIONat 4 and adds no content/upgrade test, so existing v4 Nests never receive it. #6707 bumps 4→5, tests refresh/preservation, and intentionally removes the unconditional humanSigned-off-bymandate that #6177 retains. The two branches edit the same block and #6707’s content test rejects #6177’s exact wording. Reconcile onto one policy contract; keep #6707’s version bump and upgrade coverage rather than landing both as-is.
The runtime mechanism in #6177 is broader than #6707: #6707 is policy-neutral generated guidance and does not replace identity enforcement. Its wording already allows a managed agent identity while keeping repository policy authoritative, so it is the safer documentation base to combine with a corrected runtime implementation.
Current CI for #6177 is green and git diff --check passes, but neither covers these boundary failures.
A user-supplied GIT_CONFIG_* entry (persona/agent/global env_vars) is layered onto the spawn command AFTER the relay credential-helper GIT_CONFIG_* Buzz stages, so a single GIT_CONFIG_COUNT=0 silently orphans the helper. In BUZZ_GIT_IDENTITY=user mode install_git_identity returns before re-staging anything, so the clobber stands and relay git auth is erased — violating the auth-is-not-attribution guarantee. Reserve the whole GIT_CONFIG* family (bare name + GIT_CONFIG_ prefix) in the shared is_reserved_env_key predicate. This is the single chokepoint every env layer already routes through (save-time validation, spawn-time filter, and the remote-deploy launch env), closing both the local and headless paths at once. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: (54 commits) Extract community persistence (#6668) Fix mobile Huddle agent voice turn states (#6611) Add inline profile camera capture (#6680) Hide Huddles in mobile agent DMs (#6676) fix(desktop): polish inline chip states (#6718) Centralize replaceable event persistence (#6660) feat(workflows): discover trigger filter values (#6712) feat(desktop): simplify the message action rail (#6529) fix(desktop): restore icon-only remote marker (#6491) fix(ci): prevent poisoned Rust caches (#6618) docs(security): route reports through private advisories (#6728) fix(composer): wrap Buzz chip labels without orphaning icons (#6581) fix(desktop): bound thread /query and surface load errors, not false-empty (#6447) fix(messages): route edits to the owning composer (#6575) fix(mobile): join starter channels after accepting invite (#5915) Add mobile profile editing (#6583) fix(desktop): align jump-to-latest pill with composer height (#6606) fix(desktop): emit singular `mention` feed category so alerts route correctly (#6665) fix(mobile): recover stale and shuffled messages (#6691) feat(mobile): browse and join open channels (#6243) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary - replace the generated Nest's unconditional human author/sign-off rules with portable guidance that separates authorship, material co-authorship, DCO certification, and cryptographic signing - defer attribution to repository-local policy, forbid inferred or guessed identities, and require inspection of every outgoing commit - bump the Nest template version so existing installations refresh, with regression coverage for fresh generation and upgrade preservation ### Related issue None found. Related runtime identity work exists in #6177, but this PR is intentionally limited to the generated Nest guidance and its refresh behavior. ### Testing - `bin/just desktop-tauri-clippy` - `bin/just desktop-tauri-test` - `bin/just file-size-check` - `git diff --check` - pre-push hooks: `push-head-scope`, `branch-skew`, `file-size-check`, and `desktop-tauri-checks` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
…c-agent-commit-identity Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…sh gate Unsigned agent-authored commits created via git merge/pull --no-gpg-sign or the commit-tree plumbing sail past the flag-based enforce() rejection, which only covers commit/am/tag/rebase/cherry-pick/revert on the literal argv. Extend the existing verify_push walk — which already visits every outgoing commit not on a remote and checks author email — to also require a valid NIP-GS signature by the agent key on each agent-authored commit, gated on the session actually enforcing signing (commit.gpgSign=true). One check then covers every commit-creation path, including plumbing, without growing the enforce() blocklist. 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(cli): preserve signatures in event reads (#6884) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
ff6a3e4 to
d157e94
Compare
Prevent `--no-dry-run` (and prefix abbreviations) from clearing the dry-run bit that the verification probe injects, which would turn the read-only probe into a real push before authorship/signature checks run. Guard added to `reject_receive_pack_override` before any probe starts. Classify push commands from the already-validated `effective_argv` rather than re-resolving from the original argv. With the original argv, `alias.pub = -p push` causes the classifier to take `-p` as the command word, find no `alias.-p`, and return NotPush — bypassing push verification. Using the validated expansion gives the correct result without a second alias walk. Covers the exact ten-hop alias boundary. Inject `--no-replace-objects` into all five verification helpers (`commit_author_email`, `commit_signature_is_agent`, `commit_patch_id`, both paths of `upstream_patch_ids`, `rev_list_outgoing`). Git's documented behavior: replacements affect ordinary object reads but NOT pack transfer. Without this flag, `git replace REAL DECOY` makes the probes inspect DECOY while the push sends REAL. Fix scheme extraction in `inspect_push_config` to strip a `key=` prefix before `://`. For tokens like `remote.origin.url=https://…` or `--repo=https://…` the old code treated everything before `://` as the scheme, falsely refusing valid built-in transports. New formula: find the last `=` before `://` and start the scheme field after it. Seven new regressions in `git_wrapper::tests` cover all four surfaces with mutation-sensitive evidence. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: Add voice notes to desktop messages (#6978) feat(desktop): add thread-scoped ACP session experiment (#6909) fix(desktop): scope composer autocomplete to focus (#6860) feat(desktop): add isolated named demo builds (#6407) fix(model-capabilities): humanize databricks goose model names (#7135) feat(db): add NIP-FI identity and final-admission schema foundation (#6994) feat(buzz-acp): give each channel thread its own agent session (#6732) docs: add review-proven failure-path & async-state rules to AGENTS.md (#7061) fix(desktop): back split thread headers (#7137) add public descriptions to agent personas (#7126) feat(desktop): add protected-build Bestie experiment (#6902) fix(relay): reject a frame on its own acknowledgement channel (#6961) fix(acp): wake agents from workflow messages (#6953) feat: render agent avatars as squircles (#7106) fix(ci): salvage Codex review output on PTY-shutdown hang (#7042) fix: retrieving cold memories; add regression task (#6950) Enforce NIP-OA authorization time bounds (#7004) feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229) feat(desktop): use segmented controls for channel creation (#6845) feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…c-agent-commit-identity * origin/main: ci: run PostgreSQL tests in isolated lane (#6730) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…IG_* guard Two test-side defects identified in Thufir's pass 1 re-review: 1. verify_push_rejects_unsigned_commit_via_replacement_ref previously tested the author-probe bypass (REAL=human-authored, DECOY=agent-authored) instead of the signature-probe bypass it claimed to cover. Removing --no-replace-objects from commit_signature_is_agent left the old test green because the author gate fired first — the mutation target was unprotected. The test now uses a genuinely signed DECOY: writes a 0600 keyfile with the secp256k1 spec test vector (secret=0x03, pubkey=f9308a…036f9), builds a real Authority with that keypair and the absolute git-sign-nostr path, and creates REAL as an unsigned agent-authored commit. The bypass precondition (git show REAL without --no-replace-objects → %G?=G) is asserted, then commit_signature_is_agent(REAL) must return Some(false) — removing --no-replace-objects makes it return Some(true) → verify_push passes → test fails. Skips with a diagnostic when git-sign-nostr is not yet built. A find_git_sign_nostr() helper locates the binary via current_exe() path navigation (target/<profile>/git-sign-nostr) so no PATH manipulation of the outer test environment is needed. 2. clear_git_config_env() in acp.rs mutated process-global GIT_CONFIG_* state without a serialization mutex. Under parallel nextest execution, one guard's Drop could restore the ambient credential.helper while a sibling test was still in its assertion phase, causing nondeterministic failures (Thufir reproduced 1/25 red at --test-threads=8 with ambient GIT_CONFIG_COUNT=10 / GIT_CONFIG_KEY_2=credential.helper). GitConfigEnvGuard now holds a MutexGuard from a new GIT_CONFIG_ENV_LOCK that is acquired before the snapshot/clear and released only after the restore in Drop. Verified: 25/25 green at --test-threads=8 with ambient GIT_CONFIG_* intact; 7/7 green under cargo nextest run -p buzz-acp --lib. 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(dev): keep the canonical profile when launching from desktop/ (#7143) feat(buzz-auth): add production NIP-FI federated assertion runtime (#7109) Hide download action on voice notes (#7182) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes required: two P1 defects at head e98b0a2d0eb315365d35d07946561d3a3f4b0820, reviewed against base bd73490418266f267d9bb3bdf13e64582adc8e80. This is a bounded re-review of the four previously reported push-verification defects, not an expansion into sandbox enforcement.
P1: --no-dr still makes verification publish before checking commits
The new guard at lines 929–940 only rejects starts_with("--no-dry"). Git accepts the shorter unique abbreviation --no-dr, which does not match that predicate. Git’s parser strips no-, matches the unique dr prefix of dry-run, and clears the option bit. The push option is declared as a negatable bit.
Given an existing wrong-author or unsigned commit and an empty ordinary remote, git push --no-dr origin main reaches the probe constructor as push --dry-run --porcelain --no-verify --no-dr origin main. The later negation turns it into a real push, without the normal pre-push hook, before authorship/signature verification. Subsequent remote inventory can even exempt the newly published commit as already present. This is the previous P1 incompletely fixed, not a new requirement.
Required: make probe controls non-overridable for every Git-accepted spelling, including --no-dr; preserve correct option/delimiter semantics. Add a real-wrapper regression that checks the destination remains unchanged, not merely the wrapper’s exit status. The new cases stop at --no-dry and miss this spelling.
P1: a shadow alias now suppresses verification of built-in push
The changed dispatch at lines 312–334 trusts the expanded command, but the resolver substitutes alias.<name> even when that name is a built-in. Git explicitly ignores aliases that hide existing commands, including after an alias hop (dispatch source).
With the same existing rejected commit and empty remote, git -c alias.push=status push origin main is expanded to status by the wrapper. is_push_command(effective_argv) returns NotPush, so no outgoing check runs; exec_real_git then executes the original argv, and Git runs built-in push, ignoring alias.push. The commit reaches the remote. A normal pub → push alias with alias.push=status has the same mismatch. Prior dispatch recognized literal push, so the direct form is a new regression in this correction.
Required: honor Git’s built-in command precedence at every alias hop so verification and execution classify the same command. Cover direct push and pub → push with alias.push=status through the installed wrapper, asserting policy rejection and unchanged destination. Keep the original -p push and exact-ten-hop push cases working; their new tests hand-construct effective argv and call helpers, so reverting the production dispatch line would not affect those tests.
Disposition and regression evidence
The original -p push/ten-hop paths now reach verification in source. Replacement interpretation is disabled on the changed author, signature, outgoing-graph, and patch-equivalence probes. Inline -c remote.origin.url=https://… and attached --repo=https://… no longer fail the whole-token scheme check. Those production corrections are not reopened here.
Coverage is weaker than the new mutation claims: verify_push_rejects_unsigned_commit_via_replacement_ref substitutes a wrong-author fixture for a signed-decoy/unsigned-agent fixture, so it does not isolate the signature guard. Use the existing real-signer process fixture for that case, and bind alias regressions to run() rather than manually reconstructed effective argv. This is missing evidence for the agreed repair, not evidence that the new signature code itself is broken.
Read-only source/metadata review, including independent lanes: no checkout, build, tests, or PR-code execution; no new CI/runtime pass claimed. The default-PATH contract, operator-selected user mode, and separate authentication behavior remain fixed. Absolute-path Git, replacement binaries/manifests, hostile hosting, and receive-side enforcement remain out of scope.
…lation Four issues fixed: 1. Both newline-guard tests contained a mutation-evidence block that proved git truncates paths at newlines. Git 2.54.0 no longer truncates — the block's assert panicked unconditionally. Removed the version-dependent blocks and the supporting decoy-repo setup; the core security invariant (verify_push refuses, destination stays empty, error message matches) is preserved and version-portable. 2. GitConfigEnvGuard now also saves/removes BUZZ_PRIVATE_KEY and BUZZ_AUTH_TAG (in addition to GIT_CONFIG_*). git-sign-nostr loads BUZZ_PRIVATE_KEY before nostr.keyfile, so the harness key was silently winning over the test-vector key even with GIT_CONFIG_* cleared. BUZZ_AUTH_TAG is a harness-signed owner attestation; present alongside a different test-vector key, git-sign-nostr aborts with an auth-tag mismatch error. 3. verify_push_rejects_unsigned_agent_commit_when_signing_enforced was ambient-identity-sensitive: GIT_CONFIG_* injected the harness email as user.email, overriding the repo-local AGENT_EMAIL the test requires. Added GitConfigEnvGuard. 4. Justfile test-unit recipe now builds git-sign-nostr and runs cargo nextest run -p buzz-git-identity --lib. The replacement-ref test (verify_push_rejects_unsigned_commit_via_replacement_ref) panics on binary absence rather than silently skipping; building it first makes the lane unconditional. 116/116 tests pass with ambient GIT_CONFIG_COUNT=10 active. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: feat(buzz-agent): add DatabricksAuthCoordinator single-flight OAuth (#5545) fix(desktop): preserve keyring identity during recovery (#7203) feat(mobile): prepare `buzz-push-gateway` for deployment (#7158) ci: relax file-size ceilings by surface (#6485) fix(mobile): isolate extension linker flags; complete iOS build in CI (#7187) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> # Conflicts: # .github/workflows/ci.yml
Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
P1-a: extend the dry-run negation guard in
reject_receive_pack_override from starts_with("--no-dry") to
starts_with("--no-dr"). Git accepts --no-dr as the minimal unique
prefix of --no-dry-run (--no-d is ambiguous with --no-delete);
the prior guard missed that form and allowed --no-dr to clear the
dry-run bit injected by the probe, turning a read-only verification
step into a real push.
P1-b: honour git's builtin-command precedence in verify_alias_safety.
Git dispatches builtins before consulting alias config, so an
alias.push=status setting is silently ignored at exec time. The
expansion loop previously followed it, producing an effective_argv
with subcommand status -> NotPush -> verification skipped ->
exec_real_git ran the original push argv -> git ignored the alias ->
real push. Fix: call git --list-cmds=builtins once per invocation and
break the expansion loop when the current name is a builtin. Also
extract run() body into run_inner(argv, authority) so tests can
exercise the full verify_alias_safety -> is_push_command -> verify_push
dispatch path with a controlled authority.
Regression tests: --no-dr refused by verify_push + mutation precondition
on reject_receive_pack_override; two run_inner-based alias tests confirm
push with alias.push=status is refused and pub->push chain terminates at
the builtin with alias.push=status ignored. 10x parallel ambient green.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…t 2.52+) Git 2.52 changed run_argv() to call handle_alias() before handle_builtin() for deprecated builtins (whatchanged, pack-redundant). The R9 builtin short-circuit treated ALL builtins as builtin-first, so alias.whatchanged = push ... was not expanded by verify_alias_safety — classify NotPush, skip verification — while real git 2.52+ expanded the alias and pushed. Fix: query --list-cmds=deprecated for the same binary. A name in BOTH builtins and deprecated sets is alias-first on that binary; skip the short-circuit and continue alias lookup. On pre-2.52 git (--list-cmds= deprecated exits non-zero), the deprecated set is empty and all builtins remain builtin-first — matching that git's actual dispatch. Add verify_push_deprecated_builtin_alias_is_expanded_and_refused: dispatches via run_inner with alias.whatchanged=push; on 2.52+ git, asserts verify_alias_safety returns Some(push...) not Ok(None), and that run_inner refuses the human-authored push end-to-end with destination unchanged. Test self-gates on --list-cmds=deprecated support so it skips cleanly on pre-2.52 binaries. Update section comments to accurately describe the builtin-first EXCEPT deprecated rule (2.52+). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Add two installed-wrapper regressions to git_identity_enforcement.rs that exercise the full buzz-acp-as-git multicall dispatch (Carl's required real-entry-path shape) for the two builtin-alias bypass cases fixed in the previous commit. wrapper_refuses_push_via_builtin_shadowing_alias: sets alias.push=status in repo config and invokes the wrapper with push origin main. With the fix, verify_alias_safety recognises push as a non-deprecated builtin and returns Ok(None) (no expansion); verify_push fires and refuses the human-authored HEAD. Mutation: removing the builtin short-circuit expands push to status, classifies NotPush, skips verification, and the remote receives the commit (for-each-ref assertion fires). wrapper_refuses_push_via_deprecated_builtin_alias: sets alias.whatchanged=push and invokes the wrapper with whatchanged origin main. Self-gates on git --list-cmds=deprecated listing whatchanged (skips pre-2.52). With the fix, whatchanged is NOT short-circuited (deprecated builtins are alias-first on 2.52+); the alias expands to push, verify_push fires and refuses the human-authored HEAD. Mutation: removing the deprecated-set exclusion from the builtin check re-creates the bypass on 2.52+ (remote receives the commit). Both tests assert refusal (exit != 0), push-gate error message, and destination unchanged. 16/16 process-level tests pass (was 14/14); 119/119 lib tests pass (was 118/118 before deprecated unit test). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ion comments The process-level deprecated-builtin regression used alias.whatchanged=push as its fixture body. When the deprecated-exclusion fix is deleted, is_push_command (the fallback resolver) still follows alias.whatchanged=push directly to Push, so verify_push still runs and the test stays green — the mutation target was not covered. Fix: change fixture to alias.whatchanged=-p push. Without the fix, is_push_command takes -p as the command word, finds no alias.-p, returns NotPush, and real git reparses -p as a global and executes push unverified. With the fix, verify_alias_safety expands whatchanged via the alias (-p push) to effective_argv with subcommand push, policy refuses, and the destination- unchanged assertion would fire if the fix were deleted. Also rewrite all 2.52+/pre-2.52 version claims in git_wrapper.rs and git_identity_enforcement.rs as feature-based claims (binaries where --list-cmds=deprecated succeeds/exits non-zero) to match the feature-probe model the code already uses. Upstream bf68b1169 shipped in v2.51.1 not v2.52; the feature probe is the correct gate regardless. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The comment at git_identity_enforcement.rs:1378-1380 incorrectly called -p "the --porcelain global" and described the invocation as equivalent to `git push --porcelain ...`. Git's top-level -p is --paginate, not --porcelain; push's machine-readable flag is the separate long option --porcelain. Correct the comment to name --paginate and show the accurate reparsed form `git -p push origin main`. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The 16 process-level tests in git_identity_enforcement.rs were not selected by any CI lane — neither just test-unit nor any other job in ci.yml/lefthook.yml. The enforcement layer they guard could be deleted and CI would stay green. Add cargo nextest run -p buzz-acp --test git_identity_enforcement immediately after the existing --lib lane. Move cargo build -p git-sign-nostr ahead of both buzz-acp lanes (the enforcement suite requires the binary, as does buzz-git-identity --lib). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested: two residual alias-dispatch defects
Reviewed head c3cd14629432cd2e5567d3be3abfdf2da0c7e93a against base 5aed49b505a7e27f3b0e34dafa53d6c4e8cdcd64. This is a bounded corrective review of the default-PATH identity/signing contract, not a request for sandbox or receive-side enforcement. Both independent lanes are integrated. Source-only: no checkout, build, tests, or PR-code execution.
P1: Account for Git 2.54 subsection aliases before dispatch
At git_wrapper.rs:575–581, alias lookup only requests alias.<name>; the terminal-hop check and fallback classifier repeat that lookup. Git 2.54 also supports alias.<name>.command, documented here and resolved in configuration order by alias.c:23–78.
With a managed wrapper, Git 2.54, a human-authored or unsigned outgoing HEAD, and an empty reachable origin:
git -c alias.pub.command=push pub origin mainThe wrapper finds no alias.pub, treats pub as NotPush, and skips verify_push. Final exec preserves the original invocation; Git then expands the supported subsection alias and publishes the existing objects. Injecting identity/signing configuration does not change those existing commits. This is an ordinary default-path alias, not an alternate-binary escape.
Resolve both alias representations with Git-compatible ordering/case semantics, or explicitly reject an unresolved subsection alias before exec. Use the same resolution at every hop. Add an installed-wrapper regression asserting rejection and unchanged destination for this form and conflicting plain/subsection definitions. A positive control should preserve whichever alias forms are supported.
P2: Do not re-expand ignored aliases after resolving a builtin
is_push_command, lines 414–440, still performs a separate alias walk without builtin precedence:
git -c alias.status=push statusThe corrected safety resolver appropriately stops at builtin status. The classifier then follows the ignored alias.status=push and calls verify_push, which probes the original status invocation with --dry-run --porcelain --no-verify (resolve_push_sources:1744–1757). Git status rejects the push-only options, so a legitimate non-push command is blocked. The same mismatch can occur after an ordinary alias resolves to status.
After complete alias validation, classify the resolved command token directly rather than resolving aliases again. Add a real-wrapper positive control proving ignored builtin aliases do not change non-push behavior.
Prior findings and validation
The prior shortest-negation --no-dr bypass and alias.push=status bypass are corrected in source. The signed-decoy/unsigned-real replacement fixture now checks the actual production signature probe. The installed-wrapper alias tests and identity library suite are wired through just test-unit, after building git-sign-nostr.
The additional process-level --no-dr case remains a useful coverage improvement, not a residual P1: the current regression already calls production verify_push, asserts the specific refusal, and checks the remote stays empty. Do not claim that library run_inner tests alone mutation-prove their post-call assertions: on Unix, a missed gate can exec-replace the test process before those assertions. No test execution or mutation verification is claimed in this review.
Exit criteria are the two dispatch corrections and production-bound regressions above. Operator agent/user choice and the declared best-effort ceiling remain unchanged.
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.