All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Moved top-level write permissions to job level in
.github/workflows/codeql.yml(security-events: write,actions: read) and.github/workflows/release.yml(packages: write,id-token: write). Top level keepscontents: readonly. OSSF Scorecard Token-Permissions check expected to lift from 0 → 10. - Pinned Docker base images by SHA digest in
Dockerfile:golang:1.24-alpine@sha256:8bee1901…1b7191andalpine:3.21@sha256:48b0309c…b7abc07d. Tag names preserved on preceding comment lines (DockerfileFROMsyntax rejects trailing inline comments afterAS stage). Rotated weekly by Dependabot's existing docker ecosystem. - Pinned
govulncheckinstall in.github/workflows/ci.ymlby commit SHAd1f3801(v1.1.4) instead of@latest. - Replaced the Python
cryptographyinvocations intests/sec-l2b/integration.shANDscripts/smoke/core-contract.shwith a pure Go helper attests/sec-l2b/edsign/main.go(Go stdlibcrypto/ed25519, ~50 lines). Removed thepip install --user cryptographystep from CI; addedactions/setup-gotosmoke-l25since the job now builds the helper. The repo is now Python-free across.github/,tests/sec-l2b/, andscripts/smoke/. Acceptance verified locally against live broker:core-contract.sh10/10 PASS,integration.sh9 PASS / 1 SKIP (HSTS-TLS) / 0 FAIL. - Net: OSSF Scorecard Pinned-Dependencies expected to lift from 8 → 10. Overall score expected to lift from 6.2 to ~7.3.
- Replaced stale current-surface
AgentAuth/agentauthreferences withAgentWrit/agentwritin CLI text, broker startup output, Go comments, Python SDK examples, config headers, contribution-policy text, and SEC-L2b setup files. - Aligned TLS and mTLS compose overlays with
scripts/gen_test_certs.shby changing cert mounts from/tmp/agentauth-certsto/tmp/agentwrit-certs. - Refreshed SEC-L2b S2/S3 evidence so recorded current runtime output uses
spiffe://agentwrit.local/...andurn:agentwrit:error:unauthorized. - Bumped the pinned Go toolchain from
go1.25.9togo1.25.10sogovulncheckno longer reports fixed standard-library vulnerabilities in CI. - Preserved older changelog history and the CI/gates watchdog regexes that intentionally mention legacy
agentauthstrings.
- Comments and docs in 9 places claimed delegation enforces strict narrowing ("strict subset", "only narrow", "narrower-scoped"). The actual
authz.ScopeIsSubsetis a non-strict containment check — equal scopes pass, and same-scope delegation is a deliberate pattern (e.g., fan-out to workers carrying the parent's full authority, verified by SDK acceptance Story 8). Wording corrected acrossinternal/deleg/deleg_svc.go,internal/authz/scope.go,README.md,docs/security-topology.md,docs/architecture.md,docs/roles.md,docs/common-tasks.md,docs/integration-patterns.md, and thedocs/diagrams/security-topology.svgcallout label. Two source-file docstrings now carry a back-reference to issue #41 explaining why this is not a strict-subset check. Closes #41.
- New
docs/faq.md— real questions from practitioners evaluating AgentWrit. Covers identity vs credential exchange, OIDC (enterprise), scope model, scope drift detection (roadmap), SDK status, restart/HA behavior, demo status, and licensing. - FAQ added to README documentation table.
- Token lifecycle and security topology diagrams now have dedicated markdown pages (
docs/token-lifecycle.md,docs/security-topology.md) with context tables and navigation. Architecture doc links updated. - README: fixed "Key works for everything, forever" — IAM keys don't work like that. Changed to "Key is over-permissioned and long-lived."
- README hero image added — centered logo at
docs/diagrams/agentwrit-logo.png.
- Pattern component table now maps all 8 EAC v1.3 components with correct numbering. Component 6 (Mutual Authentication) documented as present but not wired. Component 7 (Delegation) and 8 (Observability) added.
- Package count corrected (14→15) and
mutauthadded to directory layout. Clarified distinction:mutauthpackage is agent-to-agent auth (Component 6, planned); server-side mTLS transport (AA_TLS_MODE=mtlsincmd/broker/serve.go) is separate and working. - Security Assumptions: removed false claim that "all previously issued tokens become unverifiable after restart (new signing keys)." The signing key is persistent via
internal/keystore— tokens survive restarts. Single-broker note corrected to describe the actual split-brain risk (in-memory state, not signing keys).
- SPDX identifier corrected from
PolyForm-Internal-Use-1.0.0toLicenseRef-PolyForm-Internal-Use-1.0.0across LICENSE and all 77 Go files. PolyForm Internal Use is not on the SPDX License List —LicenseRef-prefix is required by spec for unlisted licenses. - CI SPDX gate now checks the exact expected identifier, not just presence of
SPDX-License-Identifier:. - Middleware stack diagram in
docs/architecture.mdcorrected to match actual wrapping order incmd/broker/main.go:RequestID → Logging → MaxBytesBody → SecurityHeaders → mux.
- Corrected SQLite version in External Dependencies table (v1.35.0 → v1.46.1).
- Added missing
cobraCLI dependency to table. - Removed Prometheus branding from
obspackage descriptions — implementation detail, not a feature. - Fixed "Open Source" label in token-lifecycle SVG → "Free for internal use".
- Every
.gofile incmd/andinternal/(77 files) now carries// SPDX-License-Identifier: LicenseRef-PolyForm-Internal-Use-1.0.0as the first line. - CI
contaminationgate extended with an SPDX header check — new Go files without the header will fail the gate.
LICENSE— replaced AGPL-3.0 text with PolyForm Internal Use License 1.0.0 (source-available, permanent, no sunset). SPDX identifier:LicenseRef-PolyForm-Internal-Use-1.0.0. Dual-license header added for commercial-use contact path.README.md— license badge updated; License section rewritten with the free / paid dual-license split and thelicensing@agentwrit.comcontact line.Dockerfile—org.opencontainers.image.licensesOCI label updated fromAGPL-3.0-onlytoLicenseRef-PolyForm-Internal-Use-1.0.0.docs/api/openapi.yaml— OpenAPIinfo.licenseblock updated to reference PolyForm Internal Use 1.0.0.docs/getting-started-operator.md— OCI labels reference updated to match the new Dockerfile label.- What this means for users:
- Internal business use — including by for-profit companies — remains free and unchanged. Any individual, business, or organization may use and modify AgentWrit for their own internal operations at no cost and without contacting the author. Contractors acting on behalf of a permitted user are covered for the duration of their engagement.
- New restriction: Offering AgentWrit as a hosted or managed service to third parties (paid or free), redistributing original or modified versions, reselling, or embedding AgentWrit in a product you sell now requires a commercial license. Email
licensing@agentwrit.comwith your use case. - SemVer note: this is a license change, not a behavior change. No wire-format changes. No API changes. Deployed brokers continue to run unchanged. Anyone pinning the prior AGPL-3.0 versions (
v2.0.0and earlier) should be aware subsequent releases ship under PolyForm Internal Use 1.0.0.
docs/diagrams/— 3 SVG architecture diagrams (architecture overview, token lifecycle, security topology) replacing the inline mermaid block. Built from code review — only components that exist in the codebase. No HITL, no resource server, no monitoring boxes.docs/python-sdk.md— splash page for the Python SDK (private repo). Shows status, code sample, and links to raw HTTP alternative.docs/demos.md— splash page for MedAssist AI and Support Ticket demos (ship with Python SDK).README.md— added Ephemeral Agent Credentialing v1.3 pattern lineage in "How it works". All private-repo links now point to splash pages instead of 404s.docs/— 5 doc files updated: privateagentwrit-pythonlinks replaced with splash page links.
README.md— full restructure for newcomers who scan, not read:- Quick Start collapsed to one path (Docker Hub), 5 commands, no inline explanations. "Next steps" decision table immediately after.
- "Why this matters" side-by-side comparison table replaces prose bullet list.
- "How it works" simplified mermaid flow with numbered steps. Component table links to source packages.
- API table adds "Who uses it" column so each role finds their endpoints fast.
- Documentation section reframed as "I want to..." intent-based navigation.
- Configuration collapsed to one essential table + link to operator guide.
- TLS/mTLS/nginx config, cosign verification, image tag details moved to docs (not README).
- Audit trail wording clarified: broker logs credential lifecycle events, not agent activity at the resource server.
- Net: 169 insertions, 469 deletions — README is now a landing page, not a manual.
SECURITY.md— contact emailssecurity@agentauth.dev→security@agentwrit.com; all "AgentAuth" brand references → "AgentWrit".CODE_OF_CONDUCT.md— contact emailconduct@agentauth.dev→conduct@agentwrit.com.docs/scenarios.md,docs/agentwrit-explained.md,docs/README.md,docs/getting-started-developer.md,docs/getting-started-user.md— 11 Python SDK links still pointing to pre-renameagentauth-python→agentwrit-python.
README.md— significant rewrite of the top section for people who are not already familiar with the product:- Added a GitHub
[!IMPORTANT]build-in-public status banner right under the badges: broker is stable, SDK and demo still landing, pin tov<semver>ormain-<sha>for anything non-lab, issues welcome / external PRs paused. - Replaced the jargon-heavy hero paragraph with a plain-English "What is AgentWrit?" section that leads with the writ metaphor — narrow authority, time-limited, revocable at the source.
- Added a new "The problem AgentWrit solves" section that frames the pain (long-lived API keys + prompt-injected agents = full blast radius) before introducing the solution.
- Rewrote the Quick Start as a 5-minute zero-to-first-agent-token walkthrough: Docker Hub pull → admin auth → launch token → Python SDK registration. Every step explains what the command does and why you're running it.
- Fixed 5 remaining prose references to
AgentAuthmissed in the earlier brand sweep. - Updated Python SDK links from
github.com/devonartis/agentauth-python→github.com/devonartis/agentwrit-pythonafter the sister repo rename.
- Added a GitHub
CONTRIBUTING.md— restructured to match Decision 014 (build-in-public, external PRs paused):- New top section "Contribution Policy (READ FIRST)" with a table of what's accepted (issues, security reports, feature requests) and what's not (external PRs of any kind). Explains why PRs are paused and lists 5-item exit criteria for reopening.
- Deleted the "Pull Request Process" section and PR checklist (misleading given the current policy).
- New "Filing a good Issue" section with concrete templates for bug reports and feature requests.
- New "If you're reading this to understand the code" section — preserves dev setup / code style / testing sections as valuable for readers who want to trace behavior without submitting code.
- Fixed
agentauth/project tree root →agentwrit/and remaining prose brand references.
.github/workflows/release.yml— new workflow that publishes multi-arch (linux/amd64+linux/arm64) broker images to Docker Hub on every push tomainand onv*release tags. Tags produced:latest(tracks main),main-<sha>(per-commit traceability),v<major>.<minor>.<patch>/v<major>.<minor>/v<major>(semver releases). Builds with buildx + QEMU emulation on the hosted x86 runner; cached viatype=ghaso repeat builds are fast.- Cosign keyless signing — every published image is signed with Sigstore via GitHub Actions OIDC. No long-lived signing key to rotate. Verification command published in the README and
docs/getting-started-operator.md. - SLSA provenance + SBOM attestation —
docker/build-push-actionemits provenance (mode=max) and SBOM alongside the image. Dockerfile— added OCI image labels (org.opencontainers.image.title/description/vendor/licenses/source/url/documentation) as bake-time defaults, plus-trimpathon the Go build for reproducibility. The release workflow overridesrevision,version, andcreatedlabels at publish time viadocker/metadata-action.README.md— new "Option A: Pre-built image from Docker Hub" section in Quick Start, now the recommended onboarding path ahead of source builds. Includes signature verification command.docs/getting-started-operator.md— restructured "Quick Start" into three deployment options (pre-built image / Docker Compose local build / native binary). Pre-built image is the new default with full pull/run/verify walkthrough and OCI-image inventory.docker-compose.yml— added commented-outimage: devonartis/agentwrit:latestas an alternative tobuild: .so operators can swap without editing the compose file structurally. Also fixed staleAA_DB_PATHdefault (agentauth.db→data.db) missed in the earlier brand sweep.TECH-DEBT.md— TD-CI-002 marked RESOLVED with link to this branch.
Required manual setup before first release:
- Docker Hub repo
devonartis/agentwritmust exist (public) - Docker Hub PAT with read/write scope to
agentwritonly - GitHub repo secrets:
DOCKERHUB_USERNAME+DOCKERHUB_TOKEN
.github/workflows/ci.yml— newmain-hygienejob greps for strip-target paths on pushes tomain. Runs only whengithub.ref == 'refs/heads/main'so PR runs againstdevelopare untouched (develop legitimately carries these files). Fails thegates-passedaggregator if any dev-only file slips ontomainvia a missedstrip_for_main.shinvocation — an automated safety net on top of the existing manual strip script.scripts/gates.sh— mirror of the new gate inGATES_FULLsoscripts/test-gate-parity.shpasses. Local invocation skips withskip_gateunless the current branch ismain, since a developer runninggates.sh fullondevelopwould otherwise get a spurious failure.TECH-DEBT.md— TD-CI-003 (automated develop→main PR workflow) added as follow-on work.
internal/problemdetails/problemdetails.go— RFC 7807 problemtypeURNs now useurn:agentwrit:error:{errType}instead of the formerurn:agentauth:error:{errType}namespace, matching the published API docs.internal/obs/obs.go— Prometheus metric names now use theagentwrit_namespace. Updated the metrics test and troubleshooting/common-tasks docs so runtime output and docs agree.cmd/awrit/init_cmd.go—awrit initnow writes to the same default paths the broker auto-loads (~/.broker/config, fallback/etc/broker/config). Added a unit test for the default user config path.docs/common-tasks.md— JWTissclaim text now matches the actualAA_ISSUERbehavior, and the alert table no longer lists non-existent broker/app-auth/expired-token metrics.
docs/getting-started-user.md— admin auth examples used the literal"my-secret-key-change-in-production"while the guide starts the broker with a randomly generated$AA_ADMIN_SECRET. Examples now reference$AA_ADMIN_SECRETso the first-run path works without 401s.docs/awrit-reference.md—awrit initsample output showed~/.agentwrit/config; corrected to~/.broker/config, matching the broker's read path and the CLI default after TD-CLI-002.docs/api.md— JWT claims table corrected:issis driven byAA_ISSUER(empty by default, issuer validation skipped); app token subject isapp:{internal_app_id}notapp:{client_id};audis driven byAA_AUDIENCE(omitted if unset, audience validation skipped).docs/getting-started-operator.md—AA_AUDIENCEdefault corrected from"agentwrit"to (empty). SQLite persistence note corrected: settingAA_DB_PATH=""does not enable memory-only mode — unset uses the./data.dbdefault.docs/api/openapi.yaml—info.licensecorrected fromApache-2.0toAGPL-3.0with the correct license URL. MatchesLICENSE,README.md, andCLA.md.docker-compose.yml— Docker bridge network renamedagentauth-net→agentwrit-netto match brand sweep and operator docs.docs/README.md— API reference entry corrected from "22 HTTP endpoints" to "19". Concepts entry corrected from "seven components" to "eight".docs/concepts.md— intro sentence corrected from "seven components" to "eight".TECH-DEBT.md— added TD-CLI-002 (HIGH:awrit initwrites to~/.agentauth/config, broker reads~/.broker/config— broken first-run path introduced in commit4e197a5) and TD-CLI-003 (Low: docker-compose.yml network name lag). Bug report at.plans/bugs/BUG-CLI-002-awrit-init-config-path.md; TD-CLI-002 is resolved by the runtime rebrand alignment fix above.
cmd/aactl/→cmd/awrit/— directory renamed. Cobra command name changed (Use: "aactl"→Use: "awrit"). All internal CLI output, help text, and error messages updated.docs/aactl-reference.md→docs/awrit-reference.md— reference doc renamed. All example commands in the doc rewritten to useawrit.- Docs, scripts, tests, README, CONTRIBUTING, docker-compose.yml, .github/workflows/ci.yml, .gitignore — every
aactlreference in ship-to-main files rewritten toawrit. Evidence files undertests/*/evidence/*.mdintentionally preserved as-is because they are historical records of past test runs (rewriting history would misrepresent what happened at the time). cmd/broker/main.go— error message"Run 'aactl init'..."→"Run 'awrit init'..."..gitignore— both/awritand/aactllisted so accidentally-built binaries under either name stay untracked during the transition.internal/cfg/configfile.go— user-visible references in the env var comment block updated toawrit.
Scope: ~36 files touched plus directory + file renames. No production logic changes — pure mechanical rename. The github.com/devonartis/agentauth Go module path is NOT changed (that's gated on the GitHub repo rename, separate work).
internal/admin/admin_svc.go— deleted the magic-number constadminTTL = 300. Admin JWT TTL is now driven bycfg.AdminTokenTTL(seconds), wired through a newtokenTTLparameter onNewAdminSvc. Operators tune viaAA_ADMIN_TOKEN_TTL(default 300 / 5 min).internal/cfg/cfg.go— addedAdminTokenTTL intfield and a named constdefaultAdminTokenTTL = 300(seconds; matches existing int-seconds convention for DefaultTTL, MaxTTL, AppTokenTTL so the cfg package stays internally consistent). Env varAA_ADMIN_TOKEN_TTLadded to the inline doc comment.cmd/broker/main.go—NewAdminSvcwiring updated to passc.AdminTokenTTL.- Tests —
newTestAdminSvchelpers and directNewAdminSvccalls acrossadmin_svc_test.go,admin_hdl_test.go,app_hdl_test.go,handler/handler_test.gonow pass an explicittestAdminTokenTTL = 300fixture. Assertions that checkedresp.ExpiresIn != adminTTLnow check against the fixture value — the test drives cfg-to-claim TTL flow end-to-end inside the admin package, which is the unit-level equivalent of a config-matrix behavioral test for this field. - Rationale for int seconds (not
time.Duration) — the existing TTL fields (DefaultTTL,MaxTTL,AppTokenTTL) all use int seconds. Adding onetime.Durationfield would create two conventions in the same cfg package and leak into every caller that passes the field through. A future cleanup can migrate all TTL fields totime.Durationtogether (proposed TD-CFG-003) — but mixing conventions in this PR would be worse than preserving the existing one.
Removed hardcoded identity literals from cfg + token packages (TD-TOKEN-001, TD-TOKEN-002, TD-CFG-001, TD-CFG-002)
internal/token/tkn_svc.go— JWTissclaim is now driven bycfg.Issuerinstead of the hardcoded literal"agentauth". Issuer enforcement moved fromTknClaims.Validate()(pure structural check) intoTknSvc.Verify()where config is available. Emptycfg.Issuerskips the issuer check (mirrors the Audience contract — operator opt-in).internal/cfg/cfg.go— addedIssuer stringfield, env varAA_ISSUER. No default; empty value means "skip issuer enforcement at verify time," matching the documented Audience pattern.internal/cfg/cfg.go—TrustDomaindefault literal"agentauth.local"→"agentwrit.local"(no longer leaks the prior brand into source).internal/cfg/cfg.go—DBPathdefault literal"./agentauth.db"→"./data.db"(neutral, no brand in source).internal/cfg/cfg.go—Audiencedefault override at line 96 deleted. Thecfg.go:22doc comment said"empty = skip"but the code overrode unset →"agentauth". NowAudiencehonors its documented contract: unset OR explicitly empty both skip audience validation. No brand-coupled default.internal/cfg/configfile.go— config search paths/etc/agentauth/config→/etc/broker/configand~/.agentauth/config→~/.broker/config. Filesystem layout no longer encodes the brand. Header comment in generated config files updated from# AgentAuth Configuration→# Broker Configuration.internal/token/tkn_claims.go— package doc comment updated to reflect thatissis operator-configured viacfg.Issuer, not "always 'agentauth'".Validate()is now a pure structural check (sub, jti, exp, nbf) — issuer enforcement is the service layer's job.- Test surface — test fixtures across
cfg/,token/,authz/,deleg/,admin/,identity/,mutauth/updated to use brand-neutral test values (test-issuer,test.local,spiffe://test.local/...) instead of leaked"agentauth"andagentauth.localliterals. Tests now drive issuer/audience expectations from fixture cfg, not hardcoded constants. - Root cause:
IssuerURLwas an OIDC-coupled config field stripped during the open-core split. The strip removed the field, the validation, AND the tests (tombstone preserved atinternal/token/tkn_svc_test.go:521), but the validation was replaced with a hardcoded literal"agentauth"rather than left as configurable. The general JWTissclaim is independent of OIDC and core still needs it — this PR restores configurability without re-coupling to OIDC. Full audit at.plans/reviews/2026-04-10-hardcoded-identity-audit.md. - Standing rule added:
~/.claude/CLAUDE.mdnow contains "No Hardcoded Identity Values — Universal, Non-Negotiable" as a global rule. Identity-shaped string literals in source code (brand names, issuers, trust domains, search paths) are non-negotiable findings going forward.
README.md— added three CI-health badges ahead of the existing language/license/tech row:- CI —
ci.ymlworkflow status onmain - CodeQL —
codeql.ymlSAST status onmain - OpenSSF Scorecard — supply-chain posture score Badges will show as "not found" or broken while the repo is private (CI badge requires viewer auth; CodeQL and Scorecard require public repo access). They're added now so the moment the repo flips public they light up without a README update — fire-and-forget. CodeQL and Scorecard will ALSO need their workflow triggers re-enabled per TD-VUL-006 fix sequence. A comment in the README notes this.
- CI —
.vscode/settings.json— was tracked on develop but carries per-user editor settings (e.g. Snyk IDE prefs). Untracked viagit rmand added to.gitignoreso it stays out of every branch. This closes the loop on the leak that happened during the first develop → main strip merge attempt: VSCode recreated the file betweenrm -rfandgit commit, so it landed in the merge commit. The commit was amended to remove it (seea72a959), but the root cause was that the file was tracked on develop at all. Now both the strip script and .gitignore cooperate to keep it out.
scripts/strip_for_main.sh— the documentedgit merge develop --no-commit→ strip flow could never actually work because the script's dirty-tree guard refused to run mid-merge. Added merge-state detection ($GIT_DIR/MERGE_HEADpresence); when mid-merge the strip usesgit rm -rf --ignore-unmatchso modify/delete conflicts get deleted AND staged as resolved in one step. The "absolute refusal to run on develop" guard is preserved regardless of merge state.scripts/strip_for_main.sh+.githooks/pre-commit— added.vscode/(editor settings, often carry per-user Snyk / IDE prefs) to both strip lists, andadr/to the pre-commit FORBIDDEN list (it was already in the strip script). The two defense layers now agree. A note in pre-commit tells future editors to keep both lists in sync.
.gosec.yml— explicit gosec configuration with documented rule exclusions (G117, G304, G101) rationalized for a credential broker's API surface. Every excluded rule carries a reviewer-auditable rationale..golangci.yml— security-awaregolangci-lintconfig (errcheck, gosec, govet, ineffassign, staticcheck, unused, gosimple, bodyclose, misspell, gofmt, goimports) with tuned govet subchecks (fieldalignment and shadow disabled with rationale) and mirrored gosec excludes.scripts/smoke/core-contract.sh— L2.5 core contract smoke test. 10-step verification (health, admin auth, launch token, challenge, Ed25519 challenge-response register, JWT structure, validate-accepted, revoke, validate-rejected, out-of-scope denied) against a running broker. Usespython3 + cryptographyfor the Ed25519 signing step.scripts/test-gate-parity.sh— enforces gate list alignment betweenscripts/gates.sh --list-gatesand.github/workflows/ci.ymlGATE_LIST_START/ENDblock. Prevents silent drift.syft scanbaseline — SBOM generation integrated into the localgates.sh fullpipeline (SPDX-2.3, 27 packages at baseline).
scripts/gates.sh— extended from 4 gates to 13. New blocking gates:contamination(enterprise refs grep),govulncheck(stdlib and dependency vulnerabilities),go-mod-verify(module integrity + tidy drift),vet,format, plusfull-mode-only:unit-tests-race,docker-build,smoke-l2.5,sbom.gosecflipped from warn-only to blocking.modulerenamed tofull(deprecated alias retained). Dead references tolive_test.sh/live_test_docker.shremoved.golangci-lintandgosecare now required (no fallback). Added--list-gatesfor parity enforcement. HonorsBROKER_URLfor smoke-l2.5 on non-default ports.TECH-DEBT.md— recorded TD-VUL-001..004 (four Go stdlib CVEs fixed by bumpinggo.modtoolchain fromgo1.25.7togo1.25.9, scheduled for landing at the first CI push).
- gofmt drift — 24 pre-existing gofmt-dirty files normalized in a
single style commit. No behavior change. Surfaced by adding
formatas a blocking gate. internal/keystore/parseKey— defensive type-assertion onpriv.Public().(ed25519.PublicKey)to satisfyerrcheck check-type-assertions. Unreachable on the happy path.internal/mutauth/heartbeat.sweep— heartbeat auto-revoke failures are now logged viaobs.Warninstead of being silently dropped. Previously_, _ = h.revSvc.Revoke(...)was followed by an unconditional "agent auto-revoked" log line, even when the revocation actually failed.cmd/aactl/client—json.Marshalandio.ReadAllerrors are now propagated as wrapped errors instead of being discarded. Affectsauthenticate()(two sites) anddoPostWithToken().internal/store/sql_store.QueryAuditEvents— documented#nosec G202on the audit query SELECT, explaining why the fragment concatenation is safe (fixed template, parameterized values).internal/admin/admin_svc_test.TestLaunchTokenRecord_SpecCompliance— clarified the exhaustive-literal intent in a doc comment and silencedgovet unusedwritewith_ = rec.
Security hardening
- Token TTL enforcement —
AA_MAX_TTLconfiguration sets the maximum token lifetime ceiling (default 86400s, set to 0 to disable). The broker clamps any requested TTL to this ceiling. - TTL carry-forward on renewal — Renewed tokens preserve the original token's TTL instead of falling back to the default. Closes a privilege escalation path where a short-lived token could be renewed to the broker default.
- JWT algorithm validation — The broker rejects tokens with
alg != EdDSA, preventing thealg:noneand HS256/RS256 algorithm confusion attacks. - JWT key ID validation — The broker rejects tokens with a mismatched
kid, preventing cross-broker token replay. - Revocation check in Verify() — Every token verification path checks the revocation list. Defense in depth.
- Transactional renewal — Predecessor token is revoked before the new token is issued. If revocation fails, renewal fails.
- Startup warning when DefaultTTL > MaxTTL — Surfaces silent clamping at startup.
- Token expiry required — Tokens with
exp=0or missingexpare rejected.
HTTP hardening
- SecurityHeaders middleware — All responses carry
X-Content-Type-Options: nosniff,Cache-Control: no-store,X-Frame-Options: DENY. HSTS added on TLS/mTLS deployments. - Request body size limit — Global 1MB limit on all endpoints, enforced by eager buffering so streaming decoders can't bypass it. Returns 413 on oversized bodies.
- Error sanitization — Token validation, renewal, and auth middleware errors return generic messages to the client. Full errors are recorded in the audit trail with a correlation
request_id. - Bind address safety — Broker defaults to
127.0.0.1; warns at startup when binding to0.0.0.0without TLS. - HTTP server timeouts — Read, write, and idle timeouts prevent slowloris-style attacks.
- TLS 1.2 minimum + AEAD-only ciphers — Enforced when TLS is enabled.
- Weak secret denylist — The broker refuses to start with a known-weak admin secret (empty,
change-me-in-production, etc.). Useaactl initor generate a strong value.
Operator tooling
aactl initcommand — Generates a secure admin secret and config file in dev or prod mode. Atomic file creation withO_EXCL, rejects symlinks, enforces 0600 file / 0700 directory permissions.- Config file support — KEY=VALUE format at
AA_CONFIG_PATH>/etc/agentauth/config>~/.agentauth/config. Rejects insecure permissions like SSH/GPG does. - Bcrypt admin authentication — Admin secret stored as a bcrypt hash; plaintext only shown once at init. Dev mode supports plaintext config for convenience, bcrypt is derived at startup.
gates.shdeveloper tool — Build + lint + unit tests + gosec in one command (./scripts/gates.sh task). Module mode adds full tests and Docker E2E. Regression mode runs all phase regression suites.
App credential lifecycle
POST /v1/app/launch-tokens— App-facing endpoint for creating launch tokens within the app's scope ceiling. Scope ceiling enforcement prevents apps from escalating beyond what the operator granted at registration.POST /v1/admin/launch-tokens— Admin-facing endpoint for bootstrapping and break-glass scenarios. No ceiling enforcement (admin is the root of trust).- App scope ceiling — Operators set a scope ceiling when registering an app; the broker enforces it on every
POST /v1/app/launch-tokenscall. - App traceability —
app_id,app_name, andoriginal_principalclaims flow through launch tokens into agent JWTs, preserved through delegation.
Production foundations
- Persistent signing key — Ed25519 signing key loaded from disk at startup (
AA_SIGNING_KEY_PATH), generated with 0600 permissions on first start. Agent tokens survive broker restart. - Graceful shutdown — SIGINT/SIGTERM triggers clean shutdown: HTTP server drains, SQLite closed.
- Corrupt key fails fast — Broker refuses to start with a malformed signing key, surfacing the problem at deploy time.
- Token predecessor revocation on renewal — Prevents two valid tokens existing for the same agent.
- JTI blocklist pruning — Background goroutine removes expired revocation entries so memory doesn't grow unbounded.
- Agent record expiry — Agent records marked expired when their token TTL elapses.
Audit and observability
- Structured audit fields — Audit events carry
resource,outcome,deleg_depth,deleg_chain_hash, andbytes_transferredvia a backward-compatible options pattern. Hash chain tamper evidence covers all structured fields. - Outcome filtering — Query the audit trail by outcome via
--outcomeonaactl audit eventsor?outcome=onGET /v1/audit/events. - Enforcement event coverage — Audit events emitted for every denial path: missing auth, invalid scheme, token verification failure, revoked token access, scope violations, delegation attenuation violations, scope ceiling exceeded.
- New Prometheus metrics —
agentauth_audit_events_total,agentauth_audit_write_duration_seconds,agentauth_db_errors_total,agentauth_audit_events_loaded,agentauth_admin_auth_total.
Persistence
- SQLite audit persistence — Audit events persist to SQLite via
modernc.org/sqlite(pure Go, no CGo). Hash chain is rebuilt from disk on startup. Configurable viaAA_DB_PATH(default./agentauth.db). - Revocation persistence — Revocations stored in SQLite so tokens stay revoked across broker restarts.
- Documentation accuracy — Corrected public documentation to match the current broker contract for agent registration, renewal, release, app authentication, launch token creation, and health responses. Fixed copy/paste examples with stale payload shapes and outdated event names.
- Direct HTTP integration — Go developers get explicit pre-SDK guidance with end-to-end examples for registration, renewal, and release in
docs/getting-started-developer.md. - Authorization middleware —
WithRequiredScope()standalone function replaced byValMw.RequireScope()method. Scope checking now emitsscope_violationaudit events on denial.
Complete rewrite implementing the Ephemeral Agent Credentialing security pattern.
Identity and authentication
- Challenge-response agent registration with Ed25519 cryptographic verification
- SPIFFE-format agent IDs (
spiffe://{domain}/agent/{orch}/{task}/{instance}) - EdDSA-signed JWT tokens with configurable TTL (default 5 minutes)
- Token verification endpoint returning decoded claims
- Token renewal with fresh timestamps and new JTI
Authorization
ValMwmiddleware enforcing Bearer token + scope on every request- Scope format
action:resource:identifierwith wildcard support
Revocation
- 4-level token revocation (token/JTI, agent/SPIFFE ID, task, delegation chain)
Audit
- Hash-chain tamper-evident audit trail with SHA-256 linking
- Automatic PII sanitization (secrets, passwords, private keys, token values)
- 12 event types covering admin auth, registration, token lifecycle, delegation, resource access
- Query endpoint with filtering (agent, task, event type, time range) and pagination
Delegation
- Scope-attenuated token delegation with chain verification
- Maximum delegation depth of 5 hops
- Cryptographic delegation chain embedded in token claims
Admin
- Admin authentication via shared secret with constant-time comparison
- Launch token creation with policy (allowed scope, max TTL, single-use flag)
- Admin bootstrap flow for initial system setup
Observability
- Prometheus metrics (registrations, revocations, active agents)
- Structured logging via
obspackage - RFC 7807
application/problem+jsonerror responses on all endpoints - Health check endpoint reporting status, version, and uptime
- Prometheus exposition format at
/v1/metrics
Configuration
AA_*environment variable configuration with sensible defaults