Skip to content

feat: agentgate v0.2.0 — CLI, version check, tests, pathPrefix - #4

Merged
owen0x6f merged 1 commit into
mainfrom
feat/v0.2.0-hardening
Aug 5, 2026
Merged

feat: agentgate v0.2.0 — CLI, version check, tests, pathPrefix#4
owen0x6f merged 1 commit into
mainfrom
feat/v0.2.0-hardening

Conversation

@boot-coco

Copy link
Copy Markdown
Contributor

Summary

Hardening agentgate for customer delivery. Single PR, target v0.2.0.

1. Versioning + upgrade check

  • VERSION exported from src/index.js, read from package.json
  • agentgate --version / agentgate version
  • agentgate upgrade --check [--json] queries GitHub Releases
  • Exit codes: 0 (up to date), 10 (upgrade available), 20 (check failed)
  • Graceful degradation: network errors, rate limits, no releases → exit 20 with message, never crash

2. CLI (bin)

  • agentgate keygen --kid <id> [--out <path>]
  • agentgate register --kid <id> --tenant <t> --agent <a> --cp-url <url>
  • agentgate keys list / keys revoke
  • Security: private key never printed/logged/uploaded; ops token from AGENTGATE_OPS_TOKEN env var only (never CLI args); keygen writes 0600

3. keygen + register tests

  • test/keygen.test.js: key type, base64 export round-trip, fingerprint, save/load, 0600 permission enforcement, seed determinism, path sanitization
  • test/register.test.js: URL construction, body shape (exactly kid/alg/pubkey_b64), alg = "Ed25519" (capital E), error mapping (only 500 retryable), request_id pass-through, revoke/list
  • All tests use injected _fetch, no live network

4. Signing path prefix (pathPrefix)

  • signRequest, AgentSigner, createSigner accept pathPrefix
  • When set, signature covers the internal path after prefix stripping (e.g. /ajj/agent/api/v1/query → signs /api/v1/query)
  • Test proves: signed path ≠ requested path; prefix stripping; fallback to /; non-matching prefix is no-op
  • Eliminates silent 401 errors every customer agent would hit

Stats

  • 135 tests total, 0 failures
  • 13 files changed, +861 / -36

Spec

https://mike.coco.site/pages/s/49a2efe0fbbb8eddee6aa0039069c60d (password: 100735)

Test plan

  • npm test — 135 tests green
  • agentgate --version0.2.0
  • agentgate upgrade --check --json → exit 20 (no releases yet), correct JSON
  • agentgate keygen --kid test → generates key, 0600 perms
  • pathPrefix test: signed path ≠ requested path proven
  • Live registration smoke test (Mike's step at review)
  • Release cut (Mike after merge)

🤖 Generated with Claude Code

Four hardening items for customer delivery:

1. VERSION export + CLI --version + upgrade --check against GitHub
   Releases (exit codes 0/10/20, graceful degradation)
2. CLI bin (agentgate): keygen, register, keys list, keys revoke
   with security rules (ops token from env only, private key never
   printed, 0600 perms)
3. keygen + register unit tests (node:test, mock fetch, no live
   network) — 52 new tests
4. pathPrefix on signRequest/AgentSigner/createSigner: signature
   covers the internal path after proxy prefix stripping, eliminating
   silent 401 errors

135 tests total, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@owen0x6f owen0x6f left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at d712b4d. Approving — all five items delivered and correct, 8/8 checks green, golden-vector integrity preserved (byte-compat with the Go side intact).

Verified:

  • pathPrefix: strips the proxy-stripped prefix and signs the internal path; || '/' fallback when path == prefix; no-op when prefix doesn't match. This is the fix for the silent-401 trap. Good.
  • upgrade --check: GitHub Releases source, graceful degradation for rate-limit/404/timeout/ENOTFOUND, exit codes 0/10/20 mapped in the CLI; --json still sets the exit code.
  • CLI security: ops token read only from AGENTGATE_OPS_TOKEN (no token flag exists), private key never printed (only kid/pubkey/fingerprint/path), 0600 via savePrivateKey.
  • Tests: keygen + register + pathPrefix, fetch injected, no live network.

Two minor, non-blocking hardening notes (fine to defer to a later tidy, not gating this merge):

  1. signRequest prefix match uses pathname.startsWith(pathPrefix). A theoretical path like /ajj/agentX would strip to X (no leading slash). Not a real route, and worst case is a 401, not a security hole — but pathname === pathPrefix || pathname.startsWith(pathPrefix + '/') is more robust.
  2. semverCompare doesn't handle pre-release tags (0.2.0-rc1 → NaN segment compares equal). We control our own clean release tags, so it won't bite; noting for completeness.

Merging + cutting v0.2.0 so upgrade --check has a source of truth.

@owen0x6f
owen0x6f merged commit 6679617 into main Aug 5, 2026
8 checks passed
@owen0x6f
owen0x6f deleted the feat/v0.2.0-hardening branch August 5, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants