@attested-intelligence/aga-mcp-server 3.3.3 → 3.6.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,210 @@
2
2
 
3
3
  All notable changes to `@attested-intelligence/aga-mcp-server` are recorded here, newest first. This package follows [Semantic Versioning](https://semver.org). The signed receipt and evidence-bundle wire format is the canonical SEP profile; any format-affecting change is called out explicitly.
4
4
 
5
+ ## 3.6.0 — 2026-09-18
6
+
7
+ ### `aga-proxy` now honours `AGA_GATEWAY_KEY` / `AGA_GATEWAY_KEY_FILE`
8
+
9
+ This package ships two binaries and only one of them read the operator's key contract. `aga-mcp-server`
10
+ honoured both variables; `GovernanceProxy` called `generateSigner()` unconditionally in its
11
+ constructor, read neither variable, and printed no warning. An operator who set `AGA_GATEWAY_KEY`
12
+ correctly and started the proxy was **silently ignored**: no effect, no notice, and evidence bundles
13
+ whose issuing key rotated on every restart. That is worse than an unsupported feature — silence
14
+ defeats a correct configuration. It is checkable in the published 3.5.0 tarball, where `dist/proxy/`
15
+ contains zero references to either variable while `dist/server.js` contains seven.
16
+
17
+ **What changes for you:** if you already set `AGA_GATEWAY_KEY` or `AGA_GATEWAY_KEY_FILE` and ran
18
+ `aga-proxy`, that variable now actually takes effect, so the proxy's `gateway_public_key` stops
19
+ rotating and becomes the key your seed derives. Nothing breaks: a proxy with neither variable set
20
+ behaves exactly as before except that it now says so. If you *want* the old throwaway-key behaviour
21
+ with a variable set, pass the new `--ephemeral` flag.
22
+
23
+ - **One resolver, both binaries** (`src/sep/gateway-key.ts`). Order: `AGA_GATEWAY_KEY`, then
24
+ `AGA_GATEWAY_KEY_FILE`, then an ephemeral key. An invalid or unreadable key **warns and falls
25
+ back** rather than exiting — deliberately matching what `aga-mcp-server` has always done, because
26
+ refusing to start would take a governed boundary offline over a key that only affects whether
27
+ provenance is *pinnable*. Integrity, chaining and the deny path do not depend on which key signs.
28
+ A second variable name for the proxy was rejected: one installable must not carry two key
29
+ contracts.
30
+ - **The proxy prints its active PUBLIC key at startup**, with its source:
31
+ `Signing gateway key <64-hex> (persisted via AGA_GATEWAY_KEY_FILE)`. This is the line that makes an
32
+ honest pin possible. A verifier handed a key lifted out of the bundle it is checking will print
33
+ `provenance verified` and has proved nothing about issuance — the check is circular. A key printed
34
+ before any bundle exists is a key you can obtain **out of band**.
35
+ - **`--ephemeral`** turns the old silent default into a stated choice, on both `start` and `run`.
36
+ - **Never logs key material.** Only the variable name that was tried and the derived public key.
37
+ - `aga-mcp-server` is behaviourally unchanged. It passes its portal keypair's secret as the fallback
38
+ seed, so its unconfigured case stays byte-identical to previous releases; extracting the resolver
39
+ must not silently rotate either binary's key, and a test now holds that.
40
+
41
+ **Not a turnkey guarantee, stated plainly.** Persisting the key makes provenance *pinnable*; it does
42
+ not make it *pinned*. The default on both binaries is still an ephemeral key, an unconfigured
43
+ deployment carries the old risk in full, and nothing in the package detects a deployment that skipped
44
+ the configuration. `THREAT_BOUNDARY.md` item 4 now says exactly that.
45
+
46
+ ### Two halves of the above that were written but never shipped
47
+
48
+ `describeKey()` and the `--ephemeral` option both existed in the 3.5.0 tree and **neither was reachable
49
+ from the entry point**: `dist/proxy/index.js` never called `describeKey()`, so the shipped binary
50
+ printed no key line at all, and `new GovernanceProxy({...})` never passed `ephemeral`, so the flag was
51
+ parsed and discarded. Both are now wired, and the integration test below drives the built artifact
52
+ rather than the source, which is what catches this class of defect. A declared flag the entry point
53
+ does not read is not a feature.
54
+
55
+ ### Tests
56
+
57
+ - `tests/sep/gateway-key.test.ts` (17): env key, key file, precedence when both are set, an invalid
58
+ value, an unreadable file, malformed file contents, the unconfigured case, `forceEphemeral`,
59
+ `fallbackSeed` identity across all three ephemeral paths, the default warning sink being
60
+ `console.error` and never `console.log`, the log prefix, and that no path emits key material.
61
+ - `tests/integration/proxy-gateway-key.test.ts` (7): drives the **built** `dist/proxy/index.js` as a
62
+ real child process. One key file, two processes, identical public key on the banner and zero
63
+ warnings; the env var and the key file agree; unconfigured rotates between runs and says so;
64
+ `--ephemeral` overrides a configured key; an invalid key warns, falls back, and still announces what
65
+ it signs with; the seed appears in neither stream; the key line is on stdout and the warning on
66
+ stderr.
67
+ - Suite: **404 → 428** tests across 45 → 47 files. SEP conformance 6/6. `check:pack` clean.
68
+
69
+ ### Documentation corrected in the same release
70
+
71
+ `DEPLOYMENT.md` §2 and `THREAT_BOUNDARY.md` item 4 ship inside this tarball and described the *old*
72
+ proxy behaviour in the same breath as the new. The §2 comparison table had been updated in one cell
73
+ while the two paragraphs under it still told operators the proxy ignores both variables; `README.md`
74
+ still said so outright. All three are now consistent and **version-scoped**, so a reader running 3.5.0
75
+ or earlier still gets the truth for the version they are running rather than a claim that only holds
76
+ after upgrading. README test counts updated to the recomputed figure.
77
+
78
+ ## 3.5.0 — 2026-08-29
79
+
80
+ **Version ruled 2026-08-29.** `3.4.0` was claimed by two different trees ~93 commits apart, so
81
+ publishing either line under that number would permanently burn it for the other. This line ships as
82
+ **3.5.0** and `3.4.0` is abandoned unpublished. Everything below — the 2026-08-28 honesty and safety
83
+ work *and* the 3.4.0 work completed 2026-07-31 — releases together; npm `latest` has been 3.3.3 since
84
+ 2026-07-03.
85
+
86
+ ### BEHAVIOR CHANGE — TTL expiry now fails closed
87
+
88
+ **If you rely on measurement continuing after an artifact's TTL lapses, this release breaks that.**
89
+ Founder decision D1, ruled 2026-08-29.
90
+
91
+ Previously `Portal.measure()` treated TTL expiry as *graceful degradation*: it moved the portal to
92
+ `SAFE_STATE`, logged the reason, and **kept accepting measurements indefinitely**. An expired artifact
93
+ therefore went on being measured, and the receipt recorded no enforcement because none occurred.
94
+
95
+ Now TTL expiry sets `TERMINATED`, exactly as the sibling revocation branch always has — TTL was the
96
+ odd one out, not the new behavior. Concretely:
97
+
98
+ - The first post-expiry `measure()` returns `ttl_ok: false`, `degraded: true`, and the portal is
99
+ `TERMINATED`.
100
+ - **Any subsequent `measure()` throws `Portal is terminated`.** Re-attestation is the only way back.
101
+ Callers that looped on `measure()` past expiry must handle this.
102
+ - `measure_integrity` now seals `enforcement_action: 'TERMINATE'` on TTL expiry, and that is accurate:
103
+ termination genuinely happens. The invariant across every version of this code is unchanged — the
104
+ receipt says what actually happened. Only the underlying behavior moved.
105
+ - The `TTL_EXPIRED` degradation entry is still written. It is the forensic record of *why* the portal
106
+ terminated, and dropping it would trade one honesty problem for another.
107
+
108
+ **Semver note, stated plainly:** this is a behavioral break and a strict reading argues for a major
109
+ version. It ships as a minor because the previous behavior contradicted the documented intent, and
110
+ because no released consumer depends on post-expiry measurement (npm `latest` has been 3.3.3 since
111
+ 2026-07-03). If that reading is wrong for your deployment, pin `3.3.3`.
112
+
113
+ Found while implementing this: `tests/core/fail-closed.test.ts` contained a test named
114
+ **"fail-closed: expired TTL blocks execution"** that asserted `SAFE_STATE` — the state in which
115
+ measurement *continues*. A test whose name claimed the boundary blocked execution had been green while
116
+ proving it did not. It now asserts termination and that a second call throws. Six other assertions
117
+ across four files encoded the old behavior and were updated with it; all were seen to go red before
118
+ going green.
119
+
120
+ Checked and unchanged: no shipped document and no public page makes a TTL enforcement claim, so this
121
+ change corrects no external copy.
122
+
123
+ ### Documentation: proxy key-persistence claims corrected
124
+
125
+ `DEPLOYMENT.md` and `THREAT_BOUNDARY.md` ship inside this tarball, and both described key persistence
126
+ that **`aga-proxy` does not implement**. DEPLOYMENT §1 is explicitly about the proxy; §2 then told
127
+ operators to set `AGA_GATEWAY_KEY` / `AGA_GATEWAY_KEY_FILE`, said the ephemeral fallback "warns on
128
+ stderr", and pointed at `get_server_info`. For the proxy none of that is true — an operator following
129
+ it pinned a key that rotates on the next restart.
130
+
131
+ - `GovernanceProxy` calls `generateSigner()` unconditionally in its constructor: no key CLI option, no
132
+ environment variable, no signer through `ProxyServerOptions`. Two runs with an identical
133
+ `AGA_GATEWAY_KEY` produce different gateway public keys (measured). `dist/server.js` carries the
134
+ `EPHEMERAL gateway signing key` stderr warning; `dist/proxy/` has none.
135
+ - `THREAT_BOUNDARY.md` §3.4 previously read **"Key persistence — mitigated in 3.0"** while §3.1–3.3 are
136
+ each prefixed "Proxy —" and §3.4 was not, so it scanned as covering the package. A residual-risk
137
+ register must not record an unmitigated risk as mitigated. It now states the asymmetry.
138
+ - Both files gained an entry-point comparison table, and record that a verifier handed a key taken from
139
+ the bundle under test will still print `provenance verified` — that check is **circular**, and only a
140
+ key obtained beforehand proves issuance.
141
+
142
+ **KNOWN LIMITATION, unchanged in this release:** `aga-proxy` still has no way to persist its signing
143
+ key, so proxy-issued bundles are integrity-verifiable but **not provenance-pinnable across restarts**.
144
+ `aga-mcp-server` (the stdio server) is unaffected and persists normally. Closing the proxy gap is
145
+ runtime work that is deliberately out of scope here.
146
+
147
+ ### Honesty + safety fixes (2026-08-28)
148
+
149
+ - **Security: two HIGH-severity production advisories cleared, both newly in range.** `npm audit
150
+ --omit=dev --audit-level=high` was **failing** on this branch:
151
+ - `fast-uri` — GHSA-v2hh-gcrm-f6hx (host confusion via backslash authority delimiter, CVSS 7.5).
152
+ **The 3.4.0 entry below is not wrong about what it did** — it refreshed `fast-uri` to 3.1.4, which
153
+ was clean at the time. The advisory range has since **widened to `<3.1.5`**, so the pinned version
154
+ became vulnerable without anything in this repo changing. Now overridden to `^3.1.5` (resolves 3.1.6).
155
+ - `ip-address` — GHSA-mwp4-54f8-5fhr (Address4 decodes leading-zero octets as decimal while
156
+ resolvers decode them as octal; SSRF / trust-boundary bypass). Overridden to `>=10.3.1`
157
+ (resolves 10.5.0). Reached via `@modelcontextprotocol/sdk` → `express-rate-limit`.
158
+ - `hono` — the existing `>=4.12.25` override no longer covered a moderate CORS ReDoS whose range
159
+ extends to `<4.12.34`; bumped accordingly (resolves 4.13.5).
160
+
161
+ Production dependencies are once again **clean at all severities** (`npm audit --omit=dev`:
162
+ found 0 vulnerabilities), which is what the 3.4.0 entry claims — that claim had silently gone false.
163
+ **This is a standing hazard worth naming: a dependency claim is only true as of the day it was
164
+ measured.** Advisory ranges widen over unchanged code, so a "clean" statement in a changelog decays
165
+ on its own. Re-run the audit immediately before publishing, not once at RC time.
166
+ Verified after the change: suite 404/404, `npm run build` clean, cross-stack 61/61.
167
+
168
+ > Resolved 2026-08-29: this line ships as **3.5.0**; `3.4.0` is abandoned unpublished so the number
169
+ > stays free for the other tree. See the release heading above.
170
+
171
+ - **TTL expiry no longer signs an enforcement that never happened.** `measure_integrity` sealed
172
+ `enforcement_action: "TERMINATE"` into a signed receipt on TTL expiry and described the branch as
173
+ "fail-closed termination". Nothing terminates: the portal degrades to `SAFE_STATE` and keeps
174
+ accepting measurements, and `portal.enforce()` is never called on that path — nor *can* it be, since
175
+ it throws unless the state is `DRIFT_DETECTED`. Post-expiry calls kept succeeding and kept minting
176
+ fresh receipts, each asserting a termination that did not occur. The receipt now records what
177
+ actually happened. The sibling revocation branch was already honest and is unchanged.
178
+ **Not changed here:** whether TTL expiry *should* hard-terminate and force re-attestation. That is an
179
+ open product decision; until it is ruled, the record must not claim a behavior the code lacks.
180
+ - **`aga-proxy export --output` no longer destroys an existing file.** Pointing `--output` at an
181
+ existing path silently truncated it and exited 0 reporting success — an ordinary path, no symlink,
182
+ no attacker, on the very artifact a verifier consumes. Export is now exclusive-create by default;
183
+ replacing a file requires an explicit `--force`.
184
+ - **A malformed `--pubkey` is now a hard error (exit 2) instead of a silent downgrade.** A truncated or
185
+ mistyped key previously produced `VERIFIED (integrity only)` at exit 0, so an operator who intended
186
+ to pin provenance got a green result. The reference verifier has always guarded this; the guard had
187
+ never propagated to the CLI that becomes the published package.
188
+ - **The cross-stack release gate now exercises the pinned path.** The corpus had no wrong-pin case, and
189
+ the harness collapses each stack to VERIFIED/FAILED without reading `issuerVerified`, so a verifier
190
+ that silently discarded the pin still read VERIFIED and the gate stayed green. A valid-but-wrong pin
191
+ control was added; all six verifiers agree FAILED across 61 cases.
192
+ Recorded, not fixed: on a 64-hex pin that is **not a valid curve point**, the stacks split 1-vs-5 on
193
+ whether that means "no pin" or "a pin that cannot match". Needs a spec ruling on malformed-pin
194
+ semantics before "six verifiers agree" is stated without qualification.
195
+
196
+ ### Included from the abandoned 3.4.0 line (work completed 2026-07-31, never published)
197
+
198
+ > **Not published.** This header previously read as a dated, shipped release. It is not on npm and has
199
+ > not been since the work was completed. Nothing below is available to a consumer running
200
+ > `npm install @attested-intelligence/aga-mcp-server`, which still resolves 3.3.3.
201
+
202
+ - **Verification now fail-closed rejects integers outside ±2^53 in receipt/checkpoint numeric fields — bundles previously VERIFIED may now FAIL; this closes the cross-language verdict split.** JavaScript loses integer precision beyond `Number.MAX_SAFE_INTEGER`, so a bundle carrying e.g. `leaf_count > 2^53` could VERIFY in the JS stack on bytes the Go and Python verifiers read as a different number. Every stack now rejects the same out-of-range bundles at the same floor.
203
+ - Security: cleared GHSA-frvp-7c67-39w9 (`@hono/node-server` < 2.0.5, Windows encoded-backslash path traversal in `serve-static`, moderate): `@modelcontextprotocol/sdk` bumped to `^1.30.0` (resolves `@hono/node-server` 2.0.12) and an `overrides` pin holds `@hono/node-server` at `>= 2.0.5`. Production dependencies (`npm audit --omit=dev`) are clean at all severities; `body-parser` (2.3.0) and `fast-uri` (3.1.4) transitives refreshed in the same pass.
204
+ - Verifier CLI entry guard (developed in this repo, shipping alongside in `@attested-intelligence/aga-verify` 2.2.0): the CLI now runs only when the verifier module itself is the executed entry script (resolved-path identity against `import.meta.url`), replacing a substring check that ran the CLI — hijacking stdout and calling `process.exit()` — whenever the entry script's path merely contained `verify`.
205
+ - Regression coverage for the safe-integer floor above (`tests/sep/safe-integer-floor.test.ts`): a consistently re-signed bundle carrying a numeric field isolates the floor from the signature and structural checks, so removing the floor flips the verdict and the suite goes red. The cross-stack corpus also gains the ±2^53 and non-finite raw-literal cases (all six verifiers agree FAILED).
206
+ - Proxy CLI hardening: an unrecognized `--profile` value now exits 2 listing the valid names (`permissive`, `standard`, `restrictive`) instead of silently falling back to `permissive`; starting with an `audit_only` policy prints a loud multi-line stderr banner stating that every call is permitted and recorded and no call is denied in that mode. The default profile is unchanged. `aga-proxy run` (a documented foreground alias of `start`) is fixed — it exited 1 on every invocation from 3.0.0 through 3.3.3 because of a stray-argument delegation bug, and now shares the `start` implementation directly; `policy switch` gained the same own-property profile guard as `start`.
207
+ - MCP registry: `package.json` gains `"mcpName": "io.github.attestedintelligence/aga-mcp-server"` for npm package-ownership validation by the official registry.
208
+
5
209
  ## 3.3.3 — 2026-07-03
6
210
 
7
211
  - Internal source-comment cleanup only: **no runtime, verifier, or evidence-bundle change** (behavior is byte-identical to 3.3.2).
package/DEPLOYMENT.md CHANGED
@@ -38,22 +38,51 @@ Only `method === "tools/call"` is policy-evaluated. Other JSON-RPC methods are f
38
38
 
39
39
  The **gateway key** is the Ed25519 key that signs every receipt and checkpoint. Pinning its public key is what turns a bundle from "internally consistent" into "provably issued by *this* gateway."
40
40
 
41
+ > ### ⚠️ Which entry point are you running? The two do NOT handle the key the same way.
42
+ >
43
+ > | | `aga-mcp-server` (stdio MCP server) | `aga-proxy` (governance proxy) |
44
+ > |---|---|---|
45
+ > | Reads `AGA_GATEWAY_KEY` / `AGA_GATEWAY_KEY_FILE` | **yes** | **yes, since 3.6.0** — silently ignored in 3.5.0 and earlier |
46
+ > | Key when unset | ephemeral, **warns on stderr** | ephemeral, **warns on stderr** since 3.6.0 (no warning in 3.5.0 and earlier) |
47
+ > | Says which key is active at startup | `get_server_info` → `gateway_public_key` | startup banner, since 3.6.0 |
48
+ > | Provenance pinnable across restarts | yes, once persisted | yes, once persisted, since 3.6.0 — **no**, at any earlier version |
49
+ >
50
+ > **If you are on 3.5.0 or earlier:** `aga-proxy` calls `generateSigner()` unconditionally at construction — no key CLI option, no environment variable, no signer through its constructor. Setting `AGA_GATEWAY_KEY` before starting it has **no effect and produces no warning**, and two runs with an identical `AGA_GATEWAY_KEY` produce different gateway public keys. Treat evidence from such a proxy as integrity-verifiable and **not** provenance-pinnable across restarts. Upgrade, or accept that scope.
51
+ >
52
+ > **From 3.6.0 on** both binaries resolve the key through the same function, in the same order (`AGA_GATEWAY_KEY`, then `AGA_GATEWAY_KEY_FILE`, then ephemeral), with the same warnings. `aga-proxy --ephemeral` makes a throwaway key a stated choice rather than a silent default.
53
+ >
54
+ > **Pinning is still not automatic, on either binary.** A verifier given a key lifted out of the same bundle it is checking will print `provenance verified` — it cannot know where you got the key. That check is circular. Only a key you obtained **out of band, before** the bundle proves issuance; persisting the key is what makes such a key *exist*, not a substitute for obtaining it independently.
55
+ >
56
+ > Verified against 3.6.0 (`tests/integration/proxy-gateway-key.test.ts` drives the built `dist/proxy/index.js` in separate processes and asserts one key file yields one public key with zero warnings).
57
+
41
58
  ### Generate a 32-byte seed (64-hex)
42
59
  ```bash
43
60
  node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
44
61
  ```
45
62
 
46
- ### Persist it (so provenance survives restarts and is pinnable)
63
+ ### Persist it (so provenance survives restarts and is pinnable) — both binaries, since 3.6.0
47
64
  Either:
48
65
  ```bash
49
66
  export AGA_GATEWAY_KEY=<64-hex-seed> # environment variable
50
67
  # or
51
68
  export AGA_GATEWAY_KEY_FILE=/run/secrets/aga-gateway-key # file containing the hex seed
52
69
  ```
53
- If neither is set, the server uses an **ephemeral** key that rotates on every restart (it warns on stderr). Ephemeral is fine for local experiments but means **provenance cannot be pinned across restarts** — avoid it in anything you'll later audit.
70
+ If neither is set, `aga-mcp-server` uses an **ephemeral** key that rotates on every restart (it warns on stderr). Ephemeral is fine for local experiments but means **provenance cannot be pinned across restarts** — avoid it in anything you'll later audit.
71
+
72
+ `aga-proxy` honours both variables as of **3.6.0**, through the same resolver the MCP server uses, and prints the active public key at startup so you can pin it out of band. Pass `--ephemeral` to deliberately use a throwaway key instead. **In 3.5.0 and earlier the proxy ignored both variables silently** — if you are on an older version, a key you set had no effect and no warning was printed.
54
73
 
55
74
  ### Obtain the public key to pin
56
- Call the `get_server_info` tool → **`gateway_public_key`**. That 64-hex value is what verifiers pin.
75
+ For `aga-mcp-server`: call the `get_server_info` tool → **`gateway_public_key`**. That 64-hex value is what verifiers pin.
76
+
77
+ For `aga-proxy`, since **3.6.0**: the startup banner prints the active public key and where it came from, e.g.
78
+
79
+ ```
80
+ Signing gateway key 248acbdb… (persisted via AGA_GATEWAY_KEY_FILE)
81
+ ```
82
+
83
+ That line is printed before any bundle exists, which is exactly what makes it usable as an out-of-band pin. Record it from the console or from your process supervisor's log, not from the bundle. `aga-proxy status` still reports only `running` and `pid`.
84
+
85
+ The gateway key also appears inside the exported bundle (`public_key`) — whether saved to a file or fetched live from the loopback control channel's `GET /export`. **Do not pin that one.** It is the key you are trying to check, so a verifier fed it will agree with itself; and on a proxy left unconfigured (or run with `--ephemeral`) it is a throwaway that rotates on restart. Pinning it proves the bundle is internally consistent — not who issued it.
57
86
 
58
87
  ### Pin it when verifying
59
88
  ```bash
@@ -74,7 +103,9 @@ The gateway key is a signing secret — **anyone who holds it can mint a fully V
74
103
  ## 3. Recommended hardened configuration
75
104
 
76
105
  ```jsonc
77
- // Claude Desktop / MCP client config — proxy in front of a stdio upstream, persisted key
106
+ // Claude Desktop MCP config — the stdio server with a persisted gateway key.
107
+ // (This runs `aga-mcp-server`. For an aga-proxy-in-front-of-upstream deployment see §1;
108
+ // since 3.6.0 the proxy reads the same variable — §2.)
78
109
  {
79
110
  "mcpServers": {
80
111
  "aga": {
@@ -86,7 +117,7 @@ The gateway key is a signing secret — **anyone who holds it can mint a fully V
86
117
  }
87
118
  ```
88
119
  Checklist:
89
- - [ ] stdio upstream (no HTTP upstream, or HTTP only behind network isolation).
120
+ - [ ] For a proxy deployment: stdio upstream (no HTTP upstream, or HTTP only behind network isolation).
90
121
  - [ ] Persisted gateway key from a secret manager / restricted file.
91
122
  - [ ] The agent's **only** route to tools is through the proxy (network isolation).
92
123
  - [ ] Verifiers **pin** `gateway_public_key`; an unpinned PASS is treated as integrity-only, not provenance.
package/README.md CHANGED
@@ -1,20 +1,20 @@
1
1
  # AGA - Attested Governance Artifacts
2
2
 
3
- Cryptographic runtime governance for AI agents and autonomous systems.
3
+ Verifiable decision records for AI agents: each governed tool-call decision becomes a signed, hash-chained receipt, exported in evidence bundles anyone can verify offline against the published format.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@attested-intelligence/aga-mcp-server)](https://www.npmjs.com/package/@attested-intelligence/aga-mcp-server)
6
6
  [![PyPI](https://img.shields.io/pypi/v/aga-governance)](https://pypi.org/project/aga-governance/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/attestedintelligence/aga-mcp-server/blob/main/LICENSE)
8
8
  [![npm provenance](https://img.shields.io/badge/npm-SLSA%20provenance-brightgreen)](https://www.npmjs.com/package/@attested-intelligence/aga-mcp-server)
9
9
 
10
- > **Status: published to npm; this release carries SLSA build provenance (check it: `npm audit signatures`).** The server tools and the `aga-proxy` emit the **canonical SEP evidence bundle**, verifiable offline by the published `@attested-intelligence/aga-verify` and the reference verifier `aga-receipt-spec/verify/verify-sep.mjs`. **Since 3.2.0 the verifier is algorithm-agile and ships a post-quantum profile:** v1 `Ed25519-SHA256-JCS` (the default the gateway emits) and v2 `ML-DSA-65+Ed25519-SHA256-JCS` (a NIST FIPS-204 ML-DSA-65 + RFC-8032 Ed25519 **composite**, both must verify), selected per-bundle by the `algorithm` field with a `VERIFIED / FAILED / UNSUPPORTED_PROFILE` trichotomy. Pre-3.0 releases (a legacy continuity-chain bundle that does *not* verify under the SEP verifier) are deprecated; use `^3.0.0`. Claim scope and residual attack surface are documented honestly in `THREAT_BOUNDARY.md`.
10
+ > **Status: published to npm; this release carries SLSA build provenance (check it: `npm audit signatures`).** The server tools and the `aga-proxy` emit the **canonical SEP evidence bundle**, verifiable offline by the published `@attested-intelligence/aga-verify` and the reference verifier `aga-receipt-spec/verify/verify-sep.mjs`. **Since 3.2.0 the verifier is algorithm-agile and ships a post-quantum profile:** v1 `Ed25519-SHA256-JCS` (the default the gateway emits) and v2 `ML-DSA-65+Ed25519-SHA256-JCS` (a NIST FIPS-204 ML-DSA-65 + RFC-8032 Ed25519 **composite**, both must verify), selected per-bundle by the `algorithm` field with a `VERIFIED / FAILED / UNSUPPORTED_PROFILE` trichotomy. Pre-3.0 releases (a legacy continuity-chain bundle that does *not* verify under the SEP verifier) are deprecated; use `^3.0.0`. Claim scope and residual attack surface are documented honestly in `THREAT_BOUNDARY.md`. **3.5.0 (2026-08-29) changes one behavior:** an artifact's TTL now *fails closed* — on expiry the portal terminates and a further measurement is refused, where earlier releases degraded and kept measuring. If you depend on the old post-expiry behavior, pin `3.3.3`. **3.6.0 (2026-09-18) changes no existing behavior:** it makes `aga-proxy` honour `AGA_GATEWAY_KEY` / `AGA_GATEWAY_KEY_FILE`, which it had silently ignored. See `CHANGELOG.md`.
11
11
 
12
12
  ```bash
13
13
  # This package IS the AGA MCP server (TypeScript, runs over stdio). Use it from any MCP client:
14
14
  npx -y @attested-intelligence/aga-mcp-server
15
15
  ```
16
16
 
17
- A Python companion SDK (`aga-governance`) is documented in the Python SDK section below.
17
+ A Python companion SDK (`aga-governance`) is documented in the Python SDK section below. Read the advisory at the top of that section before you install it.
18
18
 
19
19
  ## Verify this yourself (don't take our word)
20
20
 
@@ -28,11 +28,13 @@ node aga-receipt-spec/verify/verify-sep.mjs fixtures/valid_minimal.json # OVER
28
28
  node aga-receipt-spec/verify/verify-sep.mjs fixtures/tampered.json # OVERALL: FAILED
29
29
  ```
30
30
 
31
- The published `@attested-intelligence/aga-verify` CLI renders the identical verdict, and `npm run conformance:cross-stack` (first: `npm run build && npm --prefix independent-verifier run build`) proves six v1 verifier configurations, spanning **three independent toolchains (JavaScript, Go, and Python, including a pure-stdlib, no-third-party-crypto path)**, agree on all **57** cross-stack cases; `npm run conformance:cross-stack-v2` proves **two genuinely independent-language oracles (@noble/JS and CIRCL/Go)** agree on the v2 composite corpus. For a full trust-free reproduction (build the package yourself, reproduce the published tarball byte-for-byte, re-run every gate), see the **[REVIEWER_GUIDE.md](https://github.com/attestedintelligence/aga-mcp-server/blob/main/REVIEWER_GUIDE.md)** (a command-by-command self-service path), **[REPRODUCIBILITY.md](https://github.com/attestedintelligence/aga-mcp-server/blob/main/REPRODUCIBILITY.md)**, and the step-by-step **[SKEPTICAL_AUDITOR.md](https://github.com/attestedintelligence/aga-mcp-server/blob/main/SKEPTICAL_AUDITOR.md)**. This release carries SLSA build provenance, checkable with `npm audit signatures`.
31
+ The published `@attested-intelligence/aga-verify` CLI renders the identical verdict, and `npm run conformance:cross-stack` (first: `npm run build && npm --prefix independent-verifier run build`) proves **six v1 verifier configurations** spanning **three independent toolchains (JavaScript, Go, and Python, including a pure-stdlib, no-third-party-crypto path)** — agree on the **54 object-level cases**, and the **five file-parsing verifiers** agree on the **7 raw-byte/file-parse cases** (**61 total**). The in-server engine is library-only, receiving parsed objects rather than raw file bytes, so it does not run the file-parse cases; six configurations do not agree on all 61 and this no longer claims they do. `npm run conformance:cross-stack-v2` proves **two genuinely independent-language oracles (@noble/JS and CIRCL/Go)** agree on the v2 composite corpus. For a full trust-free reproduction (build the package yourself, reproduce the published tarball byte-for-byte, re-run every gate), see the **[REVIEWER_GUIDE.md](https://github.com/attestedintelligence/aga-mcp-server/blob/main/REVIEWER_GUIDE.md)** (a command-by-command self-service path), **[REPRODUCIBILITY.md](https://github.com/attestedintelligence/aga-mcp-server/blob/main/REPRODUCIBILITY.md)**, and the step-by-step **[SKEPTICAL_AUDITOR.md](https://github.com/attestedintelligence/aga-mcp-server/blob/main/SKEPTICAL_AUDITOR.md)**. This release carries SLSA build provenance, checkable with `npm audit signatures`.
32
32
 
33
33
  ## What This Does
34
34
 
35
- Every tool call an AI agent makes passes through the AGA gateway. Each call is evaluated against policy, and the decision (PERMITTED or DENIED) is recorded as a signed, hash-linked governance receipt. Receipts are collected into evidence bundles that any third party can verify offline using standard cryptography.
35
+ This is built for teams shipping agentic-AI products into financial services and insurance, at the moment a customer's vendor-risk, model-risk, or internal-audit review asks what your agent did and how anyone would know.
36
+
37
+ Tool calls routed through the AGA gateway are evaluated against the sealed policy, and each decision (PERMITTED or DENIED) is recorded as a signed, hash-linked governance receipt. Receipts are collected into evidence bundles that anyone holding the published format and the public key can verify offline, with no callback to us.
36
38
 
37
39
  **Record. Prove. Verify.**
38
40
 
@@ -59,6 +61,8 @@ Claude can then seal artifacts, measure integrity, generate evidence bundles, an
59
61
 
60
62
  By default the gateway signs with an **ephemeral** key that rotates on every restart. That is fine for a first look, but evidence-bundle provenance cannot be pinned across restarts (and the server warns about it on stderr). Set one stable 64-hex Ed25519 seed so provenance stays pinnable:
61
63
 
64
+ > **Since 3.6.0 this applies to both binaries.** `aga-proxy` reads the same two variables through the same resolver and prints the active public key at startup so you can pin it out of band; `--ephemeral` makes a throwaway key a stated choice. **In 3.5.0 and earlier `aga-proxy` ignored both variables silently** — a key you set had no effect and no warning was printed, so evidence from such a proxy is integrity-verifiable but not provenance-pinnable across restarts. See `DEPLOYMENT.md` §2.
65
+
62
66
  ```bash
63
67
  # generate a seed once (32 random bytes, hex)
64
68
  node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
@@ -168,6 +172,8 @@ A **separate** `aga-proxy export` invocation reads that file and fetches the sam
168
172
  npx -p @attested-intelligence/aga-mcp-server aga-proxy start \
169
173
  --upstream "npx -y @modelcontextprotocol/server-filesystem /tmp/test" --profile standard
170
174
 
175
+ # (First, drive at least one tools/call through the proxy from your MCP client — an empty
176
+ # ledger has no receipts to checkpoint, and the export reports there is nothing to export.)
171
177
  # Terminal B — export the live ledger from a different shell, then verify it offline
172
178
  npx -p @attested-intelligence/aga-mcp-server aga-proxy export -o evidence.json
173
179
  npx -y @attested-intelligence/aga-verify evidence.json --pubkey <gateway-public-key>
@@ -184,6 +190,8 @@ Three built-in policy profiles:
184
190
  - **standard** - rate limits + blocks destructive operations
185
191
  - **restrictive** - explicit tool allowlist, all unknown tools denied
186
192
 
193
+ Because the default (`permissive`) is audit-only, starting with an `audit_only` policy prints a loud stderr banner stating that every call is permitted and recorded and **no call is denied** in that mode — denial happens only under an allowlist-mode policy (`standard`, `restrictive`, or a custom `--policy` file). An unrecognized `--profile` value is a hard error (exit 2 listing the valid names), never a silent fallback to `permissive`.
194
+
187
195
  ## Verification _(canonical SEP 3.0; normative §6 algorithm in `aga-receipt-spec/verify/verify-sep.mjs`)_
188
196
 
189
197
  1. **Structural floor** - Bundle declares Ed25519-SHA256-JCS, public key well-formed (all small-order encodings + non-canonical `y ≥ p` rejected), `receipts.length > 0`, proof count = receipt count
@@ -216,8 +224,10 @@ curl https://aga-mcp-gateway.attested-intelligence.workers.dev/bundle -o evidenc
216
224
 
217
225
  ## Python SDK
218
226
 
227
+ > **Advisory, recomputed against PyPI on 2026-09-18: do not use the published Python verifier on untrusted input.** The latest release on the registry is `0.3.0` (uploaded 2026-08-29, not yanked). Its bundle verifier has no depth guard: on a deeply nested `receipts` payload it raises instead of returning a `FAILED` verdict, so a caller that treats an exception as anything other than a rejection reads a hostile bundle as unverified rather than refused. A corrected release is built and staged. **It is not on PyPI, so `pip install aga-governance` installs the affected version today.** Until a newer version appears at <https://pypi.org/project/aga-governance/>, treat this SDK as a recording and export client only, and verify with the JavaScript reference verifier (`aga-receipt-spec/verify/verify-sep.mjs`) or the published `@attested-intelligence/aga-verify` CLI. Recheck the registry before you rely on any of this.
228
+
219
229
  ```bash
220
- pip install aga-governance
230
+ pip install aga-governance # installs 0.3.0 today; see the advisory above
221
231
  ```
222
232
 
223
233
  ```python
@@ -231,7 +241,7 @@ with AgentSession(gateway_id="my-gateway") as session:
231
241
  request_id="req-1",
232
242
  )
233
243
  bundle = session.export_bundle()
234
- result = session.verify()
244
+ result = session.verify() # see the advisory: not for untrusted input on 0.3.0
235
245
  assert result["overall_valid"]
236
246
  ```
237
247
 
@@ -239,9 +249,9 @@ with AgentSession(gateway_id="my-gateway") as session:
239
249
 
240
250
  Automated tests across TypeScript and Python, plus a conformance corpus:
241
251
 
242
- - **TypeScript MCP server:** 384 automated tests (vitest), including provable-denial and behavioral-monitor regressions
252
+ - **TypeScript MCP server:** 428 automated tests (vitest), including provable-denial and behavioral-monitor regressions
243
253
  - **SEP conformance corpus:** `npm run test:conformance` (valid → VERIFIED, negatives → FAILED)
244
- - **Python companion SDK:** the separately-published `aga-governance` PyPI package (install + smoke-checked here; its full pytest suite runs from the source tree)
254
+ - **Python companion SDK:** the separately-published `aga-governance` PyPI package (install + smoke-checked here; its full pytest suite runs from the source tree). The smoke check imports the package and prints its version. It does not exercise the verifier, which on the published 0.3.0 is subject to the advisory in the Python SDK section.
245
255
 
246
256
  ```bash
247
257
  npm test # TypeScript tests (vitest)
@@ -251,7 +261,7 @@ pip install aga-governance && python -c "import aga; print(aga.__version__)" #
251
261
 
252
262
  ## Benchmarks
253
263
 
254
- Receipt-format determinism is reproducible here: `npm test` runs the cross-language vectors, and `npm run conformance:cross-stack` (first: `npm run build && npm --prefix independent-verifier run build`) shows the six v1 verifier configurations (across three independent toolchains: JS, Go, Python) agree on the canonical 57-case corpus, while `npm run conformance:cross-stack-v2` shows the two independent-language v2 oracles agree on the composite corpus.
264
+ Receipt-format determinism is reproducible here: `npm test` runs the cross-language vectors, and `npm run conformance:cross-stack` (first: `npm run build && npm --prefix independent-verifier run build`) shows the six v1 verifier configurations (across three independent toolchains: JS, Go, Python) agree on the 54 object-level cases of the canonical 61-case corpus — the remaining 7 are raw-byte/file-parse cases run by the five file-parsing verifiers, since the in-server engine never receives raw bytes — while `npm run conformance:cross-stack-v2` shows the two independent-language v2 oracles agree on the composite corpus.
255
265
 
256
266
  ## Project Structure
257
267
 
@@ -260,11 +270,11 @@ src/
260
270
  sep/ # Canonical SEP evidence engine: single source of truth (canon, merkle, receipt, checkpoint, bundle, verify)
261
271
  core/ # Governance primitives (portal, artifact, attestation, disclosure, delegation, behavioral) + internal continuity-chain profile
262
272
  crypto/ # Internal continuity-chain crypto: Ed25519 (node:crypto), SHA-256/blake2b, salt
263
- proxy/ # MCP governance proxy (transparent interception + policy enforcement; emits SEP bundles)
273
+ proxy/ # MCP governance proxy (transparent interception + policy evaluation; emits SEP bundles)
264
274
  middleware/ # Governance PEP wrapper (records a signed PERMITTED/DENIED receipt per governed call)
265
275
  independent-verifier/ # @attested-intelligence/aga-verify: standalone SEP verifier, zero AGA imports
266
276
  scenarios/ # Demo scenarios (SCADA, autonomous vehicle, AI agent) that emit SEP bundles
267
- tests/ # TypeScript test suite (384 automated tests)
277
+ tests/ # TypeScript test suite (428 automated tests)
268
278
  ```
269
279
 
270
280
  ## Links
@@ -276,6 +286,9 @@ tests/ # TypeScript test suite (384 automated tests)
276
286
  - [Diligence Materials](https://attestedintelligence.com/diligence)
277
287
  - [MCP Server (npm)](https://www.npmjs.com/package/@attested-intelligence/aga-mcp-server)
278
288
  - [Python SDK (PyPI)](https://pypi.org/project/aga-governance/)
289
+ - [Changelog](https://github.com/attestedintelligence/aga-mcp-server/blob/main/CHANGELOG.md)
290
+ - [Threat boundary](https://github.com/attestedintelligence/aga-mcp-server/blob/main/THREAT_BOUNDARY.md)
291
+ - [Deployment guide](https://github.com/attestedintelligence/aga-mcp-server/blob/main/DEPLOYMENT.md)
279
292
 
280
293
  ## Security
281
294
 
@@ -46,11 +46,11 @@ Claim scope is **integrity-of-present-receipts, NOT non-omission**: a PASS prove
46
46
  1. **Proxy — mandatory mediation is a deployment property.** `GovernanceProxy` governs the path *through it*; if the agent can reach the upstream MCP server **directly** (network), it bypasses governance entirely. `upstreamUrl` (HTTP) is directly reachable. → **Deployment requirement:** the agent must be network-isolated so the proxy is its *only* route to tools. Documented, not code-enforceable here. *(Empirically demonstrated in CP3 A7 — a direct POST to the upstream fired a side effect with no receipt; the red-team scripts are maintained in the private validation tree, not shipped in this package.)* **See `DEPLOYMENT.md` §1: stdio-upstream is the hardened default — the upstream is a non-network-reachable child process, closing this bypass by construction; HTTP-upstream mode now warns on stderr.**
47
47
  2. **Proxy — only `method === "tools/call"` is policy-evaluated; partially mitigated in 3.0.** Other JSON-RPC methods are still forwarded (not policy-evaluated), but they are now recorded as **signed passthrough receipts** in the evidence bundle for auditability — so a side-effecting non-`tools/call` method is no longer *invisible* (the CP3 A7 `admin/deleteAllData` would now appear as a `(passthrough)` receipt with `method` set and `reason: forwarded, not policy-evaluated`). Benign protocol methods (`initialize`, `ping`, `tools/list`, `*/list`, `notifications/*`, …) are excluded by default; an optional `denyMethods` denylist rejects known side-effecting methods (a **DENIED** passthrough receipt, not forwarded). **What this closes:** invisibility — every forwarded non-benign method is now in the tamper-evident, checkpoint-bound bundle. **What it does NOT close:** the method is not policy-evaluated and still *executes* unless explicitly denylisted (this is *visibility, not governance*), and a direct-to-upstream call (§3.1) still bypasses the proxy entirely. Full per-method policy evaluation remains future work.
48
48
  3. **Proxy — denial is observable.** A DENIED call returns a distinctive `-32600` error, which an adversary can use as a policy-probing oracle. Low severity; structured denial metadata could be tightened later.
49
- 4. **Key persistence — mitigated in 3.0.** The gateway signing key can be **persisted** via `AGA_GATEWAY_KEY` (64-hex seed) or `AGA_GATEWAY_KEY_FILE` (`DEPLOYMENT.md` §2), so `gateway_public_key` is stable and **pinnable across restarts**. The default remains an ephemeral key (warned on stderr). HSM/KMS backing remains future work. A verifier MUST still pin the key it expects.
49
+ 4. **Key persistence — available on BOTH binaries as of 3.6.0, and mitigated only where an operator configures it.** The gateway signing key **can be** persisted via `AGA_GATEWAY_KEY` (64-hex seed) or `AGA_GATEWAY_KEY_FILE` (`DEPLOYMENT.md` §2), which makes `gateway_public_key` stable and **pinnable across restarts**. The default on both binaries is still an ephemeral key, so an unconfigured deployment carries this risk in full — availability of the control is not the same as the control being in effect, and nothing here detects a deployment that skipped it. Both binaries resolve the key through one shared function and warn identically on stderr when they fall back, and `aga-proxy --ephemeral` makes a throwaway key a stated choice rather than a silent one. The proxy also prints its active public key at startup, which is what makes an honest out-of-band pin possible. **Through 3.5.0 this was NOT true of `aga-proxy`:** it called `generateSigner()` unconditionally, read neither variable, and emitted no warning — so an operator who set the variable correctly was silently ignored and their proxy-issued bundles were not pinnable. If you are running 3.5.0 or earlier, treat proxy evidence as integrity-verifiable only. HSM/KMS backing remains future work. A verifier MUST still pin the key it expects — **and MUST obtain that key independently of the bundle**, since a key lifted from the bundle under test yields a circular `provenance verified` (see §3.7).
50
50
  5. **Storage durability (Sprint 5).** Default storage is in-memory → the live chain is lost on **process restart**. The cryptographic record survives via the **exported signed bundle**; durable cross-restart retention needs the SQLite/persistent backend (Sprint 5). The raw quarantine forensic buffer is in-memory by design (only the `arguments_hash` commitment is signed — privacy-preserving and sufficient to *prove* a capture).
51
51
  6. **Out of scope entirely (not what AGA does).** AGA does not prevent: model jailbreaks, model-weight theft, credential compromise, or infrastructure compromise. It provides *accountability and provenance* for governed decisions, not prevention of those classes. If an attacker holds the gateway signing key, they can author receipts — protect the key (Sprint 4).
52
52
  7. **Verifier-UX / unpinned consumers (NEW — CP3 A5).** A consumer that verifies a bundle **without pinning** the gateway key gets an integrity-only `VERIFIED` with `issuerVerified=false` — *even on a forged, attacker-signed, denial-free bundle*. This is correct (integrity-of-present-receipts ≠ provenance, and the result object/CLI say so explicitly), but a UI that shows a bare "VERIFIED" without prominently propagating `issuerVerified=false` could mislead a non-expert. → Downstream consumers (esp. the website demo) MUST pin the gateway key and never present an unpinned PASS as proof of *who* issued the bundle. **Mitigated in 3.0:** the verify result now carries a prominent `summary` — `VERIFIED (provenance verified …)` vs `VERIFIED (integrity only — NOT provenance …)` — surfaced by `verify_bundle_offline` and the reference/`aga-verify` CLIs; key-pinning ergonomics are in `DEPLOYMENT.md` §2. Consumers must still pin.
53
- 8. **Cross-stack verifier conformance (CLOSED — 2026-06-07).** Earlier in the 3.0 hardening only `src/sep/verify.ts` carried the full strict floor; the reference `verify-sep.mjs`, the published `aga-verify`, Go, and Python lagged. **That asymmetry is now closed.** All six verifiers — engine (`src/sep`), reference (`verify-sep.mjs`), `aga-verify`, Go (`verify.go`), and Python (audited library + pure-stdlib) — apply the identical strict floor and return **byte-identical verdicts**. The shared floor: strict field allowlist; `envelope_consistency` (binds the unsigned `gateway_id`/`merkle_root`/`generated_at` to the signed/recomputed values); checkpoint-algorithm binding; lexicographic-string canonicalization with RFC-8785 integral-number normalization; complete small-order/non-canonical-key rejection; **one library-free canonical-timestamp rule** (exact `.sssZ` UTC form via an ASCII regex + integer-arithmetic calendar + lexicographic ordering — no native date parser); merkle-direction-token strictness (`left`/`right` only, length-matched); unpaired-UTF-16-surrogate rejection; depth-bounded never-throw; and whole-document parse (trailing content rejected). Verified by `npm run conformance:cross-stack` — six verifiers agree on every case in the committed corpus (57 cases incl. raw-byte/file-parse, incl. an uppercase-Merkle-sibling cross-stack case), confirmed across multiple rounds of independent blind differential re-audit. See `fixtures/cross-stack/README.md`. **Residual (by design, not a divergence):** the bundle envelope still carries four *unsigned* metadata fields with no signed counterpart — `bundle_id`, `schema_version`, `policy_reference`, `offline_capable`. They are informational and are **not** security-identity fields (the identity fields `gateway_id`/`merkle_root`/`generated_at` ARE bound); a relying party must still trust only signed/verified values and pin the gateway key (§3.7). Of the four, **`policy_reference` is the only identity-grade one** — but the governing policy IS captured and cryptographically verified inside **every signed receipt's own `policy_reference` field** (one of the 15 signed fields), so the unsigned *envelope* `policy_reference` is only a convenience mirror, not the source of truth. Binding the envelope copy is a recommended near-term (3.1) format revision; a coordinated verifier-output flag that marks these four envelope fields as unsigned/not-verified is a recommended enhancement, deliberately deferred here to avoid adding an untested cross-language output surface immediately after the cross-stack-consistency campaign (the verdict surface already attests only the steps it runs, and never claims these fields). One further residual is **unreachable by construction:** SEP signed fields are strings plus the single integer `leaf_count` (the emit guard `assertSignedReceiptFieldsAreStrings` forbids any other number in a signed field), so a verifier's canonicalization of a *non-integer / exponential* number placed in a signed field — which no conformant gateway can emit — is intentionally left unspecified and MAY differ across language stdlibs; it cannot affect any bundle a conformant gateway produces. Reachable string content is fully cross-stack-consistent (an exhaustive 0..0x10FFFF code-point sweep confirms the only ever-divergent characters, `U+2028`/`U+2029`, are normalized identically by every verifier).
53
+ 8. **Cross-stack verifier conformance (CLOSED — 2026-06-07).** Earlier in the 3.0 hardening only `src/sep/verify.ts` carried the full strict floor; the reference `verify-sep.mjs`, the published `aga-verify`, Go, and Python lagged. **That asymmetry is now closed.** All six verifiers — engine (`src/sep`), reference (`verify-sep.mjs`), `aga-verify`, Go (`verify.go`), and Python (audited library + pure-stdlib) — apply the identical strict floor and return **byte-identical verdicts**. The shared floor: strict field allowlist; `envelope_consistency` (binds the unsigned `gateway_id`/`merkle_root`/`generated_at` to the signed/recomputed values); checkpoint-algorithm binding; lexicographic-string canonicalization with RFC-8785 integral-number normalization; complete small-order/non-canonical-key rejection; **one library-free canonical-timestamp rule** (exact `.sssZ` UTC form via an ASCII regex + integer-arithmetic calendar + lexicographic ordering — no native date parser); merkle-direction-token strictness (`left`/`right` only, length-matched); unpaired-UTF-16-surrogate rejection; depth-bounded never-throw; and whole-document parse (trailing content rejected). Verified by `npm run conformance:cross-stack` — six verifier configurations agree on the 54 object-level cases of the committed corpus, and the five file-parsing verifiers agree on its 7 raw-byte/file-parse cases (61 total, incl. an uppercase-Merkle-sibling cross-stack case). The agreement itself was confirmed across multiple rounds of independent blind differential re-audit. The engine is library-only and never receives raw file bytes, so it does not run the file-parse subset; the earlier phrasing here ("six verifiers agree on every case … 57 cases incl. raw-byte/file-parse") overclaimed by one verifier on those seven and carried a stale total. See `fixtures/cross-stack/README.md`. **Residual (by design, not a divergence):** the bundle envelope still carries four *unsigned* metadata fields with no signed counterpart — `bundle_id`, `schema_version`, `policy_reference`, `offline_capable`. They are informational and are **not** security-identity fields (the identity fields `gateway_id`/`merkle_root`/`generated_at` ARE bound); a relying party must still trust only signed/verified values and pin the gateway key (§3.7). Of the four, **`policy_reference` is the only identity-grade one** — but the governing policy IS captured and cryptographically verified inside **every signed receipt's own `policy_reference` field** (one of the 15 signed fields), so the unsigned *envelope* `policy_reference` is only a convenience mirror, not the source of truth. Binding the envelope copy is a recommended near-term (3.1) format revision; a coordinated verifier-output flag that marks these four envelope fields as unsigned/not-verified is a recommended enhancement, deliberately deferred here to avoid adding an untested cross-language output surface immediately after the cross-stack-consistency campaign (the verdict surface already attests only the steps it runs, and never claims these fields). One further residual is **unreachable by construction:** SEP signed fields are strings plus the single integer `leaf_count` (the emit guard `assertSignedReceiptFieldsAreStrings` forbids any other number in a signed field), so a verifier's canonicalization of a *non-integer / exponential* number placed in a signed field — which no conformant gateway can emit — is intentionally left unspecified and MAY differ across language stdlibs; it cannot affect any bundle a conformant gateway produces. Reachable string content is fully cross-stack-consistent (an exhaustive 0..0x10FFFF code-point sweep confirms the only ever-divergent characters, `U+2028`/`U+2029`, are normalized identically by every verifier).
54
54
 
55
55
  ---
56
56
 
@@ -74,4 +74,4 @@ A focused adversarial workflow ran **7 independent red-team attacks** on the cor
74
74
 
75
75
  ## 5. Net boundary statement (for public copy)
76
76
 
77
- > Every governed decision produces a signed, chained, checkpoint-bound receipt; denials cannot be silently erased by the agent through tampering, truncation, reordering, or re-attestation, and are independently verifiable offline with provenance. The behavioral monitor is detective-only by default. Mandatory mediation (network isolation), cross-session key persistence, and cross-restart durability are deployment/roadmap properties, documented above. AGA proves *what was governed*; it does not claim to prevent jailbreaks, key theft, or non-`tools/call` side channels.
77
+ > Every governed decision produces a signed, chained, checkpoint-bound receipt; denials cannot be silently erased by the agent through tampering, truncation, reordering, or re-attestation, and can be verified offline against the published format, with provenance only when the gateway key is pinned out of band. The verifier to run for a verdict is the published `@attested-intelligence/aga-verify` CLI; the in-repo reference implementations that the cross-stack conformance suite exercises are described in the README. The behavioral monitor is detective-only by default. Mandatory mediation (network isolation) and cross-restart ledger durability are deployment/roadmap properties, documented above; cross-session key persistence is available on both binaries as of 3.6.0 but only takes effect once an operator configures it, and only helps a verifier who obtains the key out of band. AGA proves *what was governed*; it does not claim to prevent jailbreaks, key theft, or non-`tools/call` side channels.
@@ -1 +1 @@
1
- {"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../src/core/portal.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,gBAAgB,EAAE,OAAO,CAAC;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,MAAM;IACjB,KAAK,EAAE,WAAW,CAAoB;IACtC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAQ;IACvC,eAAe,SAAK;IACpB,YAAY,EAAE,OAAO,GAAG,IAAI,CAAQ;IACpC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAa;IACrC,cAAc,EAAE,gBAAgB,EAAE,CAAM;IAExC,YAAY,CAAC,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAiB5F,OAAO,CAAC,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB;IAuC3E,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAUxC,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAKhC,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAEtC,KAAK,IAAI,IAAI;CAId"}
1
+ {"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../src/core/portal.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,gBAAgB,EAAE,OAAO,CAAC;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,MAAM;IACjB,KAAK,EAAE,WAAW,CAAoB;IACtC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAQ;IACvC,eAAe,SAAK;IACpB,YAAY,EAAE,OAAO,GAAG,IAAI,CAAQ;IACpC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAa;IACrC,cAAc,EAAE,gBAAgB,EAAE,CAAM;IAExC,YAAY,CAAC,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAiB5F,OAAO,CAAC,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB;IAsD3E,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAUxC,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAKhC,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAEtC,KAAK,IAAI,IAAI;CAId"}
@@ -42,11 +42,26 @@ export class Portal {
42
42
  const empty = { currentBytesHash: '', currentMetaHash: '',
43
43
  expectedBytesHash: this.artifact.subject_identifier.bytes_hash,
44
44
  expectedMetaHash: this.artifact.subject_identifier.metadata_hash };
45
- // Graceful degradation: TTL expiry -> SAFE_STATE + DEGRADATION event + continued logging
45
+ // FAIL CLOSED on TTL expiry (founder decision D1, ruled 2026-08-29).
46
+ //
47
+ // This branch previously set SAFE_STATE and kept accepting measurements, so an expired artifact
48
+ // went on being measured indefinitely and every public page describing TTL as an enforcement
49
+ // boundary was describing a behavior the code did not have. It now terminates, which is exactly
50
+ // what the revocation branch immediately below already does — TTL was the odd one out, not the
51
+ // new behavior.
52
+ //
53
+ // Consequence, deliberate: the entry guard above throws on TERMINATED, so the FIRST post-expiry
54
+ // measurement returns this result and any SUBSEQUENT call throws 'Portal is terminated'. That is
55
+ // the point of failing closed — an expired artifact stops being a usable measurement channel and
56
+ // re-attestation is required to get one back. Callers that previously looped on measure() past
57
+ // expiry must handle the throw.
58
+ //
59
+ // The degradation entry is still recorded. It is the forensic record of WHY termination happened,
60
+ // and dropping it would trade one honesty problem for another.
46
61
  const ttl_ok = !isExpired(this.artifact.issued_timestamp, this.artifact.enforcement_parameters.ttl_seconds);
47
62
  if (!ttl_ok) {
48
63
  const prevState = this.state;
49
- this.state = 'SAFE_STATE';
64
+ this.state = 'TERMINATED';
50
65
  this.degradationLog.push({
51
66
  reason: 'TTL_EXPIRED',
52
67
  timestamp: utcNow(),
@@ -1 +1 @@
1
- {"version":3,"file":"portal.js","sourceRoot":"","sources":["../../src/core/portal.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"portal.js","sourceRoot":"","sources":["../../src/core/portal.ts"],"names":[],"mappings":"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"}
@@ -88,12 +88,18 @@ export declare function fetchBundleViaControl(loc: ControlLocator): Promise<unkn
88
88
  * live proxy over its loopback control channel. Never writes an empty/placeholder bundle — if no
89
89
  * live proxy can be reached it throws {@link ExportUnavailableError}.
90
90
  */
91
+ export declare class ExportTargetExistsError extends Error {
92
+ readonly output: string;
93
+ constructor(output: string);
94
+ }
91
95
  export declare function exportBundleToFile(opts: {
92
96
  proxy: {
93
97
  exportBundle(): unknown;
94
98
  } | null;
95
99
  dataDir: string;
96
100
  output: string;
101
+ /** RC9-06 / D-21.7: replacement is opt-in only. Default refuses an existing destination. */
102
+ force?: boolean;
97
103
  writeFile?: (filePath: string, data: string) => void;
98
104
  }): Promise<{
99
105
  source: 'in-process' | 'control-channel';
@@ -1 +1 @@
1
- {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../src/proxy/control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,qEAAqE;AACrE,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,oGAAoG;AACpG,eAAO,MAAM,oBAAoB,QAAQ,CAAC;AAC1C,oGAAoG;AACpG,eAAO,MAAM,cAAc,kBAAkB,CAAC;AAC9C,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAUpD;AAED,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,YAAY,IAAI,OAAO,CAAC;IACxB,SAAS,IAAI,OAAO,CAAC;IACrB,WAAW,IAAI,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;gBAEhC,MAAM,EAAE,kBAAkB;IAItC,8FAA8F;IACxF,KAAK,CAAC,IAAI,GAAE,MAA6B,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAmB5F,wFAAwF;IACxF,OAAO,IAAI,MAAM;IAKjB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,OAAO,CAAC,MAAM;IAgCd,OAAO,CAAC,IAAI;IAQZ,qCAAqC;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAO5B;AAID,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAE3E;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAYtE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAMvD;AAID,6FAA6F;AAC7F,eAAO,MAAM,gBAAgB,8EACgD,CAAC;AAE9E,sFAAsF;AACtF,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,GAAE,MAAyB;CAI/C;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CA4BjF;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,KAAK,EAAE;QAAE,YAAY,IAAI,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACtD,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,YAAY,GAAG,iBAAiB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CAmB9F"}
1
+ {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../src/proxy/control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,qEAAqE;AACrE,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,oGAAoG;AACpG,eAAO,MAAM,oBAAoB,QAAQ,CAAC;AAC1C,oGAAoG;AACpG,eAAO,MAAM,cAAc,kBAAkB,CAAC;AAC9C,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAUpD;AAED,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,YAAY,IAAI,OAAO,CAAC;IACxB,SAAS,IAAI,OAAO,CAAC;IACrB,WAAW,IAAI,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;gBAEhC,MAAM,EAAE,kBAAkB;IAItC,8FAA8F;IACxF,KAAK,CAAC,IAAI,GAAE,MAA6B,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAmB5F,wFAAwF;IACxF,OAAO,IAAI,MAAM;IAKjB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,OAAO,CAAC,MAAM;IAgCd,OAAO,CAAC,IAAI;IAQZ,qCAAqC;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAO5B;AAID,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAE3E;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAYtE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAMvD;AAID,6FAA6F;AAC7F,eAAO,MAAM,gBAAgB,8EACgD,CAAC;AAE9E,sFAAsF;AACtF,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,GAAE,MAAyB;CAI/C;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CA4BjF;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aACpB,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM;CAQ3C;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,KAAK,EAAE;QAAE,YAAY,IAAI,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACtD,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,YAAY,GAAG,iBAAiB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CA+B9F"}
@@ -222,8 +222,32 @@ export async function fetchBundleViaControl(loc) {
222
222
  * live proxy over its loopback control channel. Never writes an empty/placeholder bundle — if no
223
223
  * live proxy can be reached it throws {@link ExportUnavailableError}.
224
224
  */
225
+ export class ExportTargetExistsError extends Error {
226
+ output;
227
+ constructor(output) {
228
+ super(`Refusing to overwrite an existing file: ${output}\n` +
229
+ `Export writes with exclusive-create by default so it can never destroy an operator's file.\n` +
230
+ `Choose a different --output path, or pass --force to replace this file deliberately.`);
231
+ this.output = output;
232
+ this.name = 'ExportTargetExistsError';
233
+ }
234
+ }
225
235
  export async function exportBundleToFile(opts) {
226
- const write = opts.writeFile ?? ((p, d) => fs.writeFileSync(p, d));
236
+ // RC9-06 (founder ruling D-21.7): this previously called fs.writeFileSync unconditionally, so
237
+ // `export --output <existing-file>` silently TRUNCATED whatever the operator pointed at — a
238
+ // config, a key file, a prior bundle — and exited 0 reporting success. No attacker and no
239
+ // unusual deployment required, on the exact artifact a verifier consumes. Default is now
240
+ // exclusive-create ('wx', which fails if the path exists); replacement requires --force.
241
+ const write = opts.writeFile ?? ((p, d) => {
242
+ try {
243
+ fs.writeFileSync(p, d, opts.force ? undefined : { flag: 'wx' });
244
+ }
245
+ catch (e) {
246
+ if (!opts.force && e?.code === 'EEXIST')
247
+ throw new ExportTargetExistsError(p);
248
+ throw e;
249
+ }
250
+ });
227
251
  let bundle;
228
252
  let source;
229
253
  if (opts.proxy) {