Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0ebb2a7
feat(acp): enforce deterministic agent git commit identity
Aug 17, 2026
14afcc2
fix(acp): close push-gate alias/-C bypasses and keyfile teardown race
Aug 18, 2026
079a82c
fix(acp): make agent git identity authoritative and fail-closed
Aug 18, 2026
fb21fd1
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 18, 2026
f2f6d29
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 18, 2026
38aa021
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 18, 2026
db8e45b
fix(acp): close review findings on agent commit-identity enforcement
Aug 18, 2026
6e80027
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 18, 2026
f8acf92
fix(git-identity): reject config-introducing git aliases in wrapper
Aug 19, 2026
7abb86a
fix(git-identity): invert alias guard to an allowlist and refuse all …
Aug 19, 2026
08715a7
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 19, 2026
d963fdb
fix(git-identity): hold expanded alias commands to the direct-command…
Aug 19, 2026
68e9658
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 19, 2026
ac5cdea
fix(git-identity): fail closed at alias resolution limit
Aug 19, 2026
4cc7339
feat(acp): add BUZZ_GIT_IDENTITY toggle for agent vs user commit iden…
Aug 21, 2026
17d50d7
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 21, 2026
2dfbebc
fix(acp): stage canonical key in user mode for the shim credential he…
Aug 21, 2026
f394b65
fix(acp): honor per-agent BUZZ_GIT_IDENTITY over global process env
Aug 21, 2026
c44f02a
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 21, 2026
c64e62e
test(acp): restore BUZZ_GIT_IDENTITY on drop in persona-precedence test
Aug 21, 2026
83397e4
fix(desktop): reserve GIT_CONFIG* env family from user overrides
Aug 25, 2026
adae0af
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 25, 2026
b444b35
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 26, 2026
fef90de
feat(git-identity): require agent signature on outgoing commits at pu…
Aug 26, 2026
d157e94
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 26, 2026
96e2378
fix(git-identity): fail closed on tampered signing contract in manifest
Aug 26, 2026
f106994
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 26, 2026
5864ccb
fix(git-identity): validate exact canonical manifest contract, reject…
Aug 26, 2026
56a4543
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 26, 2026
f2389c4
fix(git-identity): compare fixed manifest values byte-for-byte
Aug 26, 2026
cd23ed1
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 26, 2026
896cc04
fix(git-identity): honor keyless passthrough and headless relay auth
Aug 27, 2026
313e7a1
Merge remote-tracking branch 'origin/main' into wpfleger/deterministi…
Aug 27, 2026
ba05511
test(acp): isolate credential-helper gate test from ambient GIT_CONFIG
Aug 27, 2026
6fce487
fix(git-identity): resolve alias probes under caller config to close …
Aug 27, 2026
c58076a
fix(git-identity): probe aliases under the caller's complete global set
Aug 27, 2026
e8cb5da
fix(git-identity): add --shallow-file to the global-grammar table
Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ members = [
"crates/buzz-persona",
"crates/git-credential-nostr",
"crates/git-sign-nostr",
"crates/buzz-git-identity",
"crates/buzz-pair-relay",
"crates/buzz-relay-mesh",
"crates/buzz-dev-mcp",
Expand Down
4 changes: 4 additions & 0 deletions crates/buzz-acp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ url = { workspace = true }
sha2 = { workspace = true }
base64 = "0.22"
hex = { workspace = true }
buzz-git-identity = { path = "../buzz-git-identity" }
git-credential-nostr = { path = "../git-credential-nostr" }
git-sign-nostr = { path = "../git-sign-nostr" }
tempfile = "3"

# Logging
tracing = { workspace = true }
Expand Down
614 changes: 613 additions & 1 deletion crates/buzz-acp/src/acp.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ These are guidelines, not a fixed procedure — apply judgment to the task in fr
- After selecting a repository or worktree, read its root `AGENTS.md` and any path-local `AGENTS.md` files that apply before planning or editing. The workspace-level file is team context; it does not replace repository-owned instructions.
- Treat repository-owned product, architecture, and vision documents as design constraints, not optional background. Read the relevant documents before making non-trivial plans, and surface any intentional conflict with them.
- Make file changes in a worktree, not on the default branch. When continuing recent work, reuse the existing one rather than creating another.
- Before committing, read the repo-local git `user.name` / `user.email`; if email is empty, stop and ask. Include the trailers the repo requires.
- Your commit author identity is machine-managed: every commit is automatically authored and signed as your agent identity (`<pubkey>@<relay-host>`). Never set `user.name`/`user.email`, and never pass `-c user.*`, `--author`, or `--reset-author` — the managed `git` rejects those. Credit the human operator with the `Co-authored-by` and `Signed-off-by` trailers the repo requires (add them to the commit message body); if you cannot determine the operator's email for those trailers, stop and ask. (When the operator sets `BUZZ_GIT_IDENTITY=user`, commits instead carry their own git identity and these trailers are redundant.)

## Autonomy

Expand Down
121 changes: 91 additions & 30 deletions crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1895,10 +1895,47 @@ mod idle_pool_sleep_tests {
}

pub fn run() -> Result<()> {
// Multicall git-helper personalities — when the harness binary is invoked
// under one of these names (via the symlinks it installs on the
// agent-runtime child's PATH; see `AcpClient::install_git_identity`), it
// dispatches to that personality and exits before any harness setup. This
// makes buzz-acp self-contained for deterministic agent git identity: the
// enforcement wrapper AND the nostr signer/credential helper it configures
// are all reachable from a single binary, with no dependency on a separately
// resolvable buzz-dev-mcp. Mirrors buzz-dev-mcp's own shim dispatch so the
// native shells of every runtime hit the same identity-enforcing git.
match git_multicall_personality() {
Some(GitPersonality::Git) => std::process::exit(buzz_git_identity::git_wrapper::run()),
Some(GitPersonality::SignNostr) => std::process::exit(git_sign_nostr::run()),
Some(GitPersonality::CredentialNostr) => std::process::exit(git_credential_nostr::run()),
None => {}
}
config::propagate_legacy_env_vars();
tokio_main()
}

/// A git-helper multicall personality this binary can assume based on argv[0].
enum GitPersonality {
Git,
SignNostr,
CredentialNostr,
}

/// The multicall personality implied by argv[0]'s file stem, or `None` when the
/// binary was launched normally as `buzz-acp`.
fn git_multicall_personality() -> Option<GitPersonality> {
let stem = std::env::args_os()
.next()
.map(std::path::PathBuf::from)
.and_then(|p| p.file_stem().map(|s| s.to_ascii_lowercase()))?;
match stem.to_str()? {
"git" => Some(GitPersonality::Git),
"git-sign-nostr" => Some(GitPersonality::SignNostr),
"git-credential-nostr" => Some(GitPersonality::CredentialNostr),
_ => None,
}
}

#[tokio::main]
async fn tokio_main() -> Result<()> {
// Install the ring crypto provider for rustls (required for wss:// connections).
Expand Down Expand Up @@ -4826,6 +4863,18 @@ fn extract_auth_methods(init_result: &serde_json::Value) -> Vec<serde_json::Valu
.unwrap_or_default()
}

/// Shut down the client and exit with `code` after printing `msg` to stderr.
///
/// `std::process::exit` runs no destructors, so error/timeout paths must call
/// `shutdown().await` — which reaps the child AND deletes the git-identity
/// keyfile tempdir — before exiting. Taking the client by value guarantees no
/// caller can exit while still holding a live client (and thus a live keyfile).
async fn shutdown_and_exit(mut client: AcpClient, msg: &str, code: i32) -> ! {
client.shutdown().await;
eprintln!("{msg}");
std::process::exit(code)
}

/// `buzz-acp auth-methods` — spawn an adapter, initialize it, print authMethods.
async fn run_auth_methods(args: AuthMethodsArgs) -> Result<()> {
let mut client = match spawn_auth_client(&args.agent).await {
Expand All @@ -4839,14 +4888,15 @@ async fn run_auth_methods(args: AuthMethodsArgs) -> Result<()> {
let init_result = match tokio::time::timeout(MODELS_TIMEOUT, client.initialize()).await {
Ok(Ok(result)) => result,
Ok(Err(e)) => {
client.shutdown().await;
eprintln!("error: agent initialize failed: {e}");
std::process::exit(1);
shutdown_and_exit(client, &format!("error: agent initialize failed: {e}"), 1).await;
}
Err(_) => {
client.shutdown().await;
eprintln!("error: agent timed out ({MODELS_TIMEOUT:?})");
std::process::exit(1);
shutdown_and_exit(
client,
&format!("error: agent timed out ({MODELS_TIMEOUT:?})"),
1,
)
.await;
}
};

Expand Down Expand Up @@ -4887,27 +4937,31 @@ async fn run_authenticate(args: AuthenticateArgs) -> Result<()> {
let init_result = match tokio::time::timeout(MODELS_TIMEOUT, client.initialize()).await {
Ok(Ok(result)) => result,
Ok(Err(e)) => {
client.shutdown().await;
eprintln!("error: agent initialize failed: {e}");
std::process::exit(1);
shutdown_and_exit(client, &format!("error: agent initialize failed: {e}"), 1).await;
}
Err(_) => {
client.shutdown().await;
eprintln!("error: agent initialize timed out ({MODELS_TIMEOUT:?})");
std::process::exit(1);
shutdown_and_exit(
client,
&format!("error: agent initialize timed out ({MODELS_TIMEOUT:?})"),
1,
)
.await;
}
};

let supports_method = extract_auth_methods(&init_result)
.iter()
.any(|method| method.get("id").and_then(|id| id.as_str()) == Some(args.method_id.as_str()));
if !supports_method {
client.shutdown().await;
eprintln!(
"error: auth method '{}' is not advertised by this adapter",
args.method_id
);
std::process::exit(1);
shutdown_and_exit(
client,
&format!(
"error: auth method '{}' is not advertised by this adapter",
args.method_id
),
1,
)
.await;
}

let result =
Expand All @@ -4919,14 +4973,15 @@ async fn run_authenticate(args: AuthenticateArgs) -> Result<()> {
Ok(())
}
Ok(Err(e)) => {
client.shutdown().await;
eprintln!("error: authenticate failed: {e}");
std::process::exit(1);
shutdown_and_exit(client, &format!("error: authenticate failed: {e}"), 1).await;
}
Err(_) => {
client.shutdown().await;
eprintln!("error: authenticate timed out ({AUTHENTICATE_TIMEOUT:?})");
std::process::exit(1);
shutdown_and_exit(
client,
&format!("error: authenticate timed out ({AUTHENTICATE_TIMEOUT:?})"),
1,
)
.await;
}
}
}
Expand Down Expand Up @@ -4962,14 +5017,20 @@ async fn run_models(args: ModelsArgs) -> Result<()> {
let (init_result, session_resp) = match protocol_result {
Ok(Ok(tuple)) => tuple,
Ok(Err(e)) => {
client.shutdown().await;
eprintln!("error: agent communication failed: {e}");
std::process::exit(1);
shutdown_and_exit(
client,
&format!("error: agent communication failed: {e}"),
1,
)
.await;
}
Err(_) => {
client.shutdown().await;
eprintln!("error: agent timed out ({MODELS_TIMEOUT:?})");
std::process::exit(1);
shutdown_and_exit(
client,
&format!("error: agent timed out ({MODELS_TIMEOUT:?})"),
1,
)
.await;
}
};

Expand Down
Loading
Loading