Skip to content

feat: end-to-end ANS identity enforcement via a commerce (AP2) egress capability - #11

Open
aciobanu-godaddy wants to merge 14 commits into
mainfrom
feat/ans-identity-commerce-egress
Open

aciobanu-godaddy wants to merge 14 commits into
mainfrom
feat/ans-identity-commerce-egress

Conversation

@aciobanu-godaddy

Copy link
Copy Markdown
Collaborator

Related issue

Fixes #10

Summary

Adds a commerce (AP2) capability to the egress middleware as a concrete vehicle
for exercising and proving ANS identity enforcement end-to-end.

The egress fetches an authority-signed, scoped mandate, mints a DPoP proof, and
stamps a transparency-log-vouched ANS identity onto the outbound request, all
outside the (untrusted) sandbox so the workload cannot strip or forge it.
Commerce is implemented as a pluggable capability transform (discovered via
agent-card / MCP tools-list), not a hardcoded branch, so other capabilities can
reuse the same identity-stamping path.

A live two-agent demo (deploy/live-demo/) shows enforcement both ways: a
vouched buyer agent transacts with the live seller and gets a ticket; an
unvouched dev-mode rogue making the same call is refused (MANDATE_REJECTED).
It runs against the live seller and the GoDaddy production transparency log, is
driven from a single .env (no secrets or identity values in the repo), and has
a --narrate mode that displays the real decoded cert, mandate, and DPoP proof
for a reviewer to verify the chain themselves.

The companion seller-side DPoP clock-skew fix is tracked separately in
interop-agents-go.

Testing

  • go build ./... clean.
  • go test ./... -race -count=1 green.
  • golangci-lint run ./... reports 0 new issues.
  • make cover at 95.6% (above the 95% gate).
  • Live demo verified end-to-end from a fresh clone: plain run DEMO PASS, and
    --narrate --report shows the real decoded identity/mandate/proof and writes a
    markdown report. Confirmed the plain-run output is unchanged and that no
    private key material is printed in any mode.

AI assistance

Assisted-by: Claude Code (claude-opus-4-8)

Checklist

  • The PR title follows Conventional Commits — release notes are generated from it
  • Tests cover the change
  • The linked issue above uses a closing keyword
  • Every commit is signed off (git commit -s) certifying the DCO

Self-signing signer + self-signed artifact source selected by dev_mode config,
deduplicated COSE minting with selfsign coverage. The rogue is stamped at the
egress but rejected only at the ingress.

Squashes: a904935 5cbb964 d8d3e03 34214fc 16aa51a
Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
…pt toggle

Seed+refresh root keys from the transparency log, configurable SCITT receipt
enforcement, forward verified requests to the seller, reject oversize bodies
(400) before verification, and the ans-reference-callee flags that drive it.

Squashes: 416a3d8 5256e52 51f571d bb41bbf 38681b4 f4558a9
Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
P-256 commerce DPoP key (JWK + RFC 7638 jkt), byte-exact RFC 9449 DPoP minter,
mandate type with ans_host version-stripping + validation, single-slot mandate
cache with skew + injectable clock, authority request_mandate JSON-RPC client,
and the rewrite_book_flight orchestrator. The middleware rewrites the body THEN
signs the rewritten bytes (R1), behind a commerce gate/config with demo prefetch.

Squashes: 3dd638d c4cc4a8 c594177 8a5f72a 5df9704 7a913c0 7704ade cf02479 978635d 404c9fe 7f6df35
Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
Agent A (commerce) + Agent B (rogue) example configs, sandbox policies, and the
Tier 2 gateway registration; docs/ and secrets/ gitignored (local-only).

Squashes: c45a9b0 17b5eb8 7a95bc4 9ff2072 526db12
Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
…hint

Replace the hardcoded commerce branch with a capability.Transform chain driven
by a Discoverer (A2A agent-card JWS + MCP tools/list); commerce becomes one
transform with a lenient static fallback. Discovery hardened: tool-gated probing,
verified-htu only, hermetic tests. HttpResponsePreReturn stage surfaces a seller
capability rejection as a machine-readable middleware_hint.

Squashes: 29ffc2b 36cc56e 5ed69a7 c02c3f1 293c1ae 36a593b 1120c1c ff1e05b 9789157 4833761 546fc61 ea8f1d1 057afaf
Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
…eanup

Forward the authority's mandate bytes verbatim (avoid float-format divergence),
bind the identity-proof htu to the transform's external (seller) URL (G1), drop
deprecated elliptic.Marshal + unify kid computation, neutralize a doc-comment
example.

Squashes: 57d5338 b604a91 7428387 c42823e
Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
…mode

run-demo.sh drives the live two-agent OpenShell demo end-to-end from a single
.env (no values/secrets in the repo; renders configs at runtime). --narrate adds
a trust-boundary diagram and a step-by-step walkthrough with the REAL decoded
evidence — cert chain, transparency-log vouch, scoped mandate, DPoP proof claims,
ticket, and the rogue rejection; --report writes a markdown copy. The egress
surfaces the attached mandate/proof and logs their decoded form at DEBUG only.
README/RUNBOOK/SECRETS document setup and the no-secret-leak guarantees.

Squashes: 875c19c 5cb5284 6af259c 8bbdc70 6fb2df9 fc78aaf ba47018
Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
@aciobanu-godaddy
aciobanu-godaddy force-pushed the feat/ans-identity-commerce-egress branch from 1603f6e to 57dd64a Compare October 27, 2026 07:24
Under the CI toolchain's gosec, the discovery + authority client.Do calls
(operator/policy-scoped URLs, not request input) are flagged G704, while the
duplicate directive on callee.go's NewRequestWithContext line is unused (the
real sink client.Do is already suppressed). Add the three needed suppressions
and drop the redundant one.

Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
@aciobanu-godaddy
aciobanu-godaddy force-pushed the feat/ans-identity-commerce-egress branch from 6e05160 to 348d249 Compare October 27, 2026 07:35
Pull the generic middleware assembly (identity, caller auth, vetting, gRPC
server, readiness, serve loop) out of cmd/ans-openshell-middleware into a new
exported app package: app.Run(ctx, cfg, logger, ...Option). A WithCustomize
option receives the built *middleware.Service so a caller can register
capability transforms + a discoverer before serving. The exported location
(not internal/) lets a separate module (the forthcoming demo) reuse it.

The production main shrinks to flag parsing + config load + app.Run; commerce
is wired via WithCustomize for now (it moves to the demo module in a later
commit). Server-assembly tests move to app/app_test.go. No behavior change.

Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
Replace Outcome.MandateBytes/DPoPProof (commerce-named) with a generic
Artifacts map[string]any the core treats as opaque, plus an optional
Narratable interface a transform may implement to render its artifacts at
debug. service.go no longer imports internal/commerce: it just calls
Narratable if the transform implements it. The commerce adapter now stashes
its mandate/proof into Artifacts and implements Narrate (the decoding moved
out of the core). Extracted a narrate() helper to keep evaluate() under the
cognitive-complexity gate. No behavior change.

Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
Commerce (AP2 mandate + DPoP) is a demo vertical, not generic middleware, so
it moves out of the core module entirely into a new in-repo module
'tutorial' (github.com/agentnameservice/ans-openshell/tutorial):
  internal/commerce      -> tutorial/commerce
  internal/commercekey   -> tutorial/commercekey
  internal/middleware/commerce.go (the capability.Transform adapter)
                         -> tutorial/transform (package transform)

The production binary + core packages are now commerce-free (verified: no
internal/commerce references outside tutorial/). The prod main is pure
generic middleware over app.Run. Two service-level commerce E2E tests were
dropped from internal/middleware (their coverage is retained by
tutorial/transform tests + the stub-based evaluate tests + the live demo);
service_test.go no longer imports commerce.

Module wiring uses a git-ignored go.work (harness/CI generate it); no
replace directive in go.mod. Named 'tutorial' to avoid colliding with the
existing examples/demo and the make-demo binary.

Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
The production binary (cmd/ans-openshell-middleware) now links zero commerce
code. The commerce vertical lives entirely in the tutorial module, assembled
by its own dev-only binary (tutorial/cmd/ans-demo-middleware) that reuses the
generic app.Run and registers the commerce transform + discoverer via an
option.

- tutorial/cmd/ans-demo-middleware: the demo binary (commerce customizer,
  mandate prefetch, DPoP clock/nonce), built via the go.work workspace.
- tutorial/config: the Commerce config block (moved out of internal/config);
  the core config no longer parses or knows about commerce.
- tutorial/deploy/examples_test.go: the demo example-config test (moved from
  deploy/, asserts tutorialcfg.Commerce); deployment_test.go stays in core.
- internal/middleware/service_test.go: drop helpers left unused after the M3
  commerce E2E test removal.

Verified: GOWORK=off go list -deps ./cmd/ans-openshell-middleware | grep -c
commerce = 0; both modules build and test clean; lint shows only the known
CI-authoritative gosec-directive set.

Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
Wire the two-module layout into the build harness, CI, and docs.

- Makefile: 'workspace' (re)generates the gitignored go.work with a
  version-pinned replace; 'demo-build' builds bin/ans-demo-middleware via it.
  The production 'build' target is unchanged and needs no workspace.
- run-demo.sh: MW_BIN now defaults to bin/ans-demo-middleware (the demo egress
  is the production middleware plus the commerce tutorial transform); the
  preflight hint points at 'make demo-build'.
- ci.yml: a new 'tutorial' job generates the workspace (not committed) and
  runs build, demo-binary build, race tests, and lint for ./tutorial/...; the
  core build-and-test and lint jobs stay core-only (no go.work in a fresh
  checkout).
- README: a Repository layout section documenting the core/tutorial split and
  that the shipped binary contains no commerce code; live-demo README/RUNBOOK
  build the demo binary and run it as the egress.

Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
…, docs

Follow-up to the demo/core separation, from a self-review of PR #11:

- cmd/ans-openshell-middleware: fix the stale 'demo/cmd/...' doc path
  (renamed to tutorial/cmd/... during extraction) and document that the
  production binary strict-rejects a config carrying a commerce: block, since
  app.Config has no commerce field and the loader is strict.
- tutorial/transform: replace the raw capability.OutcomeKind(outcome.Kind)
  int conversion with an explicit capabilityKind switch so the commerce and
  capability enums are not assumed to share integer values; an unknown kind
  fails closed to Deny. Add TestCapabilityKind to pin the mapping.
- tutorial/config: add an exported FileConfig (app.Config inline + Commerce)
  used by both the demo binary and the example-config test, so the test
  exercises the real load path instead of a hand-maintained mirror struct
  that could silently drift from app.Config.

Signed-off-by: Andrei Ciobanu <aciobanu@godaddy.com>
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.

Prove end-to-end ANS identity enforcement via a commerce (AP2) egress capability

1 participant