From d57283bb04cc1575f6338a4f9f329a5f10f147df Mon Sep 17 00:00:00 2001 From: Atomics-hub Date: Wed, 27 May 2026 13:21:28 -0700 Subject: [PATCH] Add release remote approval gate --- README.md | 7 +++-- docs/architecture.md | 1 + docs/key-lifecycle.md | 5 +++- docs/public-readiness.md | 9 ++++-- docs/release-checklist.md | 5 +++- docs/roadmap.md | 1 + docs/v0.1-release-notes.md | 1 + src/lib.rs | 56 ++++++++++++++++++++++++++++++++------ 8 files changed, 70 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ffc7bfa..9597c33 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,10 @@ cargo run -- release-audit Run the strict pre-push audit with a configured signing key file: ```sh -AGENTK_REQUIRE_SIGNING_KEY=1 AGENTK_SIGNING_KEY_FILE=../agentk-signing-key cargo run -- release-audit --strict +AGENTK_REQUIRE_SIGNING_KEY=1 \ +AGENTK_RELEASE_REMOTE_APPROVED=1 \ +AGENTK_SIGNING_KEY_FILE=../agentk-signing-key \ +cargo run -- release-audit --strict ``` Contribution and release rules live in [CONTRIBUTING.md](CONTRIBUTING.md), @@ -452,7 +455,7 @@ Not implemented yet: - real sandboxing, - eBPF/cgroup enforcement. -By default AgentK signs evidence with a static development key. Set `AGENTK_SIGNING_KEY_FILE` to a private key file created by `agentk keygen`, or set `AGENTK_SIGNING_KEY_HEX` to a 32-byte hex Ed25519 signing key for non-demo runs. Set `AGENTK_REQUIRE_SIGNING_KEY=1` in release gates to fail readiness if the configured signer falls back to the development key. On Unix, readiness also fails if the configured key file is readable by group/other users or if its parent directory is group/other writable. The CLI only prints the public key. +By default AgentK signs evidence with a static development key. Set `AGENTK_SIGNING_KEY_FILE` to a private key file created by `agentk keygen`, or set `AGENTK_SIGNING_KEY_HEX` to a 32-byte hex Ed25519 signing key for non-demo runs. Set `AGENTK_REQUIRE_SIGNING_KEY=1` in release gates to fail readiness if the configured signer falls back to the development key. Set `AGENTK_RELEASE_REMOTE_APPROVED=1` only after release approval and branch-protection review so strict release gates can pass with the approved public remote configured. On Unix, readiness also fails if the configured key file is readable by group/other users or if its parent directory is group/other writable. The CLI only prints the public key. See [SECURITY.md](SECURITY.md), [docs/threat-model.md](docs/threat-model.md), [docs/key-lifecycle.md](docs/key-lifecycle.md), [docs/mcp-proxy.md](docs/mcp-proxy.md), and [docs/public-readiness.md](docs/public-readiness.md). diff --git a/docs/architecture.md b/docs/architecture.md index f01e237..0b46f23 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -99,6 +99,7 @@ AGENTK_SIGNING_KEY_FILE set -> file signing key unset -> static development key invalid -> readiness failure AGENTK_REQUIRE_SIGNING_KEY=1 -> readiness failure unless AGENTK_SIGNING_KEY_HEX or AGENTK_SIGNING_KEY_FILE is valid +AGENTK_RELEASE_REMOTE_APPROVED=1 -> strict release gate accepts approved git remote ``` On Unix, readiness also verifies that an `AGENTK_SIGNING_KEY_FILE` path is owner-only and that its parent directory is not group/other writable, so loose custody permissions block release gates without printing the local path. diff --git a/docs/key-lifecycle.md b/docs/key-lifecycle.md index 6f7329c..337d81e 100644 --- a/docs/key-lifecycle.md +++ b/docs/key-lifecycle.md @@ -42,7 +42,10 @@ release evidence. Release gates must require a configured signer: ```sh -AGENTK_REQUIRE_SIGNING_KEY=1 AGENTK_SIGNING_KEY_FILE=../agentk-release-signing-key cargo run --locked -- release-audit --strict +AGENTK_REQUIRE_SIGNING_KEY=1 \ +AGENTK_RELEASE_REMOTE_APPROVED=1 \ +AGENTK_SIGNING_KEY_FILE=../agentk-release-signing-key \ +cargo run --locked -- release-audit --strict ``` The static development signer is acceptable only for demos and CI smoke checks. diff --git a/docs/public-readiness.md b/docs/public-readiness.md index d74b2b6..6784040 100644 --- a/docs/public-readiness.md +++ b/docs/public-readiness.md @@ -5,7 +5,9 @@ keep the same checks in CI and protect the default branch. ## Pre-Public Repository Hygiene -- [ ] No git remote configured. +- [ ] No git remote configured before first public push, or + `AGENTK_RELEASE_REMOTE_APPROVED=1` is set only after explicit release + approval and branch-protection review. - [ ] No generated `.agentk/` logs tracked. - [ ] No local paths, usernames, real URLs, or private traces in docs/tests. - [ ] No API keys, tokens, certs, private keys, or `.env` files. @@ -82,7 +84,10 @@ Before first public push: ```txt git remote -v git status --short -AGENTK_REQUIRE_SIGNING_KEY=1 AGENTK_SIGNING_KEY_FILE=../agentk-signing-key cargo run -- release-audit --strict +AGENTK_REQUIRE_SIGNING_KEY=1 \ +AGENTK_RELEASE_REMOTE_APPROVED=1 \ +AGENTK_SIGNING_KEY_FILE=../agentk-signing-key \ +cargo run -- release-audit --strict cargo fmt --check cargo test cargo clippy --all-targets --all-features diff --git a/docs/release-checklist.md b/docs/release-checklist.md index b01dbdd..37e289a 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -55,7 +55,10 @@ cargo run -- key-rotate-verify --manifest docs/key-rotation-vNEXT.json Run the strict audit with the local release key: ```sh -AGENTK_REQUIRE_SIGNING_KEY=1 AGENTK_SIGNING_KEY_FILE=../agentk-release-signing-key cargo run --locked -- release-audit --strict +AGENTK_REQUIRE_SIGNING_KEY=1 \ +AGENTK_RELEASE_REMOTE_APPROVED=1 \ +AGENTK_SIGNING_KEY_FILE=../agentk-release-signing-key \ +cargo run --locked -- release-audit --strict ``` On Unix, the audit fails if the release key file is group- or world-readable or diff --git a/docs/roadmap.md b/docs/roadmap.md index 0f8223b..fda3388 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -180,6 +180,7 @@ Status: in progress. - [x] Protect the default branch with the CI `audit` check. - [x] Add contributor guidelines for security-sensitive changes. - [x] Add a signed release checklist for tagged versions. +- [x] Add an explicit remote-approval signal for strict release gates. ## Milestone 6: v0.1 Release Shape diff --git a/docs/v0.1-release-notes.md b/docs/v0.1-release-notes.md index 9be79a2..70212e9 100644 --- a/docs/v0.1-release-notes.md +++ b/docs/v0.1-release-notes.md @@ -48,6 +48,7 @@ a durable signing key outside the repository: ```sh AGENTK_REQUIRE_SIGNING_KEY=1 \ +AGENTK_RELEASE_REMOTE_APPROVED=1 \ AGENTK_SIGNING_KEY_FILE= \ cargo run --locked -- release-audit --strict ``` diff --git a/src/lib.rs b/src/lib.rs index daf5288..a41bb15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,7 @@ const DEV_SIGNING_KEY_BYTES: [u8; 32] = [0x41; 32]; pub const SIGNING_KEY_ENV: &str = "AGENTK_SIGNING_KEY_HEX"; pub const SIGNING_KEY_FILE_ENV: &str = "AGENTK_SIGNING_KEY_FILE"; pub const REQUIRE_SIGNING_KEY_ENV: &str = "AGENTK_REQUIRE_SIGNING_KEY"; +pub const RELEASE_REMOTE_APPROVED_ENV: &str = "AGENTK_RELEASE_REMOTE_APPROVED"; #[derive(Debug, Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] #[serde(rename_all = "kebab-case")] @@ -11463,15 +11464,7 @@ fn check_git_remote(root: &Path) -> ReadinessCheck { { Ok(output) if output.status.success() => { let stdout = String::from_utf8_lossy(&output.stdout); - if stdout.trim().is_empty() { - readiness_check("git remote", ReadinessStatus::Pass, "no remotes configured") - } else { - readiness_check( - "git remote", - ReadinessStatus::Warn, - "remote configured; verify release approval and branch protection", - ) - } + check_git_remote_output(&stdout, release_remote_approved()) } Ok(output) => readiness_check( "git remote", @@ -11486,6 +11479,32 @@ fn check_git_remote(root: &Path) -> ReadinessCheck { } } +fn release_remote_approved() -> bool { + env_flag_enabled(env::var(RELEASE_REMOTE_APPROVED_ENV).ok().as_deref()) +} + +fn check_git_remote_output(stdout: &str, release_remote_approved: bool) -> ReadinessCheck { + if stdout.trim().is_empty() { + readiness_check("git remote", ReadinessStatus::Pass, "no remotes configured") + } else if release_remote_approved { + readiness_check( + "git remote", + ReadinessStatus::Pass, + format!( + "remote configured with explicit release approval via {RELEASE_REMOTE_APPROVED_ENV}; verify branch protection" + ), + ) + } else { + readiness_check( + "git remote", + ReadinessStatus::Warn, + format!( + "remote configured; set {RELEASE_REMOTE_APPROVED_ENV}=1 only after release approval and branch protection review" + ), + ) + } +} + fn check_gitignore(root: &Path) -> ReadinessCheck { match fs::read_to_string(root.join(".gitignore")) { Ok(content) if content.lines().any(|line| line.trim() == ".agentk/") => readiness_check( @@ -17180,6 +17199,25 @@ done } } + #[test] + fn git_remote_warning_requires_explicit_release_approval() { + let no_remote = check_git_remote_output("", false); + assert_eq!(no_remote.status, ReadinessStatus::Pass); + + let configured_remote = "origin\thttps://github.com/Atomics-hub/agentk.git (fetch)\n"; + let without_approval = check_git_remote_output(configured_remote, false); + assert_eq!(without_approval.status, ReadinessStatus::Warn); + assert!( + without_approval + .detail + .contains(RELEASE_REMOTE_APPROVED_ENV) + ); + + let with_approval = check_git_remote_output(configured_remote, true); + assert_eq!(with_approval.status, ReadinessStatus::Pass); + assert!(with_approval.detail.contains(RELEASE_REMOTE_APPROVED_ENV)); + } + #[test] fn release_audit_passes_with_warnings_but_not_failures() { let warn_only = release_audit_from_checks(