@ainra/mcp 0.7.0 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainra/mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.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,7 +17,7 @@
17
17
  "src/"
18
18
  ],
19
19
  "dependencies": {
20
- "@ainra/sdk": "^0.7.0"
20
+ "@ainra/sdk": "^0.8.0"
21
21
  },
22
22
  "homepage": "https://ainra.org/",
23
23
  "repository": {
package/src/reasons.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "_note": "The 20 frozen INVALID reasons, in verify order, each in plain words. Single source for the CLI/SDK/MCP/console legends and docs. The strings themselves are frozen (they match crates/ainra-core/src/verify.rs and packages/sdk-ts Reason); only these glosses are editorial. tools/reasons-check.mjs fails the build if this list and ainra-core's Reason::ALL ever diverge.",
2
+ "_note": "The 21 frozen INVALID reasons: the 20 the nine-step verify returns, in verify order, then denied_by_policy, which only a gate with its own policy returns (D-100) — each in plain words. Single source for the CLI/SDK/MCP/console legends and docs. The strings themselves are frozen (they match crates/ainra-core/src/verify.rs and packages/sdk-ts Reason); only these glosses are editorial. tools/reasons-check.mjs fails the build if this list and ainra-core's Reason::ALL ever diverge.",
3
3
  "valid": "All nine checks passed — genuine, in-window, in-scope, logged, and not revoked.",
4
4
  "schema_violation": "Not a well-formed passport, or it carries a forbidden (identity/score/money) field.",
5
5
  "name_malformed": "The subject or issuer name doesn't fit the AINRA name grammar.",
@@ -20,5 +20,6 @@
20
20
  "instance_expired": "The running copy's instance credential is outside its window, or its lifetime exceeds the one-hour ADR-019 ceiling. The passport is fine — the copy needs a fresh credential.",
21
21
  "instance_scope_exceeds": "The instance credential asks for a capability its passport does not hold. A running copy may only narrow, never widen (ADR-019).",
22
22
  "instance_sig_invalid": "The instance credential was not minted by this passport's control key, or is not bound to this passport (wrong subject or wrong logged leaf).",
23
- "instance_pop_invalid": "The presenter did not prove it holds the instance key: wrong audience, a timestamp outside tolerance, or a bad proof-of-possession signature."
23
+ "instance_pop_invalid": "The presenter did not prove it holds the instance key: wrong audience, a timestamp outside tolerance, or a bad proof-of-possession signature.",
24
+ "denied_by_policy": "Genuine, but this gate's own policy refuses it: a local deny entry names its registrar, lineage or name, or the signed directory is older than the gate allows."
24
25
  }
package/src/tools.mjs CHANGED
@@ -110,6 +110,11 @@ export const TOOLS = [
110
110
  roots: { type: "object", description: "The two ceremony root keys, { root_ed25519, root_slh } (roots.json). Obtain them out of band — never from the presenter." },
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
+ policy: {
114
+ type: "object", additionalProperties: false,
115
+ description: "YOUR own policy (D-100), optional. `deny`: registrar ids, lineages or exact names you refuse even when genuine. `max_directory_age_s`: refuse when the signed directory is older than this (off unless set, so offline use keeps working). It only ever turns accept into refuse, with the reason denied_by_policy.",
116
+ properties: { deny: { type: "array", items: { type: "string" } }, max_directory_age_s: { type: "integer", minimum: 0 } },
117
+ },
113
118
  now: { type: "integer", description: "Your clock, unix seconds. Defaults to this server's clock. Never read from the bundle." },
114
119
  },
115
120
  required: ["presentation"],
@@ -157,7 +162,10 @@ export const TOOLS = [
157
162
  const freshness = input.freshness ?? "F2";
158
163
  if (!["F1", "F2", "F3"].includes(freshness)) throw new Error("freshness must be F1, F2 or F3. Nothing was verified.");
159
164
  const audience = input.audience ?? "";
160
- const verifier = Verifier.fromDirectoryB64(directory, roots.root_ed25519, roots.root_slh, freshness, false, audience);
165
+ const pol = input.policy ?? {};
166
+ if (pol.deny !== undefined && !(Array.isArray(pol.deny) && pol.deny.every((e) => typeof e === "string"))) throw new Error("policy.deny must be a list of registrar ids, lineages or names. Nothing was verified.");
167
+ if (pol.max_directory_age_s !== undefined && !(Number.isInteger(pol.max_directory_age_s) && pol.max_directory_age_s >= 0)) throw new Error("policy.max_directory_age_s must be a whole number of seconds. Nothing was verified.");
168
+ const verifier = Verifier.fromDirectoryB64(directory, roots.root_ed25519, roots.root_slh, freshness, false, audience, { deny: pol.deny, maxDirectoryAgeS: pol.max_directory_age_s });
161
169
  if (!verifier)
162
170
  throw new Error("the directory does not verify against the roots — nothing in it can be trusted, so nothing was verified.");
163
171
  const now = Number.isInteger(input.now) ? input.now : Math.floor(Date.now() / 1000);