feat(cli): add signed manifest verification commands#43
Merged
marmar9615-cloud merged 2 commits intomainfrom Apr 29, 2026
Merged
feat(cli): add signed manifest verification commands#43marmar9615-cloud merged 2 commits intomainfrom
marmar9615-cloud merged 2 commits intomainfrom
Conversation
Fifth implementation PR for v0.5.0 signed manifests. Wires the core verifier from PR #39 + the scanner check IDs from PR #41 into operator-facing CLI commands. No MCP server enforcement, no remote /.well-known/agentbridge-keys.json fetch in this PR. Adds: - packages/cli/src/commands/verify.ts — `agentbridge verify <manifest> --keys <keyset.json>` with optional flags --expected-issuer, --now, --clock-skew-seconds, --json. Exit 0 on verified, exit 1 on any failure (carries the verifier's stable reason enum), exit 2 on usage error. --json mode emits a parseable structured outcome on stdout with no prose. - packages/cli/src/commands/keys.ts — `agentbridge keys generate --kid <id> --issuer <origin> --out-public <path> --out-private <path> [--alg EdDSA|ES256]`. Local-dev helper for bootstrapping a key set. Defaults to Ed25519. Refuses to silently discard freshly-generated material (--out-private is required). Writes the private key with mode 0o600 (owner-only) on POSIX. Never echoes the private `d` parameter to stdout/stderr; prints a sensitivity warning routed to stderr. - packages/cli/src/commands/key-loader.ts — small shared helper that loads a key set from disk and runs validateKeySet(). - packages/cli/src/tests/signature-cli.test.ts — 25 tests: * Default unsigned validation behavior unchanged. * --require-signature on an unsigned manifest exits 1. * --require-signature without --keys on a signed manifest exits 1 (refuses to silently report "verified" without verifying). * Ed25519 + ES256 happy paths via the spec/signing/test-vectors.json vectors. * Tampered, unknown-kid, expired, malformed-key-set, missing key-set path. * verify happy path, --json structure (success + failure), --expected-issuer mismatch, missing --keys (exit 2), missing manifest (exit 2). * Output never echoes the public-key x/y bytes (pinned for both success and failure paths). * keys generate: Ed25519 keypair, schema-valid public key set (no private `d`), POSIX mode 0o600 on the private file, no private bytes on stdout/stderr, sensitivity warning on stderr, --out-private required (exit 2 if omitted), non-canonical issuer rejected, freshly-generated key round-trips through Node crypto sign + agentbridge verify. Modifies: - packages/cli/src/commands/validate.ts — accepts new flags --keys, --require-signature, --expected-issuer, --now, --clock-skew-seconds. Default (no signature flags) behavior bit-identical to v0.4.x. With --keys, runs verifier and prints outcome. With --require-signature without --keys, refuses silent "verified" outcomes — surfaces missing-signature (unsigned) or no-key-set-supplied (signed but unverifiable) with exit 1. JSON mode includes a `signature` field next to the existing schema-validation result. Exports summarizeVerifyResult so the verify command shares the same output shape. - packages/cli/src/index.ts — dispatches `verify` and `keys` subcommands; threads new flags into runValidate / runVerify; expands --help with the new commands and a parseSkew helper. - packages/cli/README.md — adds concise sections for validate --keys / --require-signature, the verify command, the keys generate command (with explicit local-dev / KMS-in-production warnings), updated regression-tested-examples blurb, no-MCP-enforcement-yet reminder. Does NOT (deliberately): - add MCP server signature enforcement - fetch remote key sets - require signatures by default - bump any package version - add any runtime dependency - publish, tag, or release Browser / computer-use status: - Not needed. The CLI is a Node binary; no UI/browser surface changed. The Studio dashboard and demo-app weren't touched. Live CLI command smoke against the built dist (validate default, validate --keys, verify happy, verify --json, verify tampered → exit 1 + signature-invalid, keys generate with POSIX 0o600 + private bytes never on stdout) is the relevant live test for this surface and is documented in the PR body. Verified locally: - npm run typecheck:clean (clean) - npx vitest run packages/cli/src/tests/signature-cli.test.ts (25/25) - npx vitest run packages/cli/src/tests (17 + 11 + 11 + 5 + 25 = 69/69) - npm test (435/435 across 26 files; was 409/25 on main = +26) - npm run build (all packages built) - npm run pack:dry-run (all six @marmarlabs/agentbridge-* OK at 0.4.0; CLI packed 29.4 → 57.4KB for the new modules) - npm run validate:examples (✅ all examples validate) - npm run validate:mcp-config-examples (✅ all client-config examples validate) - live: validate (default + --keys), verify (happy + --json + tampered), keys generate (mode 600, no private bytes on stdout) — all pass against `node packages/cli/dist/bin.js`. Tracking: #31 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
13776cd to
1f078fa
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 13776cd251
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex review of PR #43 caught a real safety bug: `fs.writeFile(..., { mode: 0o600 })` only applies the mode when Node *creates* the file. On a rewrite — the realistic operator scenario where someone re-runs `agentbridge keys generate` against an existing private-key path — Node truncates and writes but preserves the existing permissions. An older 0o644 file would silently keep world-read after fresh private bytes were written into it, breaking the documented owner-only contract on shared systems. Fix: explicit `fs.chmod(outPrivate, 0o600)` after the write on POSIX. Windows skipped — chmod there only toggles the read-only bit, not real permissions, and our existing test only checks mode on POSIX. Test added: pre-create the private file with 0o644, run `keys generate` against it, assert the resulting file mode is 0o600. Without the fix, the test fails with mode 0o644. Verified locally: - npx vitest run packages/cli/src/tests/signature-cli.test.ts (26/26) Refs: PR #43, issue #31 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.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.
Summary
Fifth implementation PR for v0.5.0 signed manifests. Wires the
core verifier from PR #39 into operator-facing CLI commands. No
MCP server enforcement and no remote
/.well-known/agentbridge-keys.jsonfetch in this PR. Default unsigned validation behavior is
unchanged — adopters opt into signature verification via new
flags.
docs/designs/signed-manifests.md§13.846adc2cc0cd51d0bf7713314c023Validate command — new flags
agentbridge validate <manifest> [--keys <keyset.json>] [--require-signature] [--expected-issuer <origin>] [--now <iso>] [--clock-skew-seconds <n>] [--json]--keys <ks>verifyManifestSignature. Exit 0 on both green; exit 1 on either failure.--require-signaturesignatureblock (exit 1).--require-signature --keys <ks>--require-signaturewithout--keyson a signed manifestverification skipped.--jsonsignature: { ok, kid?, iss?, alg?, …, reason?, message? }next to the existing schema result.New
agentbridge verifycommandagentbridge verify <manifest> --keys <keyset.json> [--expected-issuer <origin>] [--now <iso>] [--clock-skew-seconds <n>] [--json]Always runs the verifier. Returns the structured outcome with the
stable
VerifyManifestSignatureFailureenum from@marmarlabs/agentbridge-core.--jsonmode emits a parseableJSON object on stdout with no prose, suitable for CI.
New
agentbridge keys generatecommand (local dev only)agentbridge keys generate --kid <id> --issuer <origin> --out-public <path> --out-private <path> [--alg EdDSA|ES256] [--not-before <iso>] [--not-after <iso>]Local-dev helper for bootstrapping a publisher key set. Default
algorithm Ed25519. ES256 also accepted.
Private-key safety contract (pinned by tests):
--out-privateis required — the CLI refuses to silentlydiscard freshly-generated key material.
0o600(owner-only)on POSIX.
dparameter is never echoed on stdout/stderr— only the file path and the
kid/algmetadata.--out-publicis round-trippedthrough
validateKeySetand contains no privated.The generated keypair round-trips end-to-end: a manifest signed
with the new private key verifies cleanly via
agentbridge verifyagainst the new public key set (covered by atest).
Tests added
packages/cli/src/tests/signature-cli.test.ts— 25 tests:validate --require-signatureon unsigned manifest exits 1.validate --require-signatureon signed manifest without--keysrefuses silent "verified" — exits 1.validate --keysEd25519 + ES256 happy paths viaspec/signing/test-vectors.json.validate --keysfailure modes: tampered →signature-invalid,unknown kid →
unknown-kid, expired (with--now+--clock-skew-seconds) →expired, missing key-set path,malformed key set.
verifyhappy path;verifyno positional → exit 2;verifyno
--keys→ exit 2;verifytampered →reason: signature-invalid.verify --jsonemits parseable JSON for both success andfailure with no prose on stdout.
verify --expected-issuermismatch →issuer-mismatch.verifymalformed key set → exit 1, "failed to load".bytes (success + failure paths).
keys generatehappy path: schema-valid public set, no privatedin public set.keys generatePOSIX mode 0o600 on private file.keys generateprivate bytes never appear on stdout/stderr;sensitivity warning printed.
keys generaterejects when--out-privateis omitted (exit 2).keys generaterejects non-canonical issuer.keys generateend-to-end: freshly-generated keypair signs +agentbridge verifysucceeds against the new public set.Live CLI command smoke (against built dist)
All five live invocations green.
Browser / computer-use status
Not needed. The CLI is a Node binary; no UI / browser surface
changed. The Studio dashboard and demo-app weren't touched. The
live CLI command smoke against the built
distis the relevantlive test for this surface and is included above.
Confirmations
validate:examples, and by the existingcli.test.ts(17 tests) continuing to pass without modification.pack:dry-runconfirms all six@marmarlabs/agentbridge-*still at0.4.0.crypto+ the existing@marmarlabs/agentbridge-coreimport.package-lock.jsonuntouched.packages/scanner/*,examples/scanner-signature-reporting/*,examples/README.md,apps/mcp-server/*,docs/releases/*, the root README, orCHANGELOG.md.verify(no public-key bytes echoed) andkeys generate(no privatedechoed).Commands run
Test plan
0.4.0.🤖 Generated with Claude Code