@ainra/mcp 0.5.0 → 0.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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/package.json +2 -5
  3. package/src/tools.mjs +16 -30
package/README.md CHANGED
@@ -41,8 +41,9 @@ freshness class and audience. Nothing a presenter put in the bundle is believed.
41
41
 
42
42
  > **In 0.4.1 this tool did not do that.** It replayed the bundle through the fixture-semantics verifier with the
43
43
  > bundle's own clock, freshness class and status list, so a revoked or expired presenter could be reported `valid`
44
- > (D-074). Upgrade. Replaying a conformance vector is still possible and is now a separate, labelled mode:
45
- > `anchors` plus `fixture: true`, which returns `decision: null`.
44
+ > (D-074). Upgrade. In 0.5.0 replay was a separate, labelled mode (`anchors` plus `fixture: true`); since 0.6.0 the
45
+ > tool has no replay mode at all and every answer is a decision. To replay a conformance vector, use
46
+ > `@ainra/sdk/conformance`.
46
47
 
47
48
  `ainra_lookup` resolves a name to its public record; `ainra_status` reports the target's head — network, root label,
48
49
  registrar count, freshness window.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainra/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "AINRA as native agent tools — a zero-dependency MCP server wrapping the published verifier, the public record, and the registrar you control.",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "type": "module",
@@ -17,10 +17,7 @@
17
17
  "src/"
18
18
  ],
19
19
  "dependencies": {
20
- "@ainra/sdk": "*"
21
- },
22
- "peerDependencies": {
23
- "@ainra/sdk": "*"
20
+ "@ainra/sdk": "^0.6.0"
24
21
  },
25
22
  "homepage": "https://ainra.org/",
26
23
  "repository": {
package/src/tools.mjs CHANGED
@@ -9,7 +9,7 @@
9
9
  import { readFileSync, existsSync } from "node:fs";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { execFileSync } from "node:child_process";
12
- import { runVector, Verifier, verdictEvent, serializeVerdictEvent, linkFromIndex, jwkThumbprintEd25519, strictB64u } from "@ainra/sdk"; // file:../sdk-ts in a checkout; ^x.y.z when published
12
+ import { Verifier, verdictEvent, serializeVerdictEvent, linkFromIndex, presentedKeyThumbprint, strictB64u } from "@ainra/sdk"; // file:../sdk-ts in a checkout; ^x.y.z when published
13
13
  export { verdictEvent, serializeVerdictEvent }; // one event shape across CLI, middleware, MCP (docs/PRESENTATION.md)
14
14
 
15
15
  const ROOT = fileURLToPath(new URL("../../../", import.meta.url));
@@ -100,7 +100,7 @@ export const TOOLS = [
100
100
  {
101
101
  name: "ainra_verify",
102
102
  title: "Verify an AINRA passport",
103
- description: "Decide whether a presentation someone handed you verifies. Pass the `presentation` with the signed `directory` and the two `roots` you trust (omit them and a URL target's own are fetched): the directory is checked against both roots, the status list against the registrar's status key, and the clock, the freshness class and the audience are YOURS — nothing a presenter put in the bundle is believed. Returns `decision` (accept or refuse), the verdict, and the named reason in plain words. Read-only. If the bundle carries an ADR-019 instance credential (a RUNNING COPY of an agent), pass your own `audience`; an empty audience refuses every instance credential. To replay a conformance VECTOR instead — which carries its own anchors, clock and status on purpose — pass `anchors` with `fixture: true`; that mode believes the bundle and decides nothing.",
103
+ description: "Decide whether a presentation someone handed you verifies. Pass the `presentation` with the signed `directory` and the two `roots` you trust (omit them and a URL target's own are fetched): the directory is checked against both roots, the status list against the registrar's status key, and the clock, the freshness class and the audience are YOURS — nothing a presenter put in the bundle is believed. Returns `decision` (accept or refuse), the verdict, and the named reason in plain words. Read-only. If the bundle carries an ADR-019 instance credential (a RUNNING COPY of an agent), pass your own `audience`; an empty audience refuses every instance credential. (Replaying a conformance vector is not this tool's job: use `@ainra/sdk/conformance`.)",
104
104
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
105
105
  inputSchema: {
106
106
  type: "object",
@@ -111,8 +111,6 @@ export const TOOLS = [
111
111
  audience: { type: "string", description: "YOUR audience (ADR-019), e.g. https://api.example. Required to accept an instance credential; a presenter can never supply it. Omit for plain passport bundles." },
112
112
  freshness: { type: "string", enum: ["F1", "F2", "F3"], description: "How old a status publication you accept: F1 30 s, F2 5 min (default), F3 24 h. Yours, never the bundle's." },
113
113
  now: { type: "integer", description: "Your clock, unix seconds. Defaults to this server's clock. Never read from the bundle." },
114
- anchors: { type: "object", description: "FIXTURE MODE ONLY: a conformance vector's own trust anchors (issuer_key + log_root_key per registrar). Requires `fixture: true`." },
115
- fixture: { type: "boolean", description: "Replay a conformance vector with fixture semantics: the bundle's own clock, freshness class and status list are believed. Decides nothing about a real presenter." },
116
114
  },
117
115
  required: ["presentation"],
118
116
  },
@@ -138,28 +136,11 @@ export const TOOLS = [
138
136
  };
139
137
  };
140
138
 
141
- // ── fixture mode: a conformance vector, replayed. Explicit, labelled, and never a decision (D-074). ──────
142
- if (input.anchors !== undefined && input.directory === undefined && input.roots === undefined) {
143
- if (input.fixture !== true)
144
- throw new Error(
145
- "ainra_verify with `anchors` replays a conformance vector: it believes the bundle's own clock, freshness " +
146
- "class and status list, so a revoked or expired presenter could make it say valid. To decide whether to " +
147
- "trust a presentation, pass the signed `directory` and the `roots` instead. To replay a vector on " +
148
- "purpose, add `fixture: true`. Nothing was verified.");
149
- // The audience is the CALLER's even here; everything else is the vector's, which is what a vector is for.
150
- const presentation = { ...input.presentation, audience: input.audience ?? "" };
151
- const v = runVector({ name: "mcp", expect: {}, anchors: input.anchors, presentation });
152
- return {
153
- mode: "fixture",
154
- decision: null,
155
- warning: "FIXTURE SEMANTICS: the bundle's own clock, freshness class and status list were believed. This replays a conformance vector; it does not decide whether to trust a presenter.",
156
- verdict: v.verdict,
157
- reason: v.reason ?? null,
158
- explanation: gloss(v),
159
- event: verdictEvent(presentation, v, presentation?.now ?? 0),
160
- instance: instanceReport(presentation, v, presentation.audience),
161
- };
162
- }
139
+ // Fixture mode was removed in 0.6.0 (PLAN-M48 step 7). It replayed a vector by believing the bundle's own clock,
140
+ // class and status list; an agent that reached for it to decide trust would have been told a revoked presenter
141
+ // was valid (D-074). Conformance replay lives in `@ainra/sdk/conformance`, where the import says what it is.
142
+ if (input.anchors !== undefined || input.fixture !== undefined)
143
+ throw new Error("ainra_verify no longer replays conformance vectors (fixture mode was removed in 0.6.0): it only decides, against a signed `directory` and its `roots`. To replay a vector, use `@ainra/sdk/conformance` or `ainra verify`. Nothing was verified.");
163
144
 
164
145
  // ── gate mode: what a verifier decides. The directory against both roots; the status against the registrar's
165
146
  // status key; the clock, the class and the audience from the CALLER. ─────────────────────────────────────────
@@ -196,12 +177,12 @@ export const TOOLS = [
196
177
  {
197
178
  name: "ainra_verify_link",
198
179
  title: "Who stands behind this agent key?",
199
- description: "AINRA Link (D-078): after you have verified a request's signature with an agent's Ed25519 key, ask which accountable operator stands behind that key and whether the link is still good. Pass the key (`key`, base64url), and either the `link` as a gate receives it, or the registrar's link `index` (GET /links) and its signed `status` publication (GET /status-list), from which the link for your key is found. Trust is YOURS: the signed `directory` and the two `roots` (omit both to use AINRA_TARGET's). The status list is authenticated before it is read; the clock, freshness class and capability are yours. Returns `decision` (accept or refuse), the operator and passport on accept, or the named reason. Read-only. A key with no link at all is refused as not_linked.",
180
+ description: "AINRA Link (D-078): after you have verified a signature with an agent's key — Ed25519, or secp256k1 as ES256K (D-082) — ask which accountable operator stands behind that key and whether the link is still good. Pass the key (`key`, base64url), and either the `link` as a gate receives it, or the registrar's link `index` (GET /links) and its signed `status` publication (GET /status-list), from which the link for your key is found. Trust is YOURS: the signed `directory` and the two `roots` (omit both to use AINRA_TARGET's). The status list is authenticated before it is read; the clock, freshness class and capability are yours. Returns `decision` (accept or refuse), the operator and passport on accept, or the named reason. Read-only. A key with no link at all is refused as not_linked.",
200
181
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
201
182
  inputSchema: {
202
183
  type: "object",
203
184
  properties: {
204
- key: { type: "string", description: "The Ed25519 public key (base64url, 32 bytes) you ALREADY verified the request's signature with." },
185
+ key: { type: "string", description: "The public key (base64url) you ALREADY verified the signature with: 32 bytes for Ed25519, the 33-byte compressed point for ES256K (secp256k1)." },
205
186
  link: { type: "object", description: "The link as a gate receives it (an index entry with the checkpoint and the signed status publication)." },
206
187
  index: { type: "object", description: "Instead of `link`: the registrar's whole link index (GET /links)." },
207
188
  status: { type: "object", description: "With `index`: the registrar's signed status publication (GET /status-list)." },
@@ -215,11 +196,16 @@ export const TOOLS = [
215
196
  },
216
197
  async handler(input) {
217
198
  const keyBytes = typeof input.key === "string" ? strictB64u(input.key) : null;
218
- if (!keyBytes || keyBytes.length !== 32) throw new Error("`key` must be a base64url Ed25519 public key (32 bytes). Nothing was verified.");
199
+ if (!keyBytes) throw new Error("`key` must be base64url. Nothing was verified.");
200
+ // By length, as every verifier: 32 bytes Ed25519, 33 a compressed secp256k1 point (D-082). Any other key is
201
+ // refused by name, as the core refuses it — not an input error, a decision.
202
+ if (keyBytes.length !== 32 && keyBytes.length !== 33)
203
+ return { decision: "refuse", verdict: "invalid", reason: "schema_violation" };
219
204
  let link = input.link;
220
205
  if (link === undefined) {
221
206
  if (!input.index || !input.status) throw new Error("pass either `link`, or both `index` (GET /links) and `status` (GET /status-list). Nothing was verified.");
222
- link = linkFromIndex(input.index, input.status, jwkThumbprintEd25519(keyBytes));
207
+ const tp = presentedKeyThumbprint(keyBytes);
208
+ link = tp === null ? null : linkFromIndex(input.index, input.status, tp);
223
209
  if (!link)
224
210
  return { decision: "refuse", verdict: "invalid", reason: "not_linked",
225
211
  explanation: "This key has no link at this registrar: a valid signature from it is not, by itself, an accountable operator." };