axis-platform-sdk 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ # Case study: gating comments on a live news site
2
+
3
+ > **This is a case study — an example of using `axis-platform-sdk`, not the
4
+ > product.** The product is the SDK and the drop-in starters in
5
+ > [`templates/`](templates/). This page shows what one real integration looks
6
+ > like, so you can picture your own.
7
+
8
+ ## The platform
9
+
10
+ **Offworld News** is a (fictional) news site that wanted to let AI agents post
11
+ comments — but only agents with a real, accountable identity and explicit
12
+ permission to comment, not anonymous bots. Classic bouncer problem: an API key
13
+ can't say *which operator stands behind this agent* or *whether this agent was
14
+ actually allowed to comment*, and you can't revoke one bad agent without
15
+ disrupting everyone.
16
+
17
+ ## The integration
18
+
19
+ The comment endpoint is wrapped exactly the way the starters are:
20
+
21
+ 1. **Published a door policy** at `/.well-known/axis-access` declaring the
22
+ platform's audience and that commenting requires the standard
23
+ `content:comment` scope at `email` tier or above.
24
+ 2. **Gated the accept-comment path** with the SDK: pull the agent's AIT off the
25
+ request → verify against the public registry → check it holds
26
+ `content:comment` against the trustworthy `effective_scope` → accept or bounce.
27
+ 3. **Kept the bouncer state** — an arrivals ledger and a runtime blocklist — using
28
+ the same record shapes the SDK defines, so a moderator can see who commented
29
+ and boot a bad agent without a redeploy.
30
+
31
+ The adapter depends on the **published** package (`axis-platform-sdk@^0.2.1`) — it
32
+ does not vendor or fork the engine. That's the whole point of shipping the SDK as
33
+ a product: a real platform consumes it like any other dependency.
34
+
35
+ ## Two things this case study illustrates
36
+
37
+ - **Standard scopes matter.** The integration uses `content:comment` (the standard
38
+ AXIS scope for commenting), not a bespoke `comments:write`. A non-standard scope
39
+ would fail to match the agent's proven `effective_scope` and the agent would be
40
+ denied. When you pick `requireScopes`, use the standard vocabulary where one
41
+ fits.
42
+ - **Self-host is the product; the cloud-hosted version is the same engine.**
43
+ Offworld runs the gate on its own infrastructure (a Cloudflare Worker) — that is
44
+ the shipping, day-one path. It also pilots the in-development cloud-hosted version
45
+ for hosted arrival history and a moderator console. Because the SDK is the port
46
+ and the cloud-hosted version is being built as an adapter over it, those are the
47
+ same verification path — not a different system. For everyone else today,
48
+ self-host is the whole story; the cloud-hosted version is in alpha (Q3 2026).
49
+
50
+ ## What "the product" is, again
51
+
52
+ If you run a platform and want this, you do **not** start from Offworld's code.
53
+ You start from [`templates/node-express/`](templates/node-express/) or
54
+ [`templates/cloudflare-worker/`](templates/cloudflare-worker/), follow
55
+ [QUICKSTART.md](QUICKSTART.md), and you have your own bouncer in about ten
56
+ minutes. Offworld is just proof that the shape works on a real site.
package/CHANGELOG.md CHANGED
@@ -1,112 +1,179 @@
1
- # Changelog
2
-
3
- All notable changes to `axis-platform-sdk`. Pre-release; not yet published to npm.
4
-
5
- ## [0.2.1] — 2026-06-25
6
-
7
- ### Added TypeScript declarations
8
-
9
- - The package now ships `.d.ts` types (it stays authored in plain JS). A
10
- complete `src/index.d.ts` covers the full main-entry surface (verify,
11
- authorizer, scope, gate, client, ledger, blocklist, reportback); each subpath
12
- export (`./scope`, `./gate`, `./authorizer`, `./ledger`, `./blocklist`,
13
- `./reportback`) re-exports its slice. `package.json` exposes them via a
14
- top-level `types` field and per-subpath `types` conditions in `exports`.
15
- - This lets TypeScript consumers (e.g. Owyhee "The Door", swapping its vendored
16
- copy for the npm dependency) drop their hand-maintained declaration and get
17
- types from the package. Verified by a clean-room install + strict `tsc` of a
18
- consumer importing from the root and a subpath.
19
-
20
- ## [0.2.0] 2026-06-25
21
-
22
- First npm publish + public repo. Adds the stateful half (ledger, blocklist,
23
- reputation report-back) and reconciles it with Owyhee "The Door" (governor#27)
24
- so the SDK and the product share one arrival/block shape.
25
-
26
- ### Added the stateful half (ledger, blocklist, reputation report-back)
27
-
28
- - **`AccessLedger` (`src/ledger.js`)** — the platform's "who showed up" record.
29
- Logs every verdict to a pluggable store (default in-memory `MemoryLedgerStore`;
30
- documented adapter shape for D1 / SQLite / Postgres). `recent()` / `byOperator()`
31
- query helpers. `loggedGate(gate, ledger, fields)` wraps any gate so verdicts are
32
- logged as a side effect (a store failure never changes the verdict). Only the
33
- trustworthy `effective_scope` is recorded.
34
- - **`Blocklist` (`src/blocklist.js`)** a runtime, stateful block list over the
35
- same adapter shape. Blocks by `operator_id` AND by `agent_id` (agent-level
36
- blocking is the SDK's superset over The Door's operator-only `operator_blocks`).
37
- `blockedOperatorIds()` feeds verifyAgent's `blockedOperators`; `checkVerdict()`
38
- enforces agent-level blocks post-verify (needs the resolved agent_id).
39
- `gatedWithBlocklist(gate, blocklist)` wraps a gate.
40
-
41
- ### Reconciled with The Door (single source of truth, no duplication)
42
-
43
- - The ledger entry shape is now byte-compatible with The Door's `ArrivalRecord`
44
- / `arrivals` columns: carries `tier`, `delegation_valid`, `gate_id`,
45
- `requested_action`, `display_name`; `created_at` is epoch ms (was an ISO `ts`);
46
- `decision` uses the `auto_allow | denied | held | approved | booted` vocabulary
47
- (was `accepted | denied`), with the manual-review states available via a
48
- `recordEntry(..., { decision })` override.
49
- - Blocklist meta is `{ reason, created_at }` (epoch ms), matching `operator_blocks`.
50
- - The SDK is positioned as the **port + in-memory default**; The Door is the
51
- canonical **D1-backed adapter**. README documents the mapping table. The SDK
52
- ships the genuinely-new pieces The Door lacks (agent-level blocking, reputation
53
- emit); The Door keeps its own D1 state layer and, post-publish, depends on this
54
- package instead of vendoring it.
55
- - **Reputation report-back (`src/reportback.js`)** `reportFlag()` builds a
56
- protocol-shaped negative **Trust Attestation** (AXIS Layer 3; SPEC §4.5),
57
- signs it with the platform's own Ed25519 key (`getPlatformKey()`, WebCrypto,
58
- generated + persisted via a pluggable key store), and POSTs `{ attestation,
59
- platform_public_key, signature }` to a configurable `reputationUrl`. OFF by
60
- default — unconfigured is a graceful no-op (never throws). `blockAndReport()`
61
- blocks locally + reports. `buildAttestation` / `signAttestation` /
62
- `verifyAttestation` exposed. Zero-dep (no Buffer; WebCrypto + inline base64url
63
- + inline JCS, byte-for-byte matching axis-protocol-sdk's signing convention).
64
- - **`examples/bouncer-worker.js`** reference stateful bouncer: a Worker admin
65
- over the ledger + blocklist (`/admin/arrivals` enriched for display,
66
- `/admin/boot` = block + report, plus a tiny HTML console). A reference, not a
67
- product; in-memory stores.
68
-
69
- ### Notes
70
-
71
- - The reputation index is a SEPARATE, future, commercial service — NOT the
72
- canonical registry, which stays identity-only (Layer 1 + Layer 2). The
73
- `axis-reputation` stub receiver accepts-and-discards until the index is built.
74
- - New tests for ledger / blocklist / reportback + the Door-compatible shape.
75
- Full suite: 40 passing (`node --test`).
76
-
77
- ## [0.1.0] 2026-06-16 (unreleased)
78
-
79
- First cut of the platform/verifier ("bouncer") side of AXIS.
80
-
81
- ### Added
82
-
83
- - **`verifyAgent(token, opts)`** verifies an AIT against the registry
84
- (signature + revocation + delegation chain, all server-side) and applies a
85
- platform's policy (audience match, required scopes against the trustworthy
86
- `effective_scope`, blocked/approved operators, minimum verification tier).
87
- Returns one structured verdict.
88
- - **`aitGate(opts)` / `extractToken()` / `denialResponse()`** — a drop-in
89
- request gate for Cloudflare Workers (and Request-like objects). Pulls the AIT
90
- from `Authorization: Bearer`, `X-AXIS-Token`, or `?ait=`.
91
- - **`SwitchAuthorizer`** the free-tier gate engine and the first implementation
92
- of the Authorizer port. Config-driven on/off gates with optional minimum tier,
93
- required scopes, and operator allow/block lists. Its `policy` object is what a
94
- "Door policy" screen edits and saves. The port is engine-agnostic so a paid
95
- `EngineAuthorizer` (Permify / OpenFGA sidecar) can drop into the same slot.
96
- - **`scopeCovers` / `coversAll`** AXIS scope matcher, ported verbatim from the
97
- operator-side gateway so both sides agree on scope semantics.
98
- - **`enrich()`**, **`loadAccessPolicy()`**, **`decodeAitPayload()`** helpers.
99
- - Worked example: `examples/toy-platform-worker.js` (a bouncer comments service
100
- gated by a `SwitchAuthorizer`).
101
-
102
- ### Tested
103
-
104
- - 15 unit tests (`node --test`) across scope matching, the verify verdict
105
- matrix, and the SwitchAuthorizer gate logic. `loadAccessPolicy` additionally
106
- verified live against `registry.axisprime.ai`.
107
-
108
- ### Notes
109
-
110
- - Zero runtime dependencies; runs in Node 20+, Cloudflare Workers, browsers.
111
- - Identity verification is fixed/core; only the authorization decision is
112
- pluggable. The demo and the free tier need no external policy engine.
1
+ # Changelog
2
+
3
+ All notable changes to `axis-platform-sdk`.
4
+
5
+ ## [0.3.0] — Unreleased
6
+
7
+ Productization pass: repackages the published SDK as a free, drop-in **product
8
+ for platforms** — the platform-side equivalent of the AXIS Prime MCP's tester
9
+ packaging and adds a first-class Express/Connect adapter. The engine
10
+ (`verify` / `authorizer` / `ledger` / `blocklist` / `reportback`) is unchanged.
11
+
12
+ ### Added Express/Connect middleware (new subpath export)
13
+
14
+ - **`axis-platform-sdk/express`** exports **`axisGate(opts)`** (and `extractToken`)
15
+ the same verify-and-bounce logic as `aitGate`, but as Express/Connect
16
+ middleware `(req, res, next)`. On accept it sets `req.axis` and calls `next()`;
17
+ on deny it responds `401` (no token) / `403` (policy) / `503` (unexpected verify
18
+ error) with `{ error, message }`. Zero-dependency: it imports nothing from
19
+ Express, so it also runs on Connect, restify, and bare `http`. Ships
20
+ `src/express.d.ts` and an `exports` map entry. 6 new unit tests (suite now 46).
21
+ This is the first-class home for the Node drop-in (previously a copy-paste
22
+ template file).
23
+
24
+ ### Added adoption tiers, badge kit, forward-compat docs
25
+
26
+ - **Adoption tiers (A/B/C)** documented in the README: identity acceptance →
27
+ access policy → scope/tier enforcement, all the same `verifyAgent` call with more
28
+ options set.
29
+ - **"Verified by AXIS" badge kit** (`badges/`): three zero-dependency SVGs (light,
30
+ dark, compact) for platforms that show verification status on agent-authored
31
+ content, plus a usage README. Shipped in the package `files`.
32
+ - **Forward-compatibility note** on provenance gating: account-age / signup-method /
33
+ abuse-flag signals are protocol-defined but not yet registry-exposed; when they
34
+ ship they arrive as additive optional `verifyAgent` options + additive verdict
35
+ fields no breaking change for existing integrations.
36
+ - **"Staying up to date"** section: self-host is pull-based (semver + GitHub
37
+ Releases + CHANGELOG; Dependabot/Renovate); an opt-in integrator updates list is
38
+ planned.
39
+
40
+ ### Added — product wrapper (docs + drop-in starters)
41
+
42
+ - **README rewritten as a product pitch** ("let verified agents into your
43
+ platform; boot the bad ones; free, drop-in, no account required"). The full API
44
+ reference is preserved lower in the same file. Headlines the free, standalone,
45
+ registry-only path; positions the cloud-hosted version (in alpha, Q3 2026) as an
46
+ optional upgrade, not a requirement.
47
+ - **`QUICKSTART.md`** "gate your platform in 10 minutes": pick a starter →
48
+ decide audience + scope → drop in the gate → publish `/.well-known/axis-access`
49
+ test a deny test an admit.
50
+ - **`templates/node-express/`** a complete, runnable Express drop-in: a ~30-line
51
+ `axisGate(...)` middleware (`axis-gate.js`), a full worked server with door
52
+ policy + arrivals ledger + runtime blocklist + `/admin` console, and a `smoke`
53
+ test that asserts the deny paths against the live registry (verified passing).
54
+ - **`templates/cloudflare-worker/`** the same as a deployable Worker
55
+ (`wrangler dev` / `wrangler deploy`), promoting `examples/bouncer-worker.js`
56
+ into a clean starter with `wrangler.toml` + `package.json`.
57
+ - **`CASE-STUDY-offworld.md`** the Offworld News comment-gating integration,
58
+ explicitly labeled as a case study (an example of using the tool), not the
59
+ product.
60
+
61
+ ### Notes
62
+
63
+ - Standard scope vocabulary: starters and docs use `content:comment` (the standard
64
+ AXIS scope for commenting), not the older non-standard `comments:write`.
65
+ - `examples/` are left in place as teaching references; `templates/` are the
66
+ copy-paste starting point. `templates/` is added to the published `files` list.
67
+ - The only code change is the additive `src/express.js` adapter + its `.d.ts` and
68
+ `exports` entry; the existing engine modules are untouched. The 40 prior tests
69
+ are unchanged (suite now 46 with the Express tests). The previously published
70
+ surface remains fully backward-compatible — `/express` is purely additive.
71
+
72
+ ## [0.2.1] 2026-06-25
73
+
74
+ ### Added TypeScript declarations
75
+
76
+ - The package now ships `.d.ts` types (it stays authored in plain JS). A
77
+ complete `src/index.d.ts` covers the full main-entry surface (verify,
78
+ authorizer, scope, gate, client, ledger, blocklist, reportback); each subpath
79
+ export (`./scope`, `./gate`, `./authorizer`, `./ledger`, `./blocklist`,
80
+ `./reportback`) re-exports its slice. `package.json` exposes them via a
81
+ top-level `types` field and per-subpath `types` conditions in `exports`.
82
+ - This lets TypeScript consumers (e.g. the cloud-hosted version, swapping its
83
+ vendored copy for the npm dependency) drop their hand-maintained declaration and get
84
+ types from the package. Verified by a clean-room install + strict `tsc` of a
85
+ consumer importing from the root and a subpath.
86
+
87
+ ## [0.2.0] 2026-06-25
88
+
89
+ First npm publish + public repo. Adds the stateful half (ledger, blocklist,
90
+ reputation report-back) and reconciles it with the cloud-hosted version so the
91
+ SDK and the cloud product share one arrival/block shape.
92
+
93
+ ### Added the stateful half (ledger, blocklist, reputation report-back)
94
+
95
+ - **`AccessLedger` (`src/ledger.js`)** the platform's "who showed up" record.
96
+ Logs every verdict to a pluggable store (default in-memory `MemoryLedgerStore`;
97
+ documented adapter shape for D1 / SQLite / Postgres). `recent()` / `byOperator()`
98
+ query helpers. `loggedGate(gate, ledger, fields)` wraps any gate so verdicts are
99
+ logged as a side effect (a store failure never changes the verdict). Only the
100
+ trustworthy `effective_scope` is recorded.
101
+ - **`Blocklist` (`src/blocklist.js`)** — a runtime, stateful block list over the
102
+ same adapter shape. Blocks by `operator_id` AND by `agent_id` (agent-level
103
+ blocking is the SDK's superset over the cloud-hosted version's operator-only `operator_blocks`).
104
+ `blockedOperatorIds()` feeds verifyAgent's `blockedOperators`; `checkVerdict()`
105
+ enforces agent-level blocks post-verify (needs the resolved agent_id).
106
+ `gatedWithBlocklist(gate, blocklist)` wraps a gate.
107
+
108
+ ### Reconciled with the cloud-hosted version (single source of truth, no duplication)
109
+
110
+ - The ledger entry shape is now byte-compatible with the cloud-hosted version's `ArrivalRecord`
111
+ / `arrivals` columns: carries `tier`, `delegation_valid`, `gate_id`,
112
+ `requested_action`, `display_name`; `created_at` is epoch ms (was an ISO `ts`);
113
+ `decision` uses the `auto_allow | denied | held | approved | booted` vocabulary
114
+ (was `accepted | denied`), with the manual-review states available via a
115
+ `recordEntry(..., { decision })` override.
116
+ - Blocklist meta is `{ reason, created_at }` (epoch ms), matching `operator_blocks`.
117
+ - The SDK is positioned as the **port + in-memory default**; the cloud-hosted
118
+ version is the canonical **D1-backed adapter**. README documents the mapping
119
+ table. The SDK ships the genuinely-new pieces the cloud-hosted version lacks
120
+ (agent-level blocking, reputation emit); the cloud-hosted version keeps its own
121
+ D1 state layer and, post-publish, depends on this package instead of vendoring it.
122
+ - **Reputation report-back (`src/reportback.js`)** — `reportFlag()` builds a
123
+ protocol-shaped negative **Trust Attestation** (AXIS Layer 3; SPEC §4.5),
124
+ signs it with the platform's own Ed25519 key (`getPlatformKey()`, WebCrypto,
125
+ generated + persisted via a pluggable key store), and POSTs `{ attestation,
126
+ platform_public_key, signature }` to a configurable `reputationUrl`. OFF by
127
+ default — unconfigured is a graceful no-op (never throws). `blockAndReport()`
128
+ blocks locally + reports. `buildAttestation` / `signAttestation` /
129
+ `verifyAttestation` exposed. Zero-dep (no Buffer; WebCrypto + inline base64url
130
+ + inline JCS, byte-for-byte matching axis-protocol-sdk's signing convention).
131
+ - **`examples/bouncer-worker.js`** — reference stateful bouncer: a Worker admin
132
+ over the ledger + blocklist (`/admin/arrivals` enriched for display,
133
+ `/admin/boot` = block + report, plus a tiny HTML console). A reference, not a
134
+ product; in-memory stores.
135
+
136
+ ### Notes
137
+
138
+ - The reputation index is a SEPARATE, future, commercial service — NOT the
139
+ canonical registry, which stays identity-only (Layer 1 + Layer 2). The
140
+ `axis-reputation` stub receiver accepts-and-discards until the index is built.
141
+ - New tests for ledger / blocklist / reportback + the Door-compatible shape.
142
+ Full suite: 40 passing (`node --test`).
143
+
144
+ ## [0.1.0] — 2026-06-16 (unreleased)
145
+
146
+ First cut of the platform/verifier ("bouncer") side of AXIS.
147
+
148
+ ### Added
149
+
150
+ - **`verifyAgent(token, opts)`** — verifies an AIT against the registry
151
+ (signature + revocation + delegation chain, all server-side) and applies a
152
+ platform's policy (audience match, required scopes against the trustworthy
153
+ `effective_scope`, blocked/approved operators, minimum verification tier).
154
+ Returns one structured verdict.
155
+ - **`aitGate(opts)` / `extractToken()` / `denialResponse()`** — a drop-in
156
+ request gate for Cloudflare Workers (and Request-like objects). Pulls the AIT
157
+ from `Authorization: Bearer`, `X-AXIS-Token`, or `?ait=`.
158
+ - **`SwitchAuthorizer`** — the free-tier gate engine and the first implementation
159
+ of the Authorizer port. Config-driven on/off gates with optional minimum tier,
160
+ required scopes, and operator allow/block lists. Its `policy` object is what a
161
+ "Door policy" screen edits and saves. The port is engine-agnostic so a paid
162
+ `EngineAuthorizer` (Permify / OpenFGA sidecar) can drop into the same slot.
163
+ - **`scopeCovers` / `coversAll`** — AXIS scope matcher, ported verbatim from the
164
+ operator-side gateway so both sides agree on scope semantics.
165
+ - **`enrich()`**, **`loadAccessPolicy()`**, **`decodeAitPayload()`** helpers.
166
+ - Worked example: `examples/toy-platform-worker.js` (a bouncer comments service
167
+ gated by a `SwitchAuthorizer`).
168
+
169
+ ### Tested
170
+
171
+ - 15 unit tests (`node --test`) across scope matching, the verify verdict
172
+ matrix, and the SwitchAuthorizer gate logic. `loadAccessPolicy` additionally
173
+ verified live against `registry.axisprime.ai`.
174
+
175
+ ### Notes
176
+
177
+ - Zero runtime dependencies; runs in Node 20+, Cloudflare Workers, browsers.
178
+ - Identity verification is fixed/core; only the authorization decision is
179
+ pluggable. The demo and the free tier need no external policy engine.
package/QUICKSTART.md ADDED
@@ -0,0 +1,168 @@
1
+ # Gate your platform in 10 minutes
2
+
3
+ Goal: an endpoint on your platform that only accepts **AXIS-verified agents** with
4
+ the right permission, turns everyone else away with a clear reason, and lets you
5
+ boot a bad actor without a redeploy.
6
+
7
+ You need: Node 20+ (or a Cloudflare Workers project). You do **not** need an
8
+ account with us, a Cloudflare account (unless you choose the Worker path), a
9
+ database, or any hosted service. The free path is one outbound HTTPS call to the
10
+ public AXIS registry.
11
+
12
+ ---
13
+
14
+ ## 0. Pick a starter (1 min)
15
+
16
+ Copy the folder that matches your stack and work inside it:
17
+
18
+ - **Node / Express** → [`templates/node-express/`](templates/node-express/)
19
+ - **Cloudflare Workers** → [`templates/cloudflare-worker/`](templates/cloudflare-worker/)
20
+
21
+ Already have an app? Skip the copy and just `npm install axis-platform-sdk`, then
22
+ follow the inline code below.
23
+
24
+ ```
25
+ npm install axis-platform-sdk # zero dependencies
26
+ ```
27
+
28
+ ---
29
+
30
+ ## 1. Decide two things (1 min)
31
+
32
+ - **Your audience** — a stable id for *your* platform, e.g. `comments.mysite.com`.
33
+ Agents address their token to this; you reject tokens addressed to anyone else.
34
+ - **The scope an agent must hold** — e.g. `content:comment`. Use a standard AXIS
35
+ scope where one fits (`content:comment` for commenting). This is checked against
36
+ the agent's **proven** permission, not what its token claims.
37
+
38
+ ---
39
+
40
+ ## 2. Drop the gate in front of your action (3 min)
41
+
42
+ **Express:**
43
+
44
+ ```js
45
+ import express from 'express';
46
+ import { axisGate } from 'axis-platform-sdk/express'; // first-class middleware export
47
+
48
+ const app = express();
49
+ app.use(express.json());
50
+
51
+ app.post('/comments',
52
+ axisGate({ audience: 'comments.mysite.com', requireScopes: ['content:comment'] }),
53
+ (req, res) => {
54
+ // req.axis.agent_id is a VERIFIED agent id. Run your real handler.
55
+ res.json({ ok: true, by: req.axis.agent_id });
56
+ });
57
+
58
+ app.listen(8787);
59
+ ```
60
+
61
+ **Cloudflare Worker:**
62
+
63
+ ```js
64
+ import { aitGate, denialResponse } from 'axis-platform-sdk';
65
+
66
+ const gate = aitGate({ audience: 'comments.mysite.com', requireScopes: ['content:comment'] });
67
+
68
+ export default {
69
+ async fetch(request) {
70
+ const verdict = await gate(request);
71
+ if (!verdict.accepted) return denialResponse(verdict);
72
+ return Response.json({ ok: true, by: verdict.agent_id });
73
+ },
74
+ };
75
+ ```
76
+
77
+ The gate pulls the token from `Authorization: Bearer <ait>`, `X-AXIS-Token`, or
78
+ `?ait=`, verifies it against the registry, applies your policy, and either runs
79
+ your handler or returns `401` (no token) / `403` (bounced) with a real reason.
80
+
81
+ ---
82
+
83
+ ## 3. Publish your door policy (2 min)
84
+
85
+ Tell agents (and their operators) what your platform requires by serving a small
86
+ JSON document at **`/.well-known/axis-access`**:
87
+
88
+ ```js
89
+ // GET /.well-known/axis-access
90
+ {
91
+ "axis_version": "0.3",
92
+ "platform_id": "comments.mysite.com",
93
+ "audience": "comments.mysite.com",
94
+ "access_policy": {
95
+ "minimum_verification_level": "email",
96
+ "required_scopes": ["content:comment"],
97
+ "allow_unverified": false
98
+ }
99
+ }
100
+ ```
101
+
102
+ Both starters already serve this. An agent's tooling can read it with
103
+ `loadAccessPolicy('https://comments.mysite.com')` to know how to get in. (For a
104
+ live example, `curl https://registry.axisprime.ai/.well-known/axis-access`.)
105
+
106
+ ---
107
+
108
+ ## 4. Test the deny path — right now, no agent needed (1 min)
109
+
110
+ A request with no identity must be turned away:
111
+
112
+ ```bash
113
+ curl -i -X POST localhost:8787/comments -H 'content-type: application/json' -d '{"text":"hi"}'
114
+ # HTTP/1.1 401 Unauthorized
115
+ # {"error":"no_token","message":"No AIT presented"}
116
+ ```
117
+
118
+ The Express starter automates this:
119
+
120
+ ```
121
+ npm run smoke
122
+ # PASS — no AIT -> 401 no_token
123
+ # PASS — invalid AIT -> 403 denied
124
+ ```
125
+
126
+ If you see those, your bouncer is live and refusing unidentified traffic. That's
127
+ the free path working end-to-end against the public registry.
128
+
129
+ ---
130
+
131
+ ## 5. Test the accept path — let a real agent in (2 min)
132
+
133
+ To see a `200`, you need an agent that has actually been **delegated**
134
+ `content:comment` by its operator and presents a token addressed to your
135
+ `audience`. Agents get their identity from the
136
+ **[AXIS Prime MCP](https://github.com/MachinesOfDesire/axis-mcp)** (the agent
137
+ side):
138
+
139
+ 1. The agent onboards via the MCP and registers with an operator.
140
+ 2. The operator **grants** it `content:comment` (`axis grant content:comment`).
141
+ 3. The agent mints an AIT with `aud = comments.mysite.com` and presents it:
142
+
143
+ ```bash
144
+ curl -X POST https://comments.mysite.com/comments \
145
+ -H "Authorization: Bearer <the-agents-AIT>" \
146
+ -H 'content-type: application/json' \
147
+ -d '{"text":"hello from a verified agent"}'
148
+ # {"ok":true,"posted_by":"axis:acme:bot","operator":"axis:acme:op","scope":["content:comment"], ... }
149
+ ```
150
+
151
+ In the starter's `/admin` console, that arrival turns **green** — and you can
152
+ **boot** it (block + optionally report) with one click if it misbehaves.
153
+
154
+ ---
155
+
156
+ ## Where to go next
157
+
158
+ - **Require stronger identity:** add `minTier: 'domain'` (or `verified`,
159
+ `kyb_organization`) to the gate to demand domain- or KYB-verified operators.
160
+ - **Persist arrivals + blocks:** the in-memory stores reset on restart. Implement
161
+ the store adapter shape (see `ledger.js` / `blocklist.js` in the SDK) against
162
+ your database. The record shape matches the cloud-hosted version, so moving to
163
+ it later won't mean reshaping data.
164
+ - **Turn on reputation report-back:** set a `reputationUrl` so booting a bad agent
165
+ emits a signed Trust Attestation onward. Off by default.
166
+ - **Don't want to host the console + state yourself?** A cloud-hosted version is in
167
+ alpha testing (planned for Q3 2026) over this exact engine. For now, self-host is
168
+ the way — and it's the whole product.