New:Socket for Asana Is Now Available.Learn more
Get Started

@archstone/runtime

Package Overview
Dependencies
Maintainers
1
Versions
35
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@archstone/runtime - npm Package Compare versions

Comparing version
0.15.0
to
0.16.0
+197
dist/chunk-7GP6HYWZ.js
// src/server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import {
inputJsonSchema,
objectJsonSchema,
applyResponseMapping,
contractViolationMessage,
evaluatePolicy,
evaluateRateLimit,
auditNow,
buildExecutionRecord,
emitExecutionRecord,
LIFECYCLE_BLOCKED_REASON,
LIFECYCLE_UNEVALUATABLE_REASON
} from "@archstone/emitter-support";
import { invokeRest } from "@archstone/provider-rest";
function effectAnnotations(effect) {
switch (effect) {
case "read":
return { readOnlyHint: true };
case "irreversible":
return { destructiveHint: true, idempotentHint: false };
case "write":
return { destructiveHint: false };
default:
return void 0;
}
}
function toolDefinitions(registry) {
const resources = registry.ir.resources;
return registry.invocableTools().filter(({ tool: t }) => registry.getExposure(t.id).listed).map(({ name, tool: t }) => {
const hint = registry.getExposure(t.id).hint;
const def = {
name,
description: hint ? `${t.description} (${hint.text})` : t.description,
inputSchema: inputJsonSchema(t.input, resources)
};
if (t.output.length > 0) def.outputSchema = objectJsonSchema(t.output, resources);
const annotations = effectAnnotations(t.effect);
if (annotations) def.annotations = annotations;
return def;
});
}
var CONTRACT_VIOLATION_META_KEY = "dev.archstone/contract_violation";
var LIFECYCLE_BLOCKED_META_KEY = "dev.archstone/lifecycle_blocked";
var LIFECYCLE_UNEVALUATABLE_META_KEY = "dev.archstone/lifecycle_unevaluatable";
var POLICY_DENIED_META_KEY = "dev.archstone/policy_denied";
async function callTool(registry, name, args, opts) {
const tool = registry.getCapability(name);
if (!tool) {
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
}
const auditSink = opts?.auditSink;
const startedAt = auditSink ? auditNow() : "";
const audit = (status) => {
if (!auditSink) return;
emitExecutionRecord(
auditSink,
buildExecutionRecord({
tool,
input: args,
// Fixed by this call site, never host-configurable: an auditor must be able to trust
// that a record claiming `mcp` came from the MCP path. `mcpHandler` mounts this same
// path and therefore also records `mcp` — the value names the protocol surface the call
// arrived on, not the npm package that mounted it.
consumer: "mcp",
caller: opts?.caller,
sessionId: opts?.sessionId,
workflowId: opts?.workflowId,
startedAt,
status
})
);
};
const exposure = registry.getExposure(tool.id);
if (!exposure.invocable) {
if (exposure.blockedReason === "unevaluatable") {
const text2 = `capability '${tool.id}' declares a lifecycle this build does not recognize and cannot evaluate \u2014 refusing (fail-closed).`;
audit({ phase: "denied", message: text2, denialReason: LIFECYCLE_UNEVALUATABLE_REASON });
return {
content: [{ type: "text", text: text2 }],
_meta: {
[LIFECYCLE_UNEVALUATABLE_META_KEY]: { error: "lifecycle_unevaluatable", capability: tool.id, lifecycle: tool.lifecycle }
},
isError: true
};
}
const text = `capability '${tool.id}' is retired and can no longer be invoked.`;
audit({ phase: "denied", message: text, denialReason: LIFECYCLE_BLOCKED_REASON });
return {
content: [{ type: "text", text }],
_meta: { [LIFECYCLE_BLOCKED_META_KEY]: { error: "lifecycle_blocked", capability: tool.id, lifecycle: tool.lifecycle } },
isError: true
};
}
const decision = opts?.callerResolutionFailed ? {
allowed: false,
denial: {
reason: "policy_unevaluatable",
message: `capability '${tool.id}' could not be evaluated \u2014 caller identity could not be established (resolveCaller failed) \u2014 refusing (fail-closed).`
}
} : evaluatePolicy(tool, {
principal: opts?.caller?.principal,
credentialPresent: opts?.caller?.accessToken !== void 0
});
if (!decision.allowed) {
audit({ phase: "denied", message: decision.denial.message, denialReason: decision.denial.reason });
return {
content: [{ type: "text", text: decision.denial.message }],
_meta: {
[POLICY_DENIED_META_KEY]: {
error: "policy_denied",
// `tool.id` — the unsanitized CDL id, never the MCP-sanitized advertised `name`
// lookup key (BR-28, mirroring ADD-19 and ADD-30 BR-7).
capability: tool.id,
reason: decision.denial.reason
}
},
isError: true
};
}
const rateDecision = await evaluateRateLimit(tool, { principal: opts?.caller?.principal }, opts?.rateLimitCounter);
if (!rateDecision.allowed) {
audit({ phase: "denied", message: rateDecision.denial.message, denialReason: rateDecision.denial.reason });
return {
content: [{ type: "text", text: rateDecision.denial.message }],
_meta: {
[POLICY_DENIED_META_KEY]: {
error: "policy_denied",
capability: tool.id,
reason: rateDecision.denial.reason
}
},
isError: true
};
}
const result = await invokeRest(tool, args, opts);
if (!result.ok) {
const text = result.error ?? "invocation failed";
audit({ phase: "failed", message: text });
return { content: [{ type: "text", text }], isError: true };
}
if (tool.response) {
const mapped = applyResponseMapping(tool, result.data, registry.ir.resources);
if (mapped.status === "violation") {
const missing = mapped.missing ?? [];
const text = contractViolationMessage(tool.id, missing);
audit({ phase: "failed", message: text });
return {
content: [{ type: "text", text }],
_meta: { [CONTRACT_VIOLATION_META_KEY]: { error: "contract_violation", capability: tool.id, missing } },
isError: true
};
}
const content = [{ type: "text", text: JSON.stringify(mapped.data, null, 2) }];
if (mapped.status === "degraded") {
content.push({ type: "text", text: `note: optional field(s) absent (degraded): ${(mapped.degraded ?? []).join(", ")}` });
}
audit({ phase: "succeeded" });
return { content, structuredContent: mapped.data, isError: false };
}
const out = { content: [{ type: "text", text: JSON.stringify(result.data ?? null, null, 2) }], isError: false };
if (tool.output.length > 0) {
const data = result.data;
if (data && typeof data === "object" && !Array.isArray(data)) {
out.structuredContent = data;
}
}
audit({ phase: "succeeded" });
return out;
}
function createMcpServer(registry, opts) {
const server = new Server({ name: "archstone", version: "0" }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions(registry) }));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const args = req.params.arguments ?? {};
const result = await callTool(registry, req.params.name, args, opts);
return result;
});
return server;
}
export {
effectAnnotations,
toolDefinitions,
CONTRACT_VIOLATION_META_KEY,
LIFECYCLE_BLOCKED_META_KEY,
LIFECYCLE_UNEVALUATABLE_META_KEY,
POLICY_DENIED_META_KEY,
callTool,
createMcpServer
};
//# sourceMappingURL=chunk-7GP6HYWZ.js.map
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["// @archstone/runtime — MCP server construction (fs-free)\n//\n// Builds an MCP Server from a Registry and routes invocations through the REST provider\n// (#6). This is the ONLY place the MCP SDK appears (alongside stdio's transport wiring in\n// mcp.ts and the /http subpath's transport wiring in http.ts) — semantic-type → JSON-Schema\n// lowering itself now lives in @archstone/emitter-support (ADD-0008 #27), never here.\n//\n// Extracted out of mcp.ts (ADD-0008 #27) specifically so this module's graph never reaches\n// registry.ts's buildRegistry/@archstone/schema `load()` (the fs edge) — only stdio's\n// `serveStdio` (mcp.ts) needs disk access. `http.ts` (the /http subpath) imports only this\n// file, so a consumer depending on that subpath alone stays fs-free.\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n type CallToolResult,\n type ToolAnnotations,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport type { IRTool } from \"@archstone/compiler\";\nimport {\n Registry,\n inputJsonSchema,\n objectJsonSchema,\n applyResponseMapping,\n contractViolationMessage,\n evaluatePolicy,\n evaluateRateLimit,\n auditNow,\n buildExecutionRecord,\n emitExecutionRecord,\n LIFECYCLE_BLOCKED_REASON,\n LIFECYCLE_UNEVALUATABLE_REASON,\n type ExecutionStatus,\n type PolicyDecision,\n} from \"@archstone/emitter-support\";\nimport { invokeRest, type InvokeOptions } from \"@archstone/provider-rest\";\n\ntype JsonSchema = Record<string, unknown>;\n\n/**\n * #126: the subset of MCP's `ToolAnnotations` this emitter is entitled to populate from CDL.\n *\n * Derived from the SDK's own `ToolAnnotations` via `Pick` rather than re-declared, so the three\n * field names are checked against the installed SDK at compile time instead of being trusted to\n * a comment. Confirmed present on the tool definition at the version `^1.12.0` resolves to\n * (1.30.0): `ToolSchema.annotations` is `ToolAnnotationsSchema.optional()`, and that schema\n * declares `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`.\n *\n * The two members deliberately NOT picked, because no CDL field means them:\n * - `openWorldHint` — whether the tool's domain of interaction is open or closed. Archstone\n * knows a capability's `effect`, never the shape of the world behind its connector. A guess\n * here would be indistinguishable, to a client, from a fact.\n * - `title` — a display concern with no CDL source; `description` already carries the\n * business-authored text (plus any exposure hint, ADD-24).\n */\nexport type McpToolAnnotations = Pick<ToolAnnotations, \"readOnlyHint\" | \"destructiveHint\" | \"idempotentHint\">;\n\n/**\n * #126: lower a capability's `effect` — the one field `archstone init` refuses to guess and\n * insists a human confirm — into MCP tool annotations, so the client's tool-confirmation dialog\n * can tell `tourism.search` from a capability that charges a card. Before this, `effect` was\n * compiled, carried through the IR, and dropped here: `McpToolDef` had no field for it and\n * `toolDefinitions` never read it, so the ONLY human-in-the-loop mechanism that exists today\n * (`human-approval` is declared and unenforced — `apply` says so) decided blind.\n *\n * The mapping, exactly and only:\n * read → readOnlyHint: true\n * irreversible → destructiveHint: true, idempotentHint: false\n * write → destructiveHint: false\n *\n * `write`'s single `false` is not a no-op: per the SDK's own schema docs, `destructiveHint`\n * DEFAULTS TO TRUE when absent, so stating it is the only thing that keeps a `write` from\n * reaching the client indistinguishable from an `irreversible`. Conversely `read` needs no\n * `destructiveHint` — the same docs describe that field as meaningful only when `readOnlyHint`\n * is false.\n *\n * The seam in this mapping, named rather than papered over: MCP describes\n * `destructiveHint: false` as a tool whose updates are purely additive, whereas CDL's `write`\n * means \"modifies, reversibly\" — `examples/manifests/booking`'s `tourism.cancel` is an\n * `effect: write` and is plainly not additive. The two vocabularies are adjacent, not\n * identical, and #126 ratified this pairing as the closest available fit, not a perfect one.\n * Read `destructiveHint: false` here as *not irreversible*, the distinction CDL actually draws.\n * Do not \"improve\" this into a finer per-capability judgement: CDL has no additivity primitive,\n * so anything more specific would be invented rather than derived — the same reasoning that\n * keeps `openWorldHint` off the list above.\n *\n * **This is a hint, not a control.** Nothing in Archstone gates, refuses, or retries on the\n * value — deliberately (#126, Not in scope). MCP's own spec says as much of every annotation,\n * and a client is free to ignore all of it.\n *\n * WHY THIS LIVES HERE AND NOT IN @archstone/emitter-support. CLAUDE.md puts lowering that is\n * *shared* between emitters in emitter-support; `readOnlyHint`/`destructiveHint`/\n * `idempotentHint` are MCP-protocol vocabulary with, as of this increment, exactly one\n * consumer. Every target format `@archstone/agent`'s `tools()` emits was checked against its\n * live reference before this call was made — the field lists, references and dates are in\n * `packages/agent/src/tools.ts`'s header comment, pinned by `packages/agent/test/tools.test.ts`\n * — and NONE has an equivalent field, so there is nothing to share. Note that `effect` itself\n * still reaches every `@archstone/agent` consumer: they hold the Registry and read it off the\n * IR directly, which is exactly why nothing needed inventing there. `exposure.ts` — the neutral\n * presentation module, and the obvious tempting\n * home — states the rule this follows verbatim: \"MCP-specific rendering ... belongs only in\n * @archstone/runtime's server.ts, never here.\" Move it to emitter-support the day a second\n * emitter needs it, and not before: re-encoding `read|write|irreversible` into some other\n * neutral vocabulary one layer down would add a translation with no second reader, when the IR\n * already carries the fact in the neutral vocabulary that matters.\n *\n * TOTAL, on purpose — same trust boundary and same reasoning as `lifecycleExposure`'s\n * `default` branch (ADD-56 D-1). `effect`'s static type is a closed three-member union, but the\n * value reaches this function un-runtime-validated whenever the Registry was built by\n * `fromIR`'s `json as IR` cast (`agent/src/index.ts`, which validates only `version === \"0\"`)\n * and served through `mcpHandler` → `createHttpHandler` → `createMcpServer` → here. A\n * hand-written or forward-versioned artifact can carry any string. That case returns\n * `undefined` — NO annotations at all — rather than any positive claim, so the client falls\n * back to MCP's own documented defaults (`readOnlyHint: false`, `destructiveHint: true`), which\n * are the cautious reading. The one outcome that must never be reachable from an unrecognized\n * value is `readOnlyHint: true`, and returning nothing is the only answer that guarantees it.\n */\nexport function effectAnnotations(effect: IRTool[\"effect\"]): McpToolAnnotations | undefined {\n switch (effect) {\n case \"read\":\n return { readOnlyHint: true };\n case \"irreversible\":\n return { destructiveHint: true, idempotentHint: false };\n case \"write\":\n return { destructiveHint: false };\n default:\n // Unrecognized `effect` across the `fromIR` trust boundary — claim nothing. See above.\n return undefined;\n }\n}\n\nexport interface McpToolDef {\n name: string;\n description: string;\n inputSchema: JsonSchema;\n outputSchema?: JsonSchema;\n /** #126: derived from the capability's `effect` — see `effectAnnotations`. Absent only when\n * `effect` is a value this build does not recognize (possible solely via `fromIR`). */\n annotations?: McpToolAnnotations;\n}\n\n/** The MCP tool list: only invocable (bound) capabilities become tools — Registry's\n * `invocableTools()` (ADD-30 D-3) is the single source of truth shared by this function\n * (what's listed) and `callTool` (what's resolvable, via `getCapability`), keeping the two\n * consistent. Input and output fields lower against the IR resource registry, so a\n * `collection: Stay` output emits a typed, described `outputSchema` (not a bare\n * `{type:object}`).\n *\n * ADD-24: a bound tool whose combined exposure (`registry.getExposure`, lifecycle + optional\n * health) is `listed:false` (lifecycle `experimental`/`retired`) is dropped from the returned\n * list entirely — unlisted, per D-10, though `experimental` remains callable by id (see\n * `callTool`). A tool carrying a `hint` (beta/deprecated, or a yellow/red health reading) has\n * its text appended to `description` — the only MCP-specific rendering of the neutral\n * exposure the emitter-support layer computed.\n *\n * #126: every listed tool also carries `annotations` derived from its `effect`\n * (`effectAnnotations`), so the client's confirmation dialog stops treating a search and a\n * payment identically. Both transports reach this one function — stdio via `serveStdio`'s\n * `createMcpServer`, HTTP via `createHttpHandler`'s — so there is no second place to keep in\n * step. */\nexport function toolDefinitions(registry: Registry): McpToolDef[] {\n const resources = registry.ir.resources;\n return registry\n .invocableTools()\n .filter(({ tool: t }) => registry.getExposure(t.id).listed)\n .map(({ name, tool: t }) => {\n const hint = registry.getExposure(t.id).hint;\n const def: McpToolDef = {\n name,\n description: hint ? `${t.description} (${hint.text})` : t.description,\n inputSchema: inputJsonSchema(t.input, resources),\n };\n if (t.output.length > 0) def.outputSchema = objectJsonSchema(t.output, resources);\n const annotations = effectAnnotations(t.effect);\n if (annotations) def.annotations = annotations;\n return def;\n });\n}\n\nexport interface CallResult {\n content: { type: \"text\"; text: string }[];\n structuredContent?: Record<string, unknown>;\n isError: boolean;\n _meta?: Record<string, unknown>;\n}\n\n/** #19 ADD-19 Rev 2 D-6: the namespaced `_meta` key a VIOLATION result's structured error\n * object is carried under. Never populated on `structuredContent` (D-3′) — the reference\n * MCP SDK client validates `structuredContent` against `outputSchema` whenever the tool\n * declares one, regardless of `isError`, so a non-conforming error object there crashes the\n * client. `_meta` is untouched by that validation and passes through the client's zod parse\n * unstripped (`ResultSchema`/`RequestMetaSchema` are `z.looseObject`). */\nexport const CONTRACT_VIOLATION_META_KEY = \"dev.archstone/contract_violation\";\n\n/** ADD-24 D-11: the namespaced `_meta` key a `retired` (or otherwise `invocable:false`)\n * tool's rejection is carried under — reuses `CONTRACT_VIOLATION_META_KEY`'s precedent\n * (ADD-19 Rev 2 D-3′/D-6) verbatim: never `structuredContent`, so the reference SDK client's\n * unconditional `structuredContent`-against-`outputSchema` validation never sees it. A\n * distinct key (not `CONTRACT_VIOLATION_META_KEY`) so a client distinguishes \"this call was\n * blocked before any connector work\" from \"the provider's response violated the contract\". */\nexport const LIFECYCLE_BLOCKED_META_KEY = \"dev.archstone/lifecycle_blocked\";\n\n/** ADD-56 D-2/OQ-56-B: the namespaced `_meta` key an unrecognized-lifecycle rejection is\n * carried under — a DISTINCT key from `LIFECYCLE_BLOCKED_META_KEY`, mirroring\n * `POLICY_DENIED_META_KEY` vs. `LIFECYCLE_BLOCKED_META_KEY` being genuinely distinct keys\n * rather than one key with a varying `error` string inside it. `retired` (a governance\n * refusal) and an unrecognized `lifecycle` (a compatibility refusal) are different facts with\n * different remediations and must be trivially distinguishable to a client — see\n * `exposure.ts`'s `Exposure.blockedReason` doc comment. */\nexport const LIFECYCLE_UNEVALUATABLE_META_KEY = \"dev.archstone/lifecycle_unevaluatable\";\n\n/** #43 ADD-43: the namespaced `_meta` key a POLICY denial is carried under — the third use of\n * the ADD-19 Rev 2 D-3′/D-6 precedent, verbatim: never `structuredContent`, because the\n * reference SDK client validates that against the tool's `outputSchema` unconditionally (not\n * gated on `isError`) and an error object there crashes it. A distinct key from the other two\n * so a client can tell \"refused by policy before any connector work\" from \"blocked by\n * lifecycle\" from \"the provider's response violated the contract\" — the three are mutually\n * exclusive on one call (BR-27). The object discloses the reason code and the capability id\n * and NOTHING about the policy itself: no metadata.id, no allow/deny entry, no other\n * principal's identifier (BR-30, Rule #7 — the MCP client is the untrusted side). */\nexport const POLICY_DENIED_META_KEY = \"dev.archstone/policy_denied\";\n\n/** Route an MCP tool call to the REST provider and format the result as MCP content. */\nexport async function callTool(\n registry: Registry,\n name: string,\n args: Record<string, unknown>,\n opts?: InvokeOptions,\n): Promise<CallResult> {\n const tool = registry.getCapability(name);\n if (!tool) {\n // #44: NO audit record. `metadata.capabilityId` is required and the only value available\n // here is an unvalidated, caller-chosen string that is not a CDL id — writing it there\n // would put unbounded attacker-controlled values into the audit log's primary correlation\n // key, in a file a compliance process treats as evidence. An `Execution` record audits a\n // capability invocation attempt; a call naming no capability is a protocol-level event and\n // belongs to the host's own mount-point instrumentation. Named residual, deliberately\n // accepted: tool-name probing (and the collision defence, which also lands here) is\n // invisible to the audit trail.\n return { content: [{ type: \"text\", text: `unknown tool: ${name}` }], isError: true };\n }\n\n // #44: the attempt clock starts HERE — before the exposure gate and before policy evaluation,\n // so every refused attempt still carries a real `startedAt` and can be placed on a timeline.\n // With no sink configured this is a strict no-op: no clock read, no id, no record, no\n // allocation, and every result byte-for-byte what it was before this increment.\n const auditSink = opts?.auditSink;\n const startedAt = auditSink ? auditNow() : \"\";\n const audit = (status: ExecutionStatus): void => {\n if (!auditSink) return;\n emitExecutionRecord(\n auditSink,\n buildExecutionRecord({\n tool,\n input: args,\n // Fixed by this call site, never host-configurable: an auditor must be able to trust\n // that a record claiming `mcp` came from the MCP path. `mcpHandler` mounts this same\n // path and therefore also records `mcp` — the value names the protocol surface the call\n // arrived on, not the npm package that mounted it.\n consumer: \"mcp\",\n caller: opts?.caller,\n sessionId: opts?.sessionId,\n workflowId: opts?.workflowId,\n startedAt,\n status,\n }),\n );\n };\n\n // ADD-24 D-10/D-11: `lifecycle: retired` sets `invocable:false` (health never does, D-9) —\n // checked immediately after resolution, before any connector/response work, same call-site\n // discipline the `contract_violation` check already uses downstream.\n //\n // ADD-56 D-1/D-2: `lifecycleExposure` is now TOTAL — an unrecognized `lifecycle` value (only\n // reachable via a hand-written or forward-versioned `fromIR` artifact, ADD-0008 D-2) ALSO sets\n // `invocable:false`, distinguished from `retired` by `exposure.blockedReason`. The two are\n // different facts with different remediations (governance vs. compatibility — see\n // `exposure.ts`'s `Exposure.blockedReason` doc comment) and MUST NOT share a message or a\n // `denialReason`. `exposure.blockedReason === \"unevaluatable\"` is the only branch this can take\n // here: the `undefined` case (D-4's unknown-id fallback) cannot occur, because `tool` above was\n // already resolved via `getCapability`, which reads the identical `exposureById` map.\n const exposure = registry.getExposure(tool.id);\n if (!exposure.invocable) {\n if (exposure.blockedReason === \"unevaluatable\") {\n const text = `capability '${tool.id}' declares a lifecycle this build does not recognize and cannot evaluate — refusing (fail-closed).`;\n // #44: `denied`, never `failed` — refusing on a compatibility gap is not a backend\n // failure. Deliberately distinct denialReason/message/meta-key from the `retired` branch\n // below (ADD-56 D-2/D-3) — never `LIFECYCLE_BLOCKED_REASON`.\n audit({ phase: \"denied\", message: text, denialReason: LIFECYCLE_UNEVALUATABLE_REASON });\n return {\n content: [{ type: \"text\", text }],\n _meta: {\n [LIFECYCLE_UNEVALUATABLE_META_KEY]: { error: \"lifecycle_unevaluatable\", capability: tool.id, lifecycle: tool.lifecycle },\n },\n isError: true,\n };\n }\n const text = `capability '${tool.id}' is retired and can no longer be invoked.`;\n // #44: `denied`, never `failed` — a refusal by lifecycle is a refusal by governance, and\n // recording it as a failure would conflate it with \"the backend broke\" in the one log where\n // that distinction is the entire product. This gate is the SECOND (and only other) producer\n // of `phase: \"denied\"`, and the reason code the policy evaluator can never return. It runs\n // before policy deliberately, so a capability that is both retired and policy-denied records\n // `lifecycle_blocked`, matching the pinned gate order.\n audit({ phase: \"denied\", message: text, denialReason: LIFECYCLE_BLOCKED_REASON });\n return {\n content: [{ type: \"text\", text }],\n _meta: { [LIFECYCLE_BLOCKED_META_KEY]: { error: \"lifecycle_blocked\", capability: tool.id, lifecycle: tool.lifecycle } },\n isError: true,\n };\n }\n\n // #43 (ADD-43 D-5/D-6): THE policy evaluation point, called unconditionally — for every tool,\n // including one with no resolved policy, because `authenticated` enforcement now lives here\n // rather than inside `invokeRest` (D-4). Deliberately AFTER the ADD-24 exposure gate above, so\n // a `retired` capability reports `lifecycle_blocked` and never `policy_denied` (BR-34), and\n // strictly BEFORE `invokeRest`, so a denial does zero connector work: no env/caller\n // resolution, no URL building, no fetch, and no `onResponse` firing (BR-25).\n // #48: a `resolveCaller` that THREW for this request (rather than returning, even\n // `undefined`) short-circuits straight to a `policy_unevaluatable` denial for every\n // capability, bypassing `evaluatePolicy` entirely — identity extraction itself failed here,\n // which is strictly less trustworthy than \"no credential offered\" and must fail closed\n // regardless of whether THIS capability happens to declare `policies:[authenticated]`\n // (ADD-42 R-11). Reuses the evaluator's own reason code and this function's existing\n // policy-denial response shaping verbatim — no parallel response path.\n const decision: PolicyDecision = opts?.callerResolutionFailed\n ? {\n allowed: false,\n denial: {\n reason: \"policy_unevaluatable\",\n message: `capability '${tool.id}' could not be evaluated — caller identity could not be established (resolveCaller failed) — refusing (fail-closed).`,\n },\n }\n : evaluatePolicy(tool, {\n principal: opts?.caller?.principal,\n credentialPresent: opts?.caller?.accessToken !== undefined,\n });\n if (!decision.allowed) {\n // #44: the evaluator's OWN reason code, copied verbatim — no re-spelling, no mapping table,\n // no superset for the policy case. The message is likewise the evaluator's, unaltered:\n // the record copies, it never authors.\n audit({ phase: \"denied\", message: decision.denial.message, denialReason: decision.denial.reason });\n return {\n content: [{ type: \"text\", text: decision.denial.message }],\n _meta: {\n [POLICY_DENIED_META_KEY]: {\n error: \"policy_denied\",\n // `tool.id` — the unsanitized CDL id, never the MCP-sanitized advertised `name`\n // lookup key (BR-28, mirroring ADD-19 and ADD-30 BR-7).\n capability: tool.id,\n reason: decision.denial.reason,\n },\n },\n isError: true,\n };\n }\n\n // #45 (ADD-45 D-2/D-3): the rate-limit evaluation step, called at the SAME point as the policy\n // evaluator above — immediately after it allows, strictly before `invokeRest` — so a\n // rate-limited call does exactly as much connector work as a policy-denied one: none. Reuses\n // the identical `policy_denied` `_meta` shape and audit wiring; only the reason code differs.\n const rateDecision = await evaluateRateLimit(tool, { principal: opts?.caller?.principal }, opts?.rateLimitCounter);\n if (!rateDecision.allowed) {\n audit({ phase: \"denied\", message: rateDecision.denial.message, denialReason: rateDecision.denial.reason });\n return {\n content: [{ type: \"text\", text: rateDecision.denial.message }],\n _meta: {\n [POLICY_DENIED_META_KEY]: {\n error: \"policy_denied\",\n capability: tool.id,\n reason: rateDecision.denial.reason,\n },\n },\n isError: true,\n };\n }\n\n const result = await invokeRest(tool, args, opts);\n if (!result.ok) {\n // #44: every attempt that never completed a usable round-trip — unbound capability, missing\n // env var, missing caller credential, no baseUrl, an allowlist rejection, a missing path\n // parameter, a network error, a non-2xx response — records `failed` carrying the shipped\n // error text verbatim, so a deployer greps the audit log and finds the same string the\n // agent was shown.\n const text = result.error ?? \"invocation failed\";\n audit({ phase: \"failed\", message: text });\n return { content: [{ type: \"text\", text }], isError: true };\n }\n\n // #12 (ADD-12): a binding with a `response:` mapping is now MAPPED + VALIDATED against the\n // resource — the outputSchema (ADD-11) becomes an enforced contract, not just declared.\n if (tool.response) {\n const mapped = applyResponseMapping(tool, result.data, registry.ir.resources);\n if (mapped.status === \"violation\") {\n // Fail closed (D-6): the declared output shape was not met — no raw pass-through.\n const missing = mapped.missing ?? [];\n // The text is unchanged byte-for-byte; it moved into a shared helper (#44) only so the\n // embedded consumer, whose own result carries no text, records the identical sentence.\n const text = contractViolationMessage(tool.id, missing);\n // #19 (ADD-19 Rev 2 D-3′/D-6): structured error object lives in `_meta`, never\n // `structuredContent` — the reference SDK client validates `structuredContent` against\n // the tool's `outputSchema` unconditionally (not gated on `isError`), so a VIOLATION\n // object there (which never conforms to the success outputSchema) crashes the client\n // (verified live against the SDK's own InMemoryTransport, R2.0/R2.2). `capability` is\n // `tool.id`, the unsanitized CDL id — never the MCP-sanitized `name` lookup key (BR-7).\n // #44: a VIOLATION is `failed` — the declared output shape was not met.\n audit({ phase: \"failed\", message: text });\n return {\n content: [{ type: \"text\", text }],\n _meta: { [CONTRACT_VIOLATION_META_KEY]: { error: \"contract_violation\", capability: tool.id, missing } },\n isError: true,\n };\n }\n const content: CallResult[\"content\"] = [{ type: \"text\", text: JSON.stringify(mapped.data, null, 2) }];\n if (mapped.status === \"degraded\") {\n content.push({ type: \"text\", text: `note: optional field(s) absent (degraded): ${(mapped.degraded ?? []).join(\", \")}` });\n }\n // #44: `degraded` records `succeeded`, NOT `failed` — every *required* field was present and\n // an optional one was not, so the invocation succeeded. Pinned in a comment because\n // \"degraded\" reads like a failure and the next reader will guess otherwise.\n audit({ phase: \"succeeded\" });\n return { content, structuredContent: mapped.data, isError: false };\n }\n\n // No response mapping: today's raw pass-through (rollout-safe). The declared outputSchema is\n // NOT yet enforced for these tools — add a `response:` block to close the loop (ADD-12 R-3).\n const out: CallResult = { content: [{ type: \"text\", text: JSON.stringify(result.data ?? null, null, 2) }], isError: false };\n if (tool.output.length > 0) {\n const data = result.data;\n if (data && typeof data === \"object\" && !Array.isArray(data)) {\n out.structuredContent = data as Record<string, unknown>;\n }\n }\n // #44: `status.output` is deliberately NOT populated here (nor anywhere) — `result.data` is\n // exactly the payload the record must never carry.\n audit({ phase: \"succeeded\" });\n return out;\n}\n\n/** Build an MCP Server that lists and invokes the registry's tools. */\nexport function createMcpServer(registry: Registry, opts?: InvokeOptions): Server {\n const server = new Server({ name: \"archstone\", version: \"0\" }, { capabilities: { tools: {} } });\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions(registry) }));\n\n server.setRequestHandler(CallToolRequestSchema, async (req) => {\n const args = (req.params.arguments ?? {}) as Record<string, unknown>;\n const result = await callTool(registry, req.params.name, args, opts);\n return result as CallToolResult;\n });\n\n return server;\n}\n"],"mappings":"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","names":["text"]}
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
import { IRTool } from '@archstone/compiler';
import { Registry } from '@archstone/emitter-support';
import { InvokeOptions } from '@archstone/provider-rest';
type JsonSchema = Record<string, unknown>;
/**
* #126: the subset of MCP's `ToolAnnotations` this emitter is entitled to populate from CDL.
*
* Derived from the SDK's own `ToolAnnotations` via `Pick` rather than re-declared, so the three
* field names are checked against the installed SDK at compile time instead of being trusted to
* a comment. Confirmed present on the tool definition at the version `^1.12.0` resolves to
* (1.30.0): `ToolSchema.annotations` is `ToolAnnotationsSchema.optional()`, and that schema
* declares `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`.
*
* The two members deliberately NOT picked, because no CDL field means them:
* - `openWorldHint` — whether the tool's domain of interaction is open or closed. Archstone
* knows a capability's `effect`, never the shape of the world behind its connector. A guess
* here would be indistinguishable, to a client, from a fact.
* - `title` — a display concern with no CDL source; `description` already carries the
* business-authored text (plus any exposure hint, ADD-24).
*/
type McpToolAnnotations = Pick<ToolAnnotations, "readOnlyHint" | "destructiveHint" | "idempotentHint">;
/**
* #126: lower a capability's `effect` — the one field `archstone init` refuses to guess and
* insists a human confirm — into MCP tool annotations, so the client's tool-confirmation dialog
* can tell `tourism.search` from a capability that charges a card. Before this, `effect` was
* compiled, carried through the IR, and dropped here: `McpToolDef` had no field for it and
* `toolDefinitions` never read it, so the ONLY human-in-the-loop mechanism that exists today
* (`human-approval` is declared and unenforced — `apply` says so) decided blind.
*
* The mapping, exactly and only:
* read → readOnlyHint: true
* irreversible → destructiveHint: true, idempotentHint: false
* write → destructiveHint: false
*
* `write`'s single `false` is not a no-op: per the SDK's own schema docs, `destructiveHint`
* DEFAULTS TO TRUE when absent, so stating it is the only thing that keeps a `write` from
* reaching the client indistinguishable from an `irreversible`. Conversely `read` needs no
* `destructiveHint` — the same docs describe that field as meaningful only when `readOnlyHint`
* is false.
*
* The seam in this mapping, named rather than papered over: MCP describes
* `destructiveHint: false` as a tool whose updates are purely additive, whereas CDL's `write`
* means "modifies, reversibly" — `examples/manifests/booking`'s `tourism.cancel` is an
* `effect: write` and is plainly not additive. The two vocabularies are adjacent, not
* identical, and #126 ratified this pairing as the closest available fit, not a perfect one.
* Read `destructiveHint: false` here as *not irreversible*, the distinction CDL actually draws.
* Do not "improve" this into a finer per-capability judgement: CDL has no additivity primitive,
* so anything more specific would be invented rather than derived — the same reasoning that
* keeps `openWorldHint` off the list above.
*
* **This is a hint, not a control.** Nothing in Archstone gates, refuses, or retries on the
* value — deliberately (#126, Not in scope). MCP's own spec says as much of every annotation,
* and a client is free to ignore all of it.
*
* WHY THIS LIVES HERE AND NOT IN @archstone/emitter-support. CLAUDE.md puts lowering that is
* *shared* between emitters in emitter-support; `readOnlyHint`/`destructiveHint`/
* `idempotentHint` are MCP-protocol vocabulary with, as of this increment, exactly one
* consumer. Every target format `@archstone/agent`'s `tools()` emits was checked against its
* live reference before this call was made — the field lists, references and dates are in
* `packages/agent/src/tools.ts`'s header comment, pinned by `packages/agent/test/tools.test.ts`
* — and NONE has an equivalent field, so there is nothing to share. Note that `effect` itself
* still reaches every `@archstone/agent` consumer: they hold the Registry and read it off the
* IR directly, which is exactly why nothing needed inventing there. `exposure.ts` — the neutral
* presentation module, and the obvious tempting
* home — states the rule this follows verbatim: "MCP-specific rendering ... belongs only in
* @archstone/runtime's server.ts, never here." Move it to emitter-support the day a second
* emitter needs it, and not before: re-encoding `read|write|irreversible` into some other
* neutral vocabulary one layer down would add a translation with no second reader, when the IR
* already carries the fact in the neutral vocabulary that matters.
*
* TOTAL, on purpose — same trust boundary and same reasoning as `lifecycleExposure`'s
* `default` branch (ADD-56 D-1). `effect`'s static type is a closed three-member union, but the
* value reaches this function un-runtime-validated whenever the Registry was built by
* `fromIR`'s `json as IR` cast (`agent/src/index.ts`, which validates only `version === "0"`)
* and served through `mcpHandler` → `createHttpHandler` → `createMcpServer` → here. A
* hand-written or forward-versioned artifact can carry any string. That case returns
* `undefined` — NO annotations at all — rather than any positive claim, so the client falls
* back to MCP's own documented defaults (`readOnlyHint: false`, `destructiveHint: true`), which
* are the cautious reading. The one outcome that must never be reachable from an unrecognized
* value is `readOnlyHint: true`, and returning nothing is the only answer that guarantees it.
*/
declare function effectAnnotations(effect: IRTool["effect"]): McpToolAnnotations | undefined;
interface McpToolDef {
name: string;
description: string;
inputSchema: JsonSchema;
outputSchema?: JsonSchema;
/** #126: derived from the capability's `effect` — see `effectAnnotations`. Absent only when
* `effect` is a value this build does not recognize (possible solely via `fromIR`). */
annotations?: McpToolAnnotations;
}
/** The MCP tool list: only invocable (bound) capabilities become tools — Registry's
* `invocableTools()` (ADD-30 D-3) is the single source of truth shared by this function
* (what's listed) and `callTool` (what's resolvable, via `getCapability`), keeping the two
* consistent. Input and output fields lower against the IR resource registry, so a
* `collection: Stay` output emits a typed, described `outputSchema` (not a bare
* `{type:object}`).
*
* ADD-24: a bound tool whose combined exposure (`registry.getExposure`, lifecycle + optional
* health) is `listed:false` (lifecycle `experimental`/`retired`) is dropped from the returned
* list entirely — unlisted, per D-10, though `experimental` remains callable by id (see
* `callTool`). A tool carrying a `hint` (beta/deprecated, or a yellow/red health reading) has
* its text appended to `description` — the only MCP-specific rendering of the neutral
* exposure the emitter-support layer computed.
*
* #126: every listed tool also carries `annotations` derived from its `effect`
* (`effectAnnotations`), so the client's confirmation dialog stops treating a search and a
* payment identically. Both transports reach this one function — stdio via `serveStdio`'s
* `createMcpServer`, HTTP via `createHttpHandler`'s — so there is no second place to keep in
* step. */
declare function toolDefinitions(registry: Registry): McpToolDef[];
interface CallResult {
content: {
type: "text";
text: string;
}[];
structuredContent?: Record<string, unknown>;
isError: boolean;
_meta?: Record<string, unknown>;
}
/** #19 ADD-19 Rev 2 D-6: the namespaced `_meta` key a VIOLATION result's structured error
* object is carried under. Never populated on `structuredContent` (D-3′) — the reference
* MCP SDK client validates `structuredContent` against `outputSchema` whenever the tool
* declares one, regardless of `isError`, so a non-conforming error object there crashes the
* client. `_meta` is untouched by that validation and passes through the client's zod parse
* unstripped (`ResultSchema`/`RequestMetaSchema` are `z.looseObject`). */
declare const CONTRACT_VIOLATION_META_KEY = "dev.archstone/contract_violation";
/** ADD-24 D-11: the namespaced `_meta` key a `retired` (or otherwise `invocable:false`)
* tool's rejection is carried under — reuses `CONTRACT_VIOLATION_META_KEY`'s precedent
* (ADD-19 Rev 2 D-3′/D-6) verbatim: never `structuredContent`, so the reference SDK client's
* unconditional `structuredContent`-against-`outputSchema` validation never sees it. A
* distinct key (not `CONTRACT_VIOLATION_META_KEY`) so a client distinguishes "this call was
* blocked before any connector work" from "the provider's response violated the contract". */
declare const LIFECYCLE_BLOCKED_META_KEY = "dev.archstone/lifecycle_blocked";
/** ADD-56 D-2/OQ-56-B: the namespaced `_meta` key an unrecognized-lifecycle rejection is
* carried under — a DISTINCT key from `LIFECYCLE_BLOCKED_META_KEY`, mirroring
* `POLICY_DENIED_META_KEY` vs. `LIFECYCLE_BLOCKED_META_KEY` being genuinely distinct keys
* rather than one key with a varying `error` string inside it. `retired` (a governance
* refusal) and an unrecognized `lifecycle` (a compatibility refusal) are different facts with
* different remediations and must be trivially distinguishable to a client — see
* `exposure.ts`'s `Exposure.blockedReason` doc comment. */
declare const LIFECYCLE_UNEVALUATABLE_META_KEY = "dev.archstone/lifecycle_unevaluatable";
/** #43 ADD-43: the namespaced `_meta` key a POLICY denial is carried under — the third use of
* the ADD-19 Rev 2 D-3′/D-6 precedent, verbatim: never `structuredContent`, because the
* reference SDK client validates that against the tool's `outputSchema` unconditionally (not
* gated on `isError`) and an error object there crashes it. A distinct key from the other two
* so a client can tell "refused by policy before any connector work" from "blocked by
* lifecycle" from "the provider's response violated the contract" — the three are mutually
* exclusive on one call (BR-27). The object discloses the reason code and the capability id
* and NOTHING about the policy itself: no metadata.id, no allow/deny entry, no other
* principal's identifier (BR-30, Rule #7 — the MCP client is the untrusted side). */
declare const POLICY_DENIED_META_KEY = "dev.archstone/policy_denied";
/** Route an MCP tool call to the REST provider and format the result as MCP content. */
declare function callTool(registry: Registry, name: string, args: Record<string, unknown>, opts?: InvokeOptions): Promise<CallResult>;
/** Build an MCP Server that lists and invokes the registry's tools. */
declare function createMcpServer(registry: Registry, opts?: InvokeOptions): Server;
export { CONTRACT_VIOLATION_META_KEY as C, LIFECYCLE_BLOCKED_META_KEY as L, type McpToolAnnotations as M, POLICY_DENIED_META_KEY as P, type CallResult as a, LIFECYCLE_UNEVALUATABLE_META_KEY as b, createMcpServer as c, type McpToolDef as d, callTool as e, effectAnnotations as f, toolDefinitions as t };
+3
-1
import { Registry } from '@archstone/emitter-support';
import { InvokeOptions, CallerContext } from '@archstone/provider-rest';
export { c as createMcpServer } from './server-D3gsLwzV.js';
export { c as createMcpServer } from './server-Bs4Zr35L.js';
import '@modelcontextprotocol/sdk/server/index.js';
import '@modelcontextprotocol/sdk/types.js';
import '@archstone/compiler';

@@ -6,0 +8,0 @@ interface CreateHttpHandlerOptions {

import {
createMcpServer
} from "./chunk-YFNW2UHK.js";
} from "./chunk-7GP6HYWZ.js";

@@ -5,0 +5,0 @@ // src/http.ts

@@ -7,4 +7,5 @@ import { LoadIssue } from '@archstone/schema';

export { ContractRecording, GoldenFixture, ProbeOutcome, RecordContractOptions, SkippedVerification, ToolVerification, VerifyRun, VerifyScope, recordContract, runVerify, verifyTool } from './verify.js';
export { C as CONTRACT_VIOLATION_META_KEY, a as CallResult, L as LIFECYCLE_BLOCKED_META_KEY, b as LIFECYCLE_UNEVALUATABLE_META_KEY, M as McpToolDef, P as POLICY_DENIED_META_KEY, d as callTool, c as createMcpServer, t as toolDefinitions } from './server-D3gsLwzV.js';
export { C as CONTRACT_VIOLATION_META_KEY, a as CallResult, L as LIFECYCLE_BLOCKED_META_KEY, b as LIFECYCLE_UNEVALUATABLE_META_KEY, M as McpToolAnnotations, d as McpToolDef, P as POLICY_DENIED_META_KEY, e as callTool, c as createMcpServer, f as effectAnnotations, t as toolDefinitions } from './server-Bs4Zr35L.js';
import '@modelcontextprotocol/sdk/server/index.js';
import '@modelcontextprotocol/sdk/types.js';

@@ -11,0 +12,0 @@ /** Conventional health-snapshot file, read once next to the manifest dir (ADD-24 D-8): the

@@ -8,4 +8,5 @@ import {

createMcpServer,
effectAnnotations,
toolDefinitions
} from "./chunk-YFNW2UHK.js";
} from "./chunk-7GP6HYWZ.js";
import {

@@ -203,2 +204,3 @@ applyResponseMapping,

createMcpServer,
effectAnnotations,
inputJsonSchema,

@@ -205,0 +207,0 @@ jsonLinesAuditSink,

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/registry.ts","../src/mcp.ts","../src/adopt.ts","../src/audit-file.ts"],"sourcesContent":["// @archstone/runtime — Capability Registry (#5)\n//\n// The product kernel: capabilities queryable at runtime, indexed over the IR.\n// File-backed (no DB) — the IR is derived from manifests on disk. The MCP emitter\n// (#7) consumes this to list and resolve tools.\n//\n// `Registry` (index-only) moved to @archstone/emitter-support (ADD-0008 #27) — re-exported\n// here for back-compat so nothing downstream breaks. This file keeps the fs-touching\n// pipeline (`buildRegistry`), which is why the /http subpath (http.ts) never imports it.\n//\n// ADD-24 (#24): `buildRegistry` also optionally reads a conventional health-snapshot file\n// (`readHealthSnapshot`, below) — the ONE other fs-touching, network-free addition this ADD\n// makes. Binding health itself is never computed here (that's `archstone verify`'s own live\n// probe, ADD-18 D-5) — only its already-serialized `--json` output is read back.\n\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { load, type LoadResult, type LoadIssue } from \"@archstone/schema\";\nimport { validateSemantics, compile, type Diagnostic } from \"@archstone/compiler\";\nimport { Registry, type HealthStatus } from \"@archstone/emitter-support\";\n\nexport { Registry } from \"@archstone/emitter-support\";\n\n/** Conventional health-snapshot file, read once next to the manifest dir (ADD-24 D-8): the\n * operator/CI populates it by redirecting the ALREADY-shipped `archstone verify --json`\n * output here — no new serialization. `buildRegistry` reads it (fs, but no network — the\n * live probe stays exclusively `verify`'s, ADD-18 D-5) and hands the parsed map to\n * `Registry`, which composes it with each tool's lifecycle exposure (ADD-24 §7 step 5). */\nexport const HEALTH_SNAPSHOT_FILE = \".archstone-health.json\";\n\nconst HEALTH_STATUSES: ReadonlySet<string> = new Set([\"green\", \"yellow\", \"red\"]);\n\n/**\n * Parse the `{results: ToolVerification[]}` shape `archstone verify --json` already produces\n * (ADD-20) into a capabilityId -> HealthStatus map. Fail-open (ADD-24 D-9): a missing file, a\n * parse error, or a malformed/unexpected shape all return `undefined` — the caller then\n * proceeds with lifecycle-only exposure, never mistaking \"no snapshot\" for \"known bad\".\n *\n * #43 (ADD-43 D-14): an entry marked `policyDenied` is SKIPPED. A refusal that happened before\n * the call is not a health fact — no request was issued, so nothing about the backend's contract\n * was observed. Left in, it would reach `combineExposure` as a `red` and append\n * `\"binding health: red — the last contract verification failed\"` to the tool's agent-facing\n * description at the highest severity: a statement that never happened, shown to every caller\n * including permitted ones, making policy affect listing (which BR-36 forbids). Skipping leaves\n * the tool with NO health entry, which is exactly ADD-24 D-9's ratified posture — absent health\n * must never be manufactured into known-bad.\n */\nfunction readHealthSnapshot(dir: string): Map<string, HealthStatus> | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(join(dir, HEALTH_SNAPSHOT_FILE), \"utf8\"));\n } catch {\n return undefined; // absent, unreadable, or invalid JSON — fail-open\n }\n\n const results = (parsed as { results?: unknown } | null)?.results;\n if (!Array.isArray(results)) return undefined;\n\n const map = new Map<string, HealthStatus>();\n for (const r of results) {\n if (!r || typeof r !== \"object\") continue;\n // ADD-43 D-14: a policy denial is not a health reading — drop it rather than let it become\n // an agent-facing \"the last contract verification failed\" hint for a verification that\n // never ran. See this function's doc comment.\n if ((r as { policyDenied?: unknown }).policyDenied === true) continue;\n const capabilityId = (r as { capabilityId?: unknown }).capabilityId;\n const status = (r as { status?: unknown }).status;\n if (typeof capabilityId === \"string\" && typeof status === \"string\" && HEALTH_STATUSES.has(status)) {\n map.set(capabilityId, status as HealthStatus);\n }\n }\n return map;\n}\n\nexport interface BuildResult {\n ok: boolean;\n registry?: Registry;\n issues: LoadIssue[];\n diagnostics: Diagnostic[];\n}\n\n/**\n * File-backed pipeline: load (#2) → semantic-validate (#3) → compile (#4) → Registry (#5).\n * `registry` is present only when shapes are valid, there are no semantic errors, AND no\n * tool-name collision (ADD-30 D-2) — folded into this function's existing `diagnostics`/\n * `ok` contract (new `tool-name-collision` diagnostic code) rather than a new mechanism, so\n * `serveStdio`/`runServeHttp` (which already refuse to proceed on `!built.ok`) inherit the\n * gate for free.\n */\nexport function buildRegistry(dir: string): BuildResult {\n const model: LoadResult = load(dir);\n const diagnostics = validateSemantics(model);\n const hasErrors = diagnostics.some((d) => d.severity === \"error\");\n let ok = model.ok && !hasErrors;\n\n const registry = ok ? new Registry(compile(model), readHealthSnapshot(dir)) : undefined;\n if (registry) {\n for (const c of registry.toolNameCollisions) {\n ok = false;\n diagnostics.push({\n severity: \"error\",\n code: \"tool-name-collision\",\n message: `tool name '${c.name}' is ambiguous — capabilities ${c.ids.join(\", \")} all sanitize to it`,\n });\n }\n }\n\n return {\n ok,\n registry: ok ? registry : undefined,\n issues: model.issues,\n diagnostics,\n };\n}\n","// @archstone/runtime — MCP emitter (#7) — stdio entrypoint\n//\n// serveStdio builds the registry from disk (buildRegistry, registry.ts) and serves it over\n// stdio, the channel Claude Desktop uses. The fs-free MCP server construction\n// (toolDefinitions/callTool/createMcpServer) lives in ./server (ADD-0008 #27) — re-exported\n// here, alongside the semantic-lowering functions from @archstone/emitter-support, for\n// back-compat so nothing downstream breaks.\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport type { InvokeOptions } from \"@archstone/provider-rest\";\nimport { buildRegistry } from \"./registry\";\nimport { toolDefinitions, createMcpServer } from \"./server\";\n\nexport { toolName, inputJsonSchema, objectJsonSchema } from \"@archstone/emitter-support\";\n/** #44: the audit sink surface, re-exported so a deployer wiring `serveStdio`/`createMcpServer`\n * imports it from the package they already depend on. See `AuditSink`'s own doc comment for\n * the fire-and-forget contract and for the statement that the trail is best-effort and lossy. */\nexport {\n jsonLinesAuditSink,\n REDACTED,\n LIFECYCLE_BLOCKED_REASON,\n LIFECYCLE_UNEVALUATABLE_REASON,\n} from \"@archstone/emitter-support\";\nexport type {\n AuditSink,\n AuditWritable,\n ExecutionRecord,\n ExecutionStatus,\n ExecutionPhase,\n ExecutionConsumer,\n ExecutionDenialReason,\n} from \"@archstone/emitter-support\";\nexport * from \"./server\";\n\n/**\n * Build the registry from a manifest dir and serve it over stdio (blocks).\n *\n * `invoke` (ADD-32) is forwarded verbatim to `createMcpServer` — this closes a real gap: prior\n * to #32, `serveStdio` passed NO `InvokeOptions` at all, so nothing (not `env`, not `caller`)\n * could ever be injected here. A stdio server is one child process per conversation (Claude\n * Desktop's model) — single-process, single-user by construction — so a static per-process\n * `invoke.caller` is architecturally sound here, unlike the HTTP case (`createHttpHandler`'s\n * `resolveCaller`, which must vary per inbound request).\n */\nexport async function serveStdio(dir: string, invoke?: InvokeOptions): Promise<void> {\n const built = buildRegistry(dir);\n if (!built.ok || !built.registry) {\n // stdout is the MCP channel — all human output goes to stderr.\n console.error(`archstone: cannot serve '${dir}' — manifest invalid:`);\n for (const i of built.issues) console.error(` - ${i.file}: ${i.message}`);\n for (const d of built.diagnostics.filter((x) => x.severity === \"error\")) console.error(` - ${d.message}`);\n process.exit(1);\n }\n const tools = toolDefinitions(built.registry);\n console.error(`archstone: serving ${tools.length} tool(s) over stdio: ${tools.map((t) => t.name).join(\", \") || \"(none)\"}`);\n const server = createMcpServer(built.registry, invoke);\n await server.connect(new StdioServerTransport());\n}\n","// @archstone/runtime — Adoption planner (ADD-117 / ADR-0008).\n//\n// Turns a field ADD-114's drift NAMED into a field the manifest DECLARES. This module is the\n// pure half: it decides what could be adopted and why each rejected candidate was rejected.\n// It touches no disk, asks no human, and mutates nothing — the CLI owns all three (D-6), which\n// is what lets every rule below be tested without a temp directory or a terminal.\n//\n// The rules exist because ADR-0008 draws a hard line: an undeclared field never reaches a\n// model. Adoption is the ONLY way across that line, and it is deliberately a human act.\n\nimport type { IRTool, IRResourceRegistry, JsonType, SemanticType, ShapeDiff } from \"@archstone/compiler\";\n\n/** Why a named path cannot be adopted. Stated, never silently skipped — a candidate that\n * disappears from the report reads as \"there was nothing there\". */\nexport type AdoptionRefusal =\n | \"outside-collection\"\n | \"nested\"\n | \"no-boolean-type\"\n | \"not-a-leaf\"\n | \"already-declared\";\n\nexport interface AdoptableField {\n adoptable: true;\n /** The JSONPath the drift reported, e.g. `$.stays[].boardType`. */\n path: string;\n /** The resource field name it would become, e.g. `boardType`. */\n field: string;\n /** The path written into the binding's `response.map`, relative to the collection item. */\n itemPath: string;\n observed: JsonType;\n /** What it is declared as. See ADD-117 §3 — the table is deliberately dull. */\n semantic: SemanticType;\n}\n\nexport interface UnadoptableField {\n adoptable: false;\n path: string;\n observed: JsonType;\n reason: AdoptionRefusal;\n /** One sentence an operator can act on, or at least understand. */\n detail: string;\n}\n\nexport type AdoptionCandidate = AdoptableField | UnadoptableField;\n\nexport interface AdoptionPlan {\n capabilityId: string;\n /** The resource the binding's `response:` maps onto — the file a field would be added to. */\n resource?: string;\n candidates: AdoptionCandidate[];\n}\n\n/**\n * ADD-117 §3. An observed JSON type becomes exactly one CDL semantic type, or nothing.\n *\n * Deliberately NOT clever. `string` does not become `date` however much a value looked like\n * one, because the shape records types and never values — there is nothing here to\n * pattern-match, and inferring a date from a field NAME is exactly the guess this project\n * refuses to make. `number` does not become `money`, because whether a number is a price is a\n * business fact and `money` carries a currency this field does not have. The human can widen\n * either afterwards; the manifest is theirs.\n */\nfunction semanticFor(observed: JsonType): SemanticType | undefined {\n if (observed === \"string\") return \"text\";\n if (observed === \"number\") return \"quantity\";\n return undefined;\n}\n\nfunction refusalDetail(reason: AdoptionRefusal, observed: JsonType): string {\n switch (reason) {\n case \"outside-collection\":\n return \"outside the collection this capability maps; it is not a field of the resource\";\n case \"nested\":\n return \"nested, or a provider key containing a dot — indistinguishable here; either way the resource field would have to be another resource, which adoption does not create\";\n case \"no-boolean-type\":\n return \"CDL has no boolean semantic type, and declaring it as text would state a lie about the shape\";\n case \"not-a-leaf\":\n return `observed as ${observed}, which is a structure rather than a value`;\n case \"already-declared\":\n return \"already declared by this capability\";\n }\n}\n\n/**\n * The path prefix every field of one collection item shares.\n *\n * A binding's `collection` is a JSONPath over the payload (`$.stays[*]`); a recorded shape\n * flattens an array to its first element (`$.stays[]`). One translation, here, rather than two\n * conventions leaking into every comparison below. A capability with no `collection` maps a\n * single object, whose fields hang off the root.\n */\nfunction itemPrefix(collection: string | undefined): string {\n if (!collection) return \"$\";\n return collection.replace(/\\[\\*\\]/g, \"[]\");\n}\n\n/**\n * What could be declared, and why the rest could not.\n *\n * Only `drift.added` is considered: `removed` is a loss with nothing to declare, and `retyped`\n * needs a judgment no shape comparison can make — is `price_per_night` the old `pricePerNight`,\n * or a new field that happens to look like it? ADR-0008 puts both out of scope, and the diff\n * still names them so a human can act.\n */\nexport function planAdoption(tool: IRTool, drift: ShapeDiff, resources: IRResourceRegistry): AdoptionPlan {\n const mapping = tool.response;\n if (!mapping) return { capabilityId: tool.id, candidates: [] };\n\n const prefix = itemPrefix(mapping.collection);\n const declared = new Set<string>([\n ...mapping.fields.map((f) => f.name),\n ...(resources[mapping.resource] ?? []).map((f) => f.name),\n ]);\n\n const refuse = (path: string, observed: JsonType, reason: AdoptionRefusal): UnadoptableField => ({\n adoptable: false,\n path,\n observed,\n reason,\n detail: refusalDetail(reason, observed),\n });\n\n const candidates = drift.added.map<AdoptionCandidate>(({ path, type: observed }) => {\n if (!path.startsWith(`${prefix}.`)) return refuse(path, observed, \"outside-collection\");\n const rest = path.slice(prefix.length + 1);\n if (rest.includes(\"[\")) return refuse(path, observed, \"nested\");\n // A dot here is either a nested object (`address.city`) or a single provider key that\n // contains a dot. Those two are INDISTINGUISHABLE in this flattened space — the same\n // collision `describeShape` documents — so there is one refusal, not a coin flip between\n // two, and its detail says so. Either way the answer is the same: not adopted.\n if (rest.includes(\".\")) return refuse(path, observed, \"nested\");\n if (observed === \"boolean\") return refuse(path, observed, \"no-boolean-type\");\n const semantic = semanticFor(observed);\n if (!semantic) return refuse(path, observed, \"not-a-leaf\");\n if (declared.has(rest)) return refuse(path, observed, \"already-declared\");\n return { adoptable: true, path, field: rest, itemPath: `$.${rest}`, observed, semantic };\n });\n\n return { capabilityId: tool.id, resource: mapping.resource, candidates };\n}\n\n/** The adoptable candidates, in the order they would be offered. */\nexport function adoptable(plan: AdoptionPlan): AdoptableField[] {\n return plan.candidates.filter((c): c is AdoptableField => c.adoptable);\n}\n","// @archstone/runtime — file-backed audit retention.\n//\n// `@archstone/emitter-support` ships `jsonLinesAuditSink`: one line, one write, no retention,\n// because that package imports no `node:` module and must stay usable on an edge runtime. A\n// self-hosted deployment that has to *keep* its audit trail — the whole point of an evidentiary\n// log — then has to solve rotation itself, which is where this lives: `runtime` already reads\n// the filesystem (`registry`), so fs belongs here and only here.\n//\n// Deliberately NOT a shipping/collector sink. Sending records to Splunk, an OTLP endpoint or an\n// S3 bucket is HTTP, and HTTP appears in exactly one package in this repository (`providers/rest`)\n// — a rule worth more than the convenience. Wrap this sink, or write your own; a sink is a\n// function.\n\nimport { appendFileSync, existsSync, mkdirSync, renameSync, statSync, unlinkSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { AuditSink, ExecutionRecord } from \"@archstone/emitter-support\";\n\nexport interface RotatingFileAuditSinkOptions {\n /** Where the live file goes. Rotated generations are `<path>.1` … `<path>.<maxFiles>`. */\n path: string;\n /** Rotate once the live file would exceed this. Default 64 MiB. */\n maxBytes?: number;\n /** How many rotated generations to keep. The oldest is deleted on rotation. Default 10. */\n maxFiles?: number;\n}\n\nconst DEFAULT_MAX_BYTES = 64 * 1024 * 1024;\nconst DEFAULT_MAX_FILES = 10;\n\n/**\n * A JSON Lines audit sink that rotates by size and bounds its own disk use.\n *\n * **Size, not time.** An audit stream grows with invocations, not with the clock: hourly\n * rotation on a quiet deployment produces a directory of empty files, and on a busy one produces\n * a single file that outgrows the disk between rotations. Size-based rotation gives the one\n * guarantee an operator actually needs — total footprint is at most\n * `maxBytes × (maxFiles + 1)`, computable before deployment and independent of traffic.\n *\n * **Synchronous, on purpose.** `appendFileSync` per record costs a syscall; a buffered writer\n * would be faster and would lose the last N records exactly when they matter most — a crash,\n * an OOM kill, a `SIGKILL` during an incident. An audit record still in a buffer when the\n * process dies is a record that never existed. Evidentiary logs trade throughput for durability;\n * if that trade is wrong for a deployment, wrap a buffered writer yourself and own the loss.\n *\n * **Single writer.** The live file's size is tracked in memory (seeded from `statSync` at\n * construction) so the common path is one `append` and no `stat`. Two processes appending to the\n * same path therefore rotate on each other's estimates — give each instance its own path, which\n * a shared volume makes trivial and which also keeps records attributable to an instance.\n *\n * Rotation is `rename`, so the live inode is replaced and no record is ever rewritten in place.\n * A failure to rotate (permissions, a full disk) surfaces as an ordinary sink failure:\n * `emitExecutionRecord` catches it, announces the loss on stderr, and the invocation itself is\n * unaffected — an audit backend must never be able to take the capability down.\n */\nexport function rotatingFileAuditSink(opts: RotatingFileAuditSinkOptions): AuditSink {\n const { path } = opts;\n const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;\n const maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;\n\n if (!path) throw new Error(\"rotatingFileAuditSink: `path` is required.\");\n if (!Number.isInteger(maxBytes) || maxBytes <= 0) {\n throw new Error(`rotatingFileAuditSink: maxBytes must be a positive integer, got ${String(maxBytes)}.`);\n }\n if (!Number.isInteger(maxFiles) || maxFiles < 1) {\n throw new Error(`rotatingFileAuditSink: maxFiles must be a positive integer, got ${String(maxFiles)}.`);\n }\n\n // Fail at wiring time, not at the first denied invocation: a deployer who mistyped the path\n // should learn now, while they are looking at the config, and not from a stream of caught\n // sink failures under load.\n mkdirSync(dirname(path), { recursive: true });\n\n let liveBytes = existsSync(path) ? statSync(path).size : 0;\n\n function rotate(): void {\n // Oldest first, so nothing is overwritten before it has been moved along.\n const oldest = `${path}.${maxFiles}`;\n if (existsSync(oldest)) unlinkSync(oldest);\n for (let i = maxFiles - 1; i >= 1; i--) {\n const from = `${path}.${i}`;\n if (existsSync(from)) renameSync(from, `${path}.${i + 1}`);\n }\n if (existsSync(path)) renameSync(path, `${path}.1`);\n liveBytes = 0;\n }\n\n return (record: ExecutionRecord) => {\n const line = `${JSON.stringify(record)}\\n`;\n const size = Buffer.byteLength(line);\n // A single record larger than the whole budget still gets written, to its own generation,\n // rather than being silently dropped: losing an oversized record is losing evidence, and a\n // deployer who sees one file over budget can raise maxBytes. Dropping it teaches nothing.\n if (liveBytes > 0 && liveBytes + size > maxBytes) rotate();\n appendFileSync(path, line);\n liveBytes += size;\n };\n}\n"],"mappings":"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","names":["Registry"]}
{"version":3,"sources":["../src/registry.ts","../src/mcp.ts","../src/adopt.ts","../src/audit-file.ts"],"sourcesContent":["// @archstone/runtime — Capability Registry (#5)\n//\n// The product kernel: capabilities queryable at runtime, indexed over the IR.\n// File-backed (no DB) — the IR is derived from manifests on disk. The MCP emitter\n// (#7) consumes this to list and resolve tools.\n//\n// `Registry` (index-only) moved to @archstone/emitter-support (ADD-0008 #27) — re-exported\n// here for back-compat so nothing downstream breaks. This file keeps the fs-touching\n// pipeline (`buildRegistry`), which is why the /http subpath (http.ts) never imports it.\n//\n// ADD-24 (#24): `buildRegistry` also optionally reads a conventional health-snapshot file\n// (`readHealthSnapshot`, below) — the ONE other fs-touching, network-free addition this ADD\n// makes. Binding health itself is never computed here (that's `archstone verify`'s own live\n// probe, ADD-18 D-5) — only its already-serialized `--json` output is read back.\n\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { load, type LoadResult, type LoadIssue } from \"@archstone/schema\";\nimport { validateSemantics, compile, type Diagnostic } from \"@archstone/compiler\";\nimport { Registry, type HealthStatus } from \"@archstone/emitter-support\";\n\nexport { Registry } from \"@archstone/emitter-support\";\n\n/** Conventional health-snapshot file, read once next to the manifest dir (ADD-24 D-8): the\n * operator/CI populates it by redirecting the ALREADY-shipped `archstone verify --json`\n * output here — no new serialization. `buildRegistry` reads it (fs, but no network — the\n * live probe stays exclusively `verify`'s, ADD-18 D-5) and hands the parsed map to\n * `Registry`, which composes it with each tool's lifecycle exposure (ADD-24 §7 step 5). */\nexport const HEALTH_SNAPSHOT_FILE = \".archstone-health.json\";\n\nconst HEALTH_STATUSES: ReadonlySet<string> = new Set([\"green\", \"yellow\", \"red\"]);\n\n/**\n * Parse the `{results: ToolVerification[]}` shape `archstone verify --json` already produces\n * (ADD-20) into a capabilityId -> HealthStatus map. Fail-open (ADD-24 D-9): a missing file, a\n * parse error, or a malformed/unexpected shape all return `undefined` — the caller then\n * proceeds with lifecycle-only exposure, never mistaking \"no snapshot\" for \"known bad\".\n *\n * #43 (ADD-43 D-14): an entry marked `policyDenied` is SKIPPED. A refusal that happened before\n * the call is not a health fact — no request was issued, so nothing about the backend's contract\n * was observed. Left in, it would reach `combineExposure` as a `red` and append\n * `\"binding health: red — the last contract verification failed\"` to the tool's agent-facing\n * description at the highest severity: a statement that never happened, shown to every caller\n * including permitted ones, making policy affect listing (which BR-36 forbids). Skipping leaves\n * the tool with NO health entry, which is exactly ADD-24 D-9's ratified posture — absent health\n * must never be manufactured into known-bad.\n */\nfunction readHealthSnapshot(dir: string): Map<string, HealthStatus> | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(join(dir, HEALTH_SNAPSHOT_FILE), \"utf8\"));\n } catch {\n return undefined; // absent, unreadable, or invalid JSON — fail-open\n }\n\n const results = (parsed as { results?: unknown } | null)?.results;\n if (!Array.isArray(results)) return undefined;\n\n const map = new Map<string, HealthStatus>();\n for (const r of results) {\n if (!r || typeof r !== \"object\") continue;\n // ADD-43 D-14: a policy denial is not a health reading — drop it rather than let it become\n // an agent-facing \"the last contract verification failed\" hint for a verification that\n // never ran. See this function's doc comment.\n if ((r as { policyDenied?: unknown }).policyDenied === true) continue;\n const capabilityId = (r as { capabilityId?: unknown }).capabilityId;\n const status = (r as { status?: unknown }).status;\n if (typeof capabilityId === \"string\" && typeof status === \"string\" && HEALTH_STATUSES.has(status)) {\n map.set(capabilityId, status as HealthStatus);\n }\n }\n return map;\n}\n\nexport interface BuildResult {\n ok: boolean;\n registry?: Registry;\n issues: LoadIssue[];\n diagnostics: Diagnostic[];\n}\n\n/**\n * File-backed pipeline: load (#2) → semantic-validate (#3) → compile (#4) → Registry (#5).\n * `registry` is present only when shapes are valid, there are no semantic errors, AND no\n * tool-name collision (ADD-30 D-2) — folded into this function's existing `diagnostics`/\n * `ok` contract (new `tool-name-collision` diagnostic code) rather than a new mechanism, so\n * `serveStdio`/`runServeHttp` (which already refuse to proceed on `!built.ok`) inherit the\n * gate for free.\n */\nexport function buildRegistry(dir: string): BuildResult {\n const model: LoadResult = load(dir);\n const diagnostics = validateSemantics(model);\n const hasErrors = diagnostics.some((d) => d.severity === \"error\");\n let ok = model.ok && !hasErrors;\n\n const registry = ok ? new Registry(compile(model), readHealthSnapshot(dir)) : undefined;\n if (registry) {\n for (const c of registry.toolNameCollisions) {\n ok = false;\n diagnostics.push({\n severity: \"error\",\n code: \"tool-name-collision\",\n message: `tool name '${c.name}' is ambiguous — capabilities ${c.ids.join(\", \")} all sanitize to it`,\n });\n }\n }\n\n return {\n ok,\n registry: ok ? registry : undefined,\n issues: model.issues,\n diagnostics,\n };\n}\n","// @archstone/runtime — MCP emitter (#7) — stdio entrypoint\n//\n// serveStdio builds the registry from disk (buildRegistry, registry.ts) and serves it over\n// stdio, the channel Claude Desktop uses. The fs-free MCP server construction\n// (toolDefinitions/callTool/createMcpServer) lives in ./server (ADD-0008 #27) — re-exported\n// here, alongside the semantic-lowering functions from @archstone/emitter-support, for\n// back-compat so nothing downstream breaks.\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport type { InvokeOptions } from \"@archstone/provider-rest\";\nimport { buildRegistry } from \"./registry\";\nimport { toolDefinitions, createMcpServer } from \"./server\";\n\nexport { toolName, inputJsonSchema, objectJsonSchema } from \"@archstone/emitter-support\";\n/** #44: the audit sink surface, re-exported so a deployer wiring `serveStdio`/`createMcpServer`\n * imports it from the package they already depend on. See `AuditSink`'s own doc comment for\n * the fire-and-forget contract and for the statement that the trail is best-effort and lossy. */\nexport {\n jsonLinesAuditSink,\n REDACTED,\n LIFECYCLE_BLOCKED_REASON,\n LIFECYCLE_UNEVALUATABLE_REASON,\n} from \"@archstone/emitter-support\";\nexport type {\n AuditSink,\n AuditWritable,\n ExecutionRecord,\n ExecutionStatus,\n ExecutionPhase,\n ExecutionConsumer,\n ExecutionDenialReason,\n} from \"@archstone/emitter-support\";\nexport * from \"./server\";\n\n/**\n * Build the registry from a manifest dir and serve it over stdio (blocks).\n *\n * `invoke` (ADD-32) is forwarded verbatim to `createMcpServer` — this closes a real gap: prior\n * to #32, `serveStdio` passed NO `InvokeOptions` at all, so nothing (not `env`, not `caller`)\n * could ever be injected here. A stdio server is one child process per conversation (Claude\n * Desktop's model) — single-process, single-user by construction — so a static per-process\n * `invoke.caller` is architecturally sound here, unlike the HTTP case (`createHttpHandler`'s\n * `resolveCaller`, which must vary per inbound request).\n */\nexport async function serveStdio(dir: string, invoke?: InvokeOptions): Promise<void> {\n const built = buildRegistry(dir);\n if (!built.ok || !built.registry) {\n // stdout is the MCP channel — all human output goes to stderr.\n console.error(`archstone: cannot serve '${dir}' — manifest invalid:`);\n for (const i of built.issues) console.error(` - ${i.file}: ${i.message}`);\n for (const d of built.diagnostics.filter((x) => x.severity === \"error\")) console.error(` - ${d.message}`);\n process.exit(1);\n }\n const tools = toolDefinitions(built.registry);\n console.error(`archstone: serving ${tools.length} tool(s) over stdio: ${tools.map((t) => t.name).join(\", \") || \"(none)\"}`);\n const server = createMcpServer(built.registry, invoke);\n await server.connect(new StdioServerTransport());\n}\n","// @archstone/runtime — Adoption planner (ADD-117 / ADR-0008).\n//\n// Turns a field ADD-114's drift NAMED into a field the manifest DECLARES. This module is the\n// pure half: it decides what could be adopted and why each rejected candidate was rejected.\n// It touches no disk, asks no human, and mutates nothing — the CLI owns all three (D-6), which\n// is what lets every rule below be tested without a temp directory or a terminal.\n//\n// The rules exist because ADR-0008 draws a hard line: an undeclared field never reaches a\n// model. Adoption is the ONLY way across that line, and it is deliberately a human act.\n\nimport type { IRTool, IRResourceRegistry, JsonType, SemanticType, ShapeDiff } from \"@archstone/compiler\";\n\n/** Why a named path cannot be adopted. Stated, never silently skipped — a candidate that\n * disappears from the report reads as \"there was nothing there\". */\nexport type AdoptionRefusal =\n | \"outside-collection\"\n | \"nested\"\n | \"no-boolean-type\"\n | \"not-a-leaf\"\n | \"already-declared\";\n\nexport interface AdoptableField {\n adoptable: true;\n /** The JSONPath the drift reported, e.g. `$.stays[].boardType`. */\n path: string;\n /** The resource field name it would become, e.g. `boardType`. */\n field: string;\n /** The path written into the binding's `response.map`, relative to the collection item. */\n itemPath: string;\n observed: JsonType;\n /** What it is declared as. See ADD-117 §3 — the table is deliberately dull. */\n semantic: SemanticType;\n}\n\nexport interface UnadoptableField {\n adoptable: false;\n path: string;\n observed: JsonType;\n reason: AdoptionRefusal;\n /** One sentence an operator can act on, or at least understand. */\n detail: string;\n}\n\nexport type AdoptionCandidate = AdoptableField | UnadoptableField;\n\nexport interface AdoptionPlan {\n capabilityId: string;\n /** The resource the binding's `response:` maps onto — the file a field would be added to. */\n resource?: string;\n candidates: AdoptionCandidate[];\n}\n\n/**\n * ADD-117 §3. An observed JSON type becomes exactly one CDL semantic type, or nothing.\n *\n * Deliberately NOT clever. `string` does not become `date` however much a value looked like\n * one, because the shape records types and never values — there is nothing here to\n * pattern-match, and inferring a date from a field NAME is exactly the guess this project\n * refuses to make. `number` does not become `money`, because whether a number is a price is a\n * business fact and `money` carries a currency this field does not have. The human can widen\n * either afterwards; the manifest is theirs.\n */\nfunction semanticFor(observed: JsonType): SemanticType | undefined {\n if (observed === \"string\") return \"text\";\n if (observed === \"number\") return \"quantity\";\n return undefined;\n}\n\nfunction refusalDetail(reason: AdoptionRefusal, observed: JsonType): string {\n switch (reason) {\n case \"outside-collection\":\n return \"outside the collection this capability maps; it is not a field of the resource\";\n case \"nested\":\n return \"nested, or a provider key containing a dot — indistinguishable here; either way the resource field would have to be another resource, which adoption does not create\";\n case \"no-boolean-type\":\n return \"CDL has no boolean semantic type, and declaring it as text would state a lie about the shape\";\n case \"not-a-leaf\":\n return `observed as ${observed}, which is a structure rather than a value`;\n case \"already-declared\":\n return \"already declared by this capability\";\n }\n}\n\n/**\n * The path prefix every field of one collection item shares.\n *\n * A binding's `collection` is a JSONPath over the payload (`$.stays[*]`); a recorded shape\n * flattens an array to its first element (`$.stays[]`). One translation, here, rather than two\n * conventions leaking into every comparison below. A capability with no `collection` maps a\n * single object, whose fields hang off the root.\n */\nfunction itemPrefix(collection: string | undefined): string {\n if (!collection) return \"$\";\n return collection.replace(/\\[\\*\\]/g, \"[]\");\n}\n\n/**\n * What could be declared, and why the rest could not.\n *\n * Only `drift.added` is considered: `removed` is a loss with nothing to declare, and `retyped`\n * needs a judgment no shape comparison can make — is `price_per_night` the old `pricePerNight`,\n * or a new field that happens to look like it? ADR-0008 puts both out of scope, and the diff\n * still names them so a human can act.\n */\nexport function planAdoption(tool: IRTool, drift: ShapeDiff, resources: IRResourceRegistry): AdoptionPlan {\n const mapping = tool.response;\n if (!mapping) return { capabilityId: tool.id, candidates: [] };\n\n const prefix = itemPrefix(mapping.collection);\n const declared = new Set<string>([\n ...mapping.fields.map((f) => f.name),\n ...(resources[mapping.resource] ?? []).map((f) => f.name),\n ]);\n\n const refuse = (path: string, observed: JsonType, reason: AdoptionRefusal): UnadoptableField => ({\n adoptable: false,\n path,\n observed,\n reason,\n detail: refusalDetail(reason, observed),\n });\n\n const candidates = drift.added.map<AdoptionCandidate>(({ path, type: observed }) => {\n if (!path.startsWith(`${prefix}.`)) return refuse(path, observed, \"outside-collection\");\n const rest = path.slice(prefix.length + 1);\n if (rest.includes(\"[\")) return refuse(path, observed, \"nested\");\n // A dot here is either a nested object (`address.city`) or a single provider key that\n // contains a dot. Those two are INDISTINGUISHABLE in this flattened space — the same\n // collision `describeShape` documents — so there is one refusal, not a coin flip between\n // two, and its detail says so. Either way the answer is the same: not adopted.\n if (rest.includes(\".\")) return refuse(path, observed, \"nested\");\n if (observed === \"boolean\") return refuse(path, observed, \"no-boolean-type\");\n const semantic = semanticFor(observed);\n if (!semantic) return refuse(path, observed, \"not-a-leaf\");\n if (declared.has(rest)) return refuse(path, observed, \"already-declared\");\n return { adoptable: true, path, field: rest, itemPath: `$.${rest}`, observed, semantic };\n });\n\n return { capabilityId: tool.id, resource: mapping.resource, candidates };\n}\n\n/** The adoptable candidates, in the order they would be offered. */\nexport function adoptable(plan: AdoptionPlan): AdoptableField[] {\n return plan.candidates.filter((c): c is AdoptableField => c.adoptable);\n}\n","// @archstone/runtime — file-backed audit retention.\n//\n// `@archstone/emitter-support` ships `jsonLinesAuditSink`: one line, one write, no retention,\n// because that package imports no `node:` module and must stay usable on an edge runtime. A\n// self-hosted deployment that has to *keep* its audit trail — the whole point of an evidentiary\n// log — then has to solve rotation itself, which is where this lives: `runtime` already reads\n// the filesystem (`registry`), so fs belongs here and only here.\n//\n// Deliberately NOT a shipping/collector sink. Sending records to Splunk, an OTLP endpoint or an\n// S3 bucket is HTTP, and HTTP appears in exactly one package in this repository (`providers/rest`)\n// — a rule worth more than the convenience. Wrap this sink, or write your own; a sink is a\n// function.\n\nimport { appendFileSync, existsSync, mkdirSync, renameSync, statSync, unlinkSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { AuditSink, ExecutionRecord } from \"@archstone/emitter-support\";\n\nexport interface RotatingFileAuditSinkOptions {\n /** Where the live file goes. Rotated generations are `<path>.1` … `<path>.<maxFiles>`. */\n path: string;\n /** Rotate once the live file would exceed this. Default 64 MiB. */\n maxBytes?: number;\n /** How many rotated generations to keep. The oldest is deleted on rotation. Default 10. */\n maxFiles?: number;\n}\n\nconst DEFAULT_MAX_BYTES = 64 * 1024 * 1024;\nconst DEFAULT_MAX_FILES = 10;\n\n/**\n * A JSON Lines audit sink that rotates by size and bounds its own disk use.\n *\n * **Size, not time.** An audit stream grows with invocations, not with the clock: hourly\n * rotation on a quiet deployment produces a directory of empty files, and on a busy one produces\n * a single file that outgrows the disk between rotations. Size-based rotation gives the one\n * guarantee an operator actually needs — total footprint is at most\n * `maxBytes × (maxFiles + 1)`, computable before deployment and independent of traffic.\n *\n * **Synchronous, on purpose.** `appendFileSync` per record costs a syscall; a buffered writer\n * would be faster and would lose the last N records exactly when they matter most — a crash,\n * an OOM kill, a `SIGKILL` during an incident. An audit record still in a buffer when the\n * process dies is a record that never existed. Evidentiary logs trade throughput for durability;\n * if that trade is wrong for a deployment, wrap a buffered writer yourself and own the loss.\n *\n * **Single writer.** The live file's size is tracked in memory (seeded from `statSync` at\n * construction) so the common path is one `append` and no `stat`. Two processes appending to the\n * same path therefore rotate on each other's estimates — give each instance its own path, which\n * a shared volume makes trivial and which also keeps records attributable to an instance.\n *\n * Rotation is `rename`, so the live inode is replaced and no record is ever rewritten in place.\n * A failure to rotate (permissions, a full disk) surfaces as an ordinary sink failure:\n * `emitExecutionRecord` catches it, announces the loss on stderr, and the invocation itself is\n * unaffected — an audit backend must never be able to take the capability down.\n */\nexport function rotatingFileAuditSink(opts: RotatingFileAuditSinkOptions): AuditSink {\n const { path } = opts;\n const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;\n const maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;\n\n if (!path) throw new Error(\"rotatingFileAuditSink: `path` is required.\");\n if (!Number.isInteger(maxBytes) || maxBytes <= 0) {\n throw new Error(`rotatingFileAuditSink: maxBytes must be a positive integer, got ${String(maxBytes)}.`);\n }\n if (!Number.isInteger(maxFiles) || maxFiles < 1) {\n throw new Error(`rotatingFileAuditSink: maxFiles must be a positive integer, got ${String(maxFiles)}.`);\n }\n\n // Fail at wiring time, not at the first denied invocation: a deployer who mistyped the path\n // should learn now, while they are looking at the config, and not from a stream of caught\n // sink failures under load.\n mkdirSync(dirname(path), { recursive: true });\n\n let liveBytes = existsSync(path) ? statSync(path).size : 0;\n\n function rotate(): void {\n // Oldest first, so nothing is overwritten before it has been moved along.\n const oldest = `${path}.${maxFiles}`;\n if (existsSync(oldest)) unlinkSync(oldest);\n for (let i = maxFiles - 1; i >= 1; i--) {\n const from = `${path}.${i}`;\n if (existsSync(from)) renameSync(from, `${path}.${i + 1}`);\n }\n if (existsSync(path)) renameSync(path, `${path}.1`);\n liveBytes = 0;\n }\n\n return (record: ExecutionRecord) => {\n const line = `${JSON.stringify(record)}\\n`;\n const size = Buffer.byteLength(line);\n // A single record larger than the whole budget still gets written, to its own generation,\n // rather than being silently dropped: losing an oversized record is losing evidence, and a\n // deployer who sees one file over budget can raise maxBytes. Dropping it teaches nothing.\n if (liveBytes > 0 && liveBytes + size > maxBytes) rotate();\n appendFileSync(path, line);\n liveBytes += size;\n };\n}\n"],"mappings":"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","names":["Registry"]}
{
"name": "@archstone/runtime",
"version": "0.15.0",
"version": "0.16.0",
"private": false,

@@ -50,6 +50,6 @@ "type": "module",

"@modelcontextprotocol/sdk": "^1.12.0",
"@archstone/emitter-support": "0.15.0",
"@archstone/provider-rest": "0.15.0",
"@archstone/compiler": "0.15.0",
"@archstone/schema": "0.15.0"
"@archstone/compiler": "0.16.0",
"@archstone/emitter-support": "0.16.0",
"@archstone/provider-rest": "0.16.0",
"@archstone/schema": "0.16.0"
},

@@ -56,0 +56,0 @@ "devDependencies": {

// src/server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import {
inputJsonSchema,
objectJsonSchema,
applyResponseMapping,
contractViolationMessage,
evaluatePolicy,
evaluateRateLimit,
auditNow,
buildExecutionRecord,
emitExecutionRecord,
LIFECYCLE_BLOCKED_REASON,
LIFECYCLE_UNEVALUATABLE_REASON
} from "@archstone/emitter-support";
import { invokeRest } from "@archstone/provider-rest";
function toolDefinitions(registry) {
const resources = registry.ir.resources;
return registry.invocableTools().filter(({ tool: t }) => registry.getExposure(t.id).listed).map(({ name, tool: t }) => {
const hint = registry.getExposure(t.id).hint;
const def = {
name,
description: hint ? `${t.description} (${hint.text})` : t.description,
inputSchema: inputJsonSchema(t.input, resources)
};
if (t.output.length > 0) def.outputSchema = objectJsonSchema(t.output, resources);
return def;
});
}
var CONTRACT_VIOLATION_META_KEY = "dev.archstone/contract_violation";
var LIFECYCLE_BLOCKED_META_KEY = "dev.archstone/lifecycle_blocked";
var LIFECYCLE_UNEVALUATABLE_META_KEY = "dev.archstone/lifecycle_unevaluatable";
var POLICY_DENIED_META_KEY = "dev.archstone/policy_denied";
async function callTool(registry, name, args, opts) {
const tool = registry.getCapability(name);
if (!tool) {
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
}
const auditSink = opts?.auditSink;
const startedAt = auditSink ? auditNow() : "";
const audit = (status) => {
if (!auditSink) return;
emitExecutionRecord(
auditSink,
buildExecutionRecord({
tool,
input: args,
// Fixed by this call site, never host-configurable: an auditor must be able to trust
// that a record claiming `mcp` came from the MCP path. `mcpHandler` mounts this same
// path and therefore also records `mcp` — the value names the protocol surface the call
// arrived on, not the npm package that mounted it.
consumer: "mcp",
caller: opts?.caller,
sessionId: opts?.sessionId,
workflowId: opts?.workflowId,
startedAt,
status
})
);
};
const exposure = registry.getExposure(tool.id);
if (!exposure.invocable) {
if (exposure.blockedReason === "unevaluatable") {
const text2 = `capability '${tool.id}' declares a lifecycle this build does not recognize and cannot evaluate \u2014 refusing (fail-closed).`;
audit({ phase: "denied", message: text2, denialReason: LIFECYCLE_UNEVALUATABLE_REASON });
return {
content: [{ type: "text", text: text2 }],
_meta: {
[LIFECYCLE_UNEVALUATABLE_META_KEY]: { error: "lifecycle_unevaluatable", capability: tool.id, lifecycle: tool.lifecycle }
},
isError: true
};
}
const text = `capability '${tool.id}' is retired and can no longer be invoked.`;
audit({ phase: "denied", message: text, denialReason: LIFECYCLE_BLOCKED_REASON });
return {
content: [{ type: "text", text }],
_meta: { [LIFECYCLE_BLOCKED_META_KEY]: { error: "lifecycle_blocked", capability: tool.id, lifecycle: tool.lifecycle } },
isError: true
};
}
const decision = opts?.callerResolutionFailed ? {
allowed: false,
denial: {
reason: "policy_unevaluatable",
message: `capability '${tool.id}' could not be evaluated \u2014 caller identity could not be established (resolveCaller failed) \u2014 refusing (fail-closed).`
}
} : evaluatePolicy(tool, {
principal: opts?.caller?.principal,
credentialPresent: opts?.caller?.accessToken !== void 0
});
if (!decision.allowed) {
audit({ phase: "denied", message: decision.denial.message, denialReason: decision.denial.reason });
return {
content: [{ type: "text", text: decision.denial.message }],
_meta: {
[POLICY_DENIED_META_KEY]: {
error: "policy_denied",
// `tool.id` — the unsanitized CDL id, never the MCP-sanitized advertised `name`
// lookup key (BR-28, mirroring ADD-19 and ADD-30 BR-7).
capability: tool.id,
reason: decision.denial.reason
}
},
isError: true
};
}
const rateDecision = await evaluateRateLimit(tool, { principal: opts?.caller?.principal }, opts?.rateLimitCounter);
if (!rateDecision.allowed) {
audit({ phase: "denied", message: rateDecision.denial.message, denialReason: rateDecision.denial.reason });
return {
content: [{ type: "text", text: rateDecision.denial.message }],
_meta: {
[POLICY_DENIED_META_KEY]: {
error: "policy_denied",
capability: tool.id,
reason: rateDecision.denial.reason
}
},
isError: true
};
}
const result = await invokeRest(tool, args, opts);
if (!result.ok) {
const text = result.error ?? "invocation failed";
audit({ phase: "failed", message: text });
return { content: [{ type: "text", text }], isError: true };
}
if (tool.response) {
const mapped = applyResponseMapping(tool, result.data, registry.ir.resources);
if (mapped.status === "violation") {
const missing = mapped.missing ?? [];
const text = contractViolationMessage(tool.id, missing);
audit({ phase: "failed", message: text });
return {
content: [{ type: "text", text }],
_meta: { [CONTRACT_VIOLATION_META_KEY]: { error: "contract_violation", capability: tool.id, missing } },
isError: true
};
}
const content = [{ type: "text", text: JSON.stringify(mapped.data, null, 2) }];
if (mapped.status === "degraded") {
content.push({ type: "text", text: `note: optional field(s) absent (degraded): ${(mapped.degraded ?? []).join(", ")}` });
}
audit({ phase: "succeeded" });
return { content, structuredContent: mapped.data, isError: false };
}
const out = { content: [{ type: "text", text: JSON.stringify(result.data ?? null, null, 2) }], isError: false };
if (tool.output.length > 0) {
const data = result.data;
if (data && typeof data === "object" && !Array.isArray(data)) {
out.structuredContent = data;
}
}
audit({ phase: "succeeded" });
return out;
}
function createMcpServer(registry, opts) {
const server = new Server({ name: "archstone", version: "0" }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions(registry) }));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const args = req.params.arguments ?? {};
const result = await callTool(registry, req.params.name, args, opts);
return result;
});
return server;
}
export {
toolDefinitions,
CONTRACT_VIOLATION_META_KEY,
LIFECYCLE_BLOCKED_META_KEY,
LIFECYCLE_UNEVALUATABLE_META_KEY,
POLICY_DENIED_META_KEY,
callTool,
createMcpServer
};
//# sourceMappingURL=chunk-YFNW2UHK.js.map
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["// @archstone/runtime — MCP server construction (fs-free)\n//\n// Builds an MCP Server from a Registry and routes invocations through the REST provider\n// (#6). This is the ONLY place the MCP SDK appears (alongside stdio's transport wiring in\n// mcp.ts and the /http subpath's transport wiring in http.ts) — semantic-type → JSON-Schema\n// lowering itself now lives in @archstone/emitter-support (ADD-0008 #27), never here.\n//\n// Extracted out of mcp.ts (ADD-0008 #27) specifically so this module's graph never reaches\n// registry.ts's buildRegistry/@archstone/schema `load()` (the fs edge) — only stdio's\n// `serveStdio` (mcp.ts) needs disk access. `http.ts` (the /http subpath) imports only this\n// file, so a consumer depending on that subpath alone stays fs-free.\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n Registry,\n inputJsonSchema,\n objectJsonSchema,\n applyResponseMapping,\n contractViolationMessage,\n evaluatePolicy,\n evaluateRateLimit,\n auditNow,\n buildExecutionRecord,\n emitExecutionRecord,\n LIFECYCLE_BLOCKED_REASON,\n LIFECYCLE_UNEVALUATABLE_REASON,\n type ExecutionStatus,\n type PolicyDecision,\n} from \"@archstone/emitter-support\";\nimport { invokeRest, type InvokeOptions } from \"@archstone/provider-rest\";\n\ntype JsonSchema = Record<string, unknown>;\n\nexport interface McpToolDef {\n name: string;\n description: string;\n inputSchema: JsonSchema;\n outputSchema?: JsonSchema;\n}\n\n/** The MCP tool list: only invocable (bound) capabilities become tools — Registry's\n * `invocableTools()` (ADD-30 D-3) is the single source of truth shared by this function\n * (what's listed) and `callTool` (what's resolvable, via `getCapability`), keeping the two\n * consistent. Input and output fields lower against the IR resource registry, so a\n * `collection: Stay` output emits a typed, described `outputSchema` (not a bare\n * `{type:object}`).\n *\n * ADD-24: a bound tool whose combined exposure (`registry.getExposure`, lifecycle + optional\n * health) is `listed:false` (lifecycle `experimental`/`retired`) is dropped from the returned\n * list entirely — unlisted, per D-10, though `experimental` remains callable by id (see\n * `callTool`). A tool carrying a `hint` (beta/deprecated, or a yellow/red health reading) has\n * its text appended to `description` — the only MCP-specific rendering of the neutral\n * exposure the emitter-support layer computed. */\nexport function toolDefinitions(registry: Registry): McpToolDef[] {\n const resources = registry.ir.resources;\n return registry\n .invocableTools()\n .filter(({ tool: t }) => registry.getExposure(t.id).listed)\n .map(({ name, tool: t }) => {\n const hint = registry.getExposure(t.id).hint;\n const def: McpToolDef = {\n name,\n description: hint ? `${t.description} (${hint.text})` : t.description,\n inputSchema: inputJsonSchema(t.input, resources),\n };\n if (t.output.length > 0) def.outputSchema = objectJsonSchema(t.output, resources);\n return def;\n });\n}\n\nexport interface CallResult {\n content: { type: \"text\"; text: string }[];\n structuredContent?: Record<string, unknown>;\n isError: boolean;\n _meta?: Record<string, unknown>;\n}\n\n/** #19 ADD-19 Rev 2 D-6: the namespaced `_meta` key a VIOLATION result's structured error\n * object is carried under. Never populated on `structuredContent` (D-3′) — the reference\n * MCP SDK client validates `structuredContent` against `outputSchema` whenever the tool\n * declares one, regardless of `isError`, so a non-conforming error object there crashes the\n * client. `_meta` is untouched by that validation and passes through the client's zod parse\n * unstripped (`ResultSchema`/`RequestMetaSchema` are `z.looseObject`). */\nexport const CONTRACT_VIOLATION_META_KEY = \"dev.archstone/contract_violation\";\n\n/** ADD-24 D-11: the namespaced `_meta` key a `retired` (or otherwise `invocable:false`)\n * tool's rejection is carried under — reuses `CONTRACT_VIOLATION_META_KEY`'s precedent\n * (ADD-19 Rev 2 D-3′/D-6) verbatim: never `structuredContent`, so the reference SDK client's\n * unconditional `structuredContent`-against-`outputSchema` validation never sees it. A\n * distinct key (not `CONTRACT_VIOLATION_META_KEY`) so a client distinguishes \"this call was\n * blocked before any connector work\" from \"the provider's response violated the contract\". */\nexport const LIFECYCLE_BLOCKED_META_KEY = \"dev.archstone/lifecycle_blocked\";\n\n/** ADD-56 D-2/OQ-56-B: the namespaced `_meta` key an unrecognized-lifecycle rejection is\n * carried under — a DISTINCT key from `LIFECYCLE_BLOCKED_META_KEY`, mirroring\n * `POLICY_DENIED_META_KEY` vs. `LIFECYCLE_BLOCKED_META_KEY` being genuinely distinct keys\n * rather than one key with a varying `error` string inside it. `retired` (a governance\n * refusal) and an unrecognized `lifecycle` (a compatibility refusal) are different facts with\n * different remediations and must be trivially distinguishable to a client — see\n * `exposure.ts`'s `Exposure.blockedReason` doc comment. */\nexport const LIFECYCLE_UNEVALUATABLE_META_KEY = \"dev.archstone/lifecycle_unevaluatable\";\n\n/** #43 ADD-43: the namespaced `_meta` key a POLICY denial is carried under — the third use of\n * the ADD-19 Rev 2 D-3′/D-6 precedent, verbatim: never `structuredContent`, because the\n * reference SDK client validates that against the tool's `outputSchema` unconditionally (not\n * gated on `isError`) and an error object there crashes it. A distinct key from the other two\n * so a client can tell \"refused by policy before any connector work\" from \"blocked by\n * lifecycle\" from \"the provider's response violated the contract\" — the three are mutually\n * exclusive on one call (BR-27). The object discloses the reason code and the capability id\n * and NOTHING about the policy itself: no metadata.id, no allow/deny entry, no other\n * principal's identifier (BR-30, Rule #7 — the MCP client is the untrusted side). */\nexport const POLICY_DENIED_META_KEY = \"dev.archstone/policy_denied\";\n\n/** Route an MCP tool call to the REST provider and format the result as MCP content. */\nexport async function callTool(\n registry: Registry,\n name: string,\n args: Record<string, unknown>,\n opts?: InvokeOptions,\n): Promise<CallResult> {\n const tool = registry.getCapability(name);\n if (!tool) {\n // #44: NO audit record. `metadata.capabilityId` is required and the only value available\n // here is an unvalidated, caller-chosen string that is not a CDL id — writing it there\n // would put unbounded attacker-controlled values into the audit log's primary correlation\n // key, in a file a compliance process treats as evidence. An `Execution` record audits a\n // capability invocation attempt; a call naming no capability is a protocol-level event and\n // belongs to the host's own mount-point instrumentation. Named residual, deliberately\n // accepted: tool-name probing (and the collision defence, which also lands here) is\n // invisible to the audit trail.\n return { content: [{ type: \"text\", text: `unknown tool: ${name}` }], isError: true };\n }\n\n // #44: the attempt clock starts HERE — before the exposure gate and before policy evaluation,\n // so every refused attempt still carries a real `startedAt` and can be placed on a timeline.\n // With no sink configured this is a strict no-op: no clock read, no id, no record, no\n // allocation, and every result byte-for-byte what it was before this increment.\n const auditSink = opts?.auditSink;\n const startedAt = auditSink ? auditNow() : \"\";\n const audit = (status: ExecutionStatus): void => {\n if (!auditSink) return;\n emitExecutionRecord(\n auditSink,\n buildExecutionRecord({\n tool,\n input: args,\n // Fixed by this call site, never host-configurable: an auditor must be able to trust\n // that a record claiming `mcp` came from the MCP path. `mcpHandler` mounts this same\n // path and therefore also records `mcp` — the value names the protocol surface the call\n // arrived on, not the npm package that mounted it.\n consumer: \"mcp\",\n caller: opts?.caller,\n sessionId: opts?.sessionId,\n workflowId: opts?.workflowId,\n startedAt,\n status,\n }),\n );\n };\n\n // ADD-24 D-10/D-11: `lifecycle: retired` sets `invocable:false` (health never does, D-9) —\n // checked immediately after resolution, before any connector/response work, same call-site\n // discipline the `contract_violation` check already uses downstream.\n //\n // ADD-56 D-1/D-2: `lifecycleExposure` is now TOTAL — an unrecognized `lifecycle` value (only\n // reachable via a hand-written or forward-versioned `fromIR` artifact, ADD-0008 D-2) ALSO sets\n // `invocable:false`, distinguished from `retired` by `exposure.blockedReason`. The two are\n // different facts with different remediations (governance vs. compatibility — see\n // `exposure.ts`'s `Exposure.blockedReason` doc comment) and MUST NOT share a message or a\n // `denialReason`. `exposure.blockedReason === \"unevaluatable\"` is the only branch this can take\n // here: the `undefined` case (D-4's unknown-id fallback) cannot occur, because `tool` above was\n // already resolved via `getCapability`, which reads the identical `exposureById` map.\n const exposure = registry.getExposure(tool.id);\n if (!exposure.invocable) {\n if (exposure.blockedReason === \"unevaluatable\") {\n const text = `capability '${tool.id}' declares a lifecycle this build does not recognize and cannot evaluate — refusing (fail-closed).`;\n // #44: `denied`, never `failed` — refusing on a compatibility gap is not a backend\n // failure. Deliberately distinct denialReason/message/meta-key from the `retired` branch\n // below (ADD-56 D-2/D-3) — never `LIFECYCLE_BLOCKED_REASON`.\n audit({ phase: \"denied\", message: text, denialReason: LIFECYCLE_UNEVALUATABLE_REASON });\n return {\n content: [{ type: \"text\", text }],\n _meta: {\n [LIFECYCLE_UNEVALUATABLE_META_KEY]: { error: \"lifecycle_unevaluatable\", capability: tool.id, lifecycle: tool.lifecycle },\n },\n isError: true,\n };\n }\n const text = `capability '${tool.id}' is retired and can no longer be invoked.`;\n // #44: `denied`, never `failed` — a refusal by lifecycle is a refusal by governance, and\n // recording it as a failure would conflate it with \"the backend broke\" in the one log where\n // that distinction is the entire product. This gate is the SECOND (and only other) producer\n // of `phase: \"denied\"`, and the reason code the policy evaluator can never return. It runs\n // before policy deliberately, so a capability that is both retired and policy-denied records\n // `lifecycle_blocked`, matching the pinned gate order.\n audit({ phase: \"denied\", message: text, denialReason: LIFECYCLE_BLOCKED_REASON });\n return {\n content: [{ type: \"text\", text }],\n _meta: { [LIFECYCLE_BLOCKED_META_KEY]: { error: \"lifecycle_blocked\", capability: tool.id, lifecycle: tool.lifecycle } },\n isError: true,\n };\n }\n\n // #43 (ADD-43 D-5/D-6): THE policy evaluation point, called unconditionally — for every tool,\n // including one with no resolved policy, because `authenticated` enforcement now lives here\n // rather than inside `invokeRest` (D-4). Deliberately AFTER the ADD-24 exposure gate above, so\n // a `retired` capability reports `lifecycle_blocked` and never `policy_denied` (BR-34), and\n // strictly BEFORE `invokeRest`, so a denial does zero connector work: no env/caller\n // resolution, no URL building, no fetch, and no `onResponse` firing (BR-25).\n // #48: a `resolveCaller` that THREW for this request (rather than returning, even\n // `undefined`) short-circuits straight to a `policy_unevaluatable` denial for every\n // capability, bypassing `evaluatePolicy` entirely — identity extraction itself failed here,\n // which is strictly less trustworthy than \"no credential offered\" and must fail closed\n // regardless of whether THIS capability happens to declare `policies:[authenticated]`\n // (ADD-42 R-11). Reuses the evaluator's own reason code and this function's existing\n // policy-denial response shaping verbatim — no parallel response path.\n const decision: PolicyDecision = opts?.callerResolutionFailed\n ? {\n allowed: false,\n denial: {\n reason: \"policy_unevaluatable\",\n message: `capability '${tool.id}' could not be evaluated — caller identity could not be established (resolveCaller failed) — refusing (fail-closed).`,\n },\n }\n : evaluatePolicy(tool, {\n principal: opts?.caller?.principal,\n credentialPresent: opts?.caller?.accessToken !== undefined,\n });\n if (!decision.allowed) {\n // #44: the evaluator's OWN reason code, copied verbatim — no re-spelling, no mapping table,\n // no superset for the policy case. The message is likewise the evaluator's, unaltered:\n // the record copies, it never authors.\n audit({ phase: \"denied\", message: decision.denial.message, denialReason: decision.denial.reason });\n return {\n content: [{ type: \"text\", text: decision.denial.message }],\n _meta: {\n [POLICY_DENIED_META_KEY]: {\n error: \"policy_denied\",\n // `tool.id` — the unsanitized CDL id, never the MCP-sanitized advertised `name`\n // lookup key (BR-28, mirroring ADD-19 and ADD-30 BR-7).\n capability: tool.id,\n reason: decision.denial.reason,\n },\n },\n isError: true,\n };\n }\n\n // #45 (ADD-45 D-2/D-3): the rate-limit evaluation step, called at the SAME point as the policy\n // evaluator above — immediately after it allows, strictly before `invokeRest` — so a\n // rate-limited call does exactly as much connector work as a policy-denied one: none. Reuses\n // the identical `policy_denied` `_meta` shape and audit wiring; only the reason code differs.\n const rateDecision = await evaluateRateLimit(tool, { principal: opts?.caller?.principal }, opts?.rateLimitCounter);\n if (!rateDecision.allowed) {\n audit({ phase: \"denied\", message: rateDecision.denial.message, denialReason: rateDecision.denial.reason });\n return {\n content: [{ type: \"text\", text: rateDecision.denial.message }],\n _meta: {\n [POLICY_DENIED_META_KEY]: {\n error: \"policy_denied\",\n capability: tool.id,\n reason: rateDecision.denial.reason,\n },\n },\n isError: true,\n };\n }\n\n const result = await invokeRest(tool, args, opts);\n if (!result.ok) {\n // #44: every attempt that never completed a usable round-trip — unbound capability, missing\n // env var, missing caller credential, no baseUrl, an allowlist rejection, a missing path\n // parameter, a network error, a non-2xx response — records `failed` carrying the shipped\n // error text verbatim, so a deployer greps the audit log and finds the same string the\n // agent was shown.\n const text = result.error ?? \"invocation failed\";\n audit({ phase: \"failed\", message: text });\n return { content: [{ type: \"text\", text }], isError: true };\n }\n\n // #12 (ADD-12): a binding with a `response:` mapping is now MAPPED + VALIDATED against the\n // resource — the outputSchema (ADD-11) becomes an enforced contract, not just declared.\n if (tool.response) {\n const mapped = applyResponseMapping(tool, result.data, registry.ir.resources);\n if (mapped.status === \"violation\") {\n // Fail closed (D-6): the declared output shape was not met — no raw pass-through.\n const missing = mapped.missing ?? [];\n // The text is unchanged byte-for-byte; it moved into a shared helper (#44) only so the\n // embedded consumer, whose own result carries no text, records the identical sentence.\n const text = contractViolationMessage(tool.id, missing);\n // #19 (ADD-19 Rev 2 D-3′/D-6): structured error object lives in `_meta`, never\n // `structuredContent` — the reference SDK client validates `structuredContent` against\n // the tool's `outputSchema` unconditionally (not gated on `isError`), so a VIOLATION\n // object there (which never conforms to the success outputSchema) crashes the client\n // (verified live against the SDK's own InMemoryTransport, R2.0/R2.2). `capability` is\n // `tool.id`, the unsanitized CDL id — never the MCP-sanitized `name` lookup key (BR-7).\n // #44: a VIOLATION is `failed` — the declared output shape was not met.\n audit({ phase: \"failed\", message: text });\n return {\n content: [{ type: \"text\", text }],\n _meta: { [CONTRACT_VIOLATION_META_KEY]: { error: \"contract_violation\", capability: tool.id, missing } },\n isError: true,\n };\n }\n const content: CallResult[\"content\"] = [{ type: \"text\", text: JSON.stringify(mapped.data, null, 2) }];\n if (mapped.status === \"degraded\") {\n content.push({ type: \"text\", text: `note: optional field(s) absent (degraded): ${(mapped.degraded ?? []).join(\", \")}` });\n }\n // #44: `degraded` records `succeeded`, NOT `failed` — every *required* field was present and\n // an optional one was not, so the invocation succeeded. Pinned in a comment because\n // \"degraded\" reads like a failure and the next reader will guess otherwise.\n audit({ phase: \"succeeded\" });\n return { content, structuredContent: mapped.data, isError: false };\n }\n\n // No response mapping: today's raw pass-through (rollout-safe). The declared outputSchema is\n // NOT yet enforced for these tools — add a `response:` block to close the loop (ADD-12 R-3).\n const out: CallResult = { content: [{ type: \"text\", text: JSON.stringify(result.data ?? null, null, 2) }], isError: false };\n if (tool.output.length > 0) {\n const data = result.data;\n if (data && typeof data === \"object\" && !Array.isArray(data)) {\n out.structuredContent = data as Record<string, unknown>;\n }\n }\n // #44: `status.output` is deliberately NOT populated here (nor anywhere) — `result.data` is\n // exactly the payload the record must never carry.\n audit({ phase: \"succeeded\" });\n return out;\n}\n\n/** Build an MCP Server that lists and invokes the registry's tools. */\nexport function createMcpServer(registry: Registry, opts?: InvokeOptions): Server {\n const server = new Server({ name: \"archstone\", version: \"0\" }, { capabilities: { tools: {} } });\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions(registry) }));\n\n server.setRequestHandler(CallToolRequestSchema, async (req) => {\n const args = (req.params.arguments ?? {}) as Record<string, unknown>;\n const result = await callTool(registry, req.params.name, args, opts);\n return result as CallToolResult;\n });\n\n return server;\n}\n"],"mappings":"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","names":["text"]}
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { Registry } from '@archstone/emitter-support';
import { InvokeOptions } from '@archstone/provider-rest';
type JsonSchema = Record<string, unknown>;
interface McpToolDef {
name: string;
description: string;
inputSchema: JsonSchema;
outputSchema?: JsonSchema;
}
/** The MCP tool list: only invocable (bound) capabilities become tools — Registry's
* `invocableTools()` (ADD-30 D-3) is the single source of truth shared by this function
* (what's listed) and `callTool` (what's resolvable, via `getCapability`), keeping the two
* consistent. Input and output fields lower against the IR resource registry, so a
* `collection: Stay` output emits a typed, described `outputSchema` (not a bare
* `{type:object}`).
*
* ADD-24: a bound tool whose combined exposure (`registry.getExposure`, lifecycle + optional
* health) is `listed:false` (lifecycle `experimental`/`retired`) is dropped from the returned
* list entirely — unlisted, per D-10, though `experimental` remains callable by id (see
* `callTool`). A tool carrying a `hint` (beta/deprecated, or a yellow/red health reading) has
* its text appended to `description` — the only MCP-specific rendering of the neutral
* exposure the emitter-support layer computed. */
declare function toolDefinitions(registry: Registry): McpToolDef[];
interface CallResult {
content: {
type: "text";
text: string;
}[];
structuredContent?: Record<string, unknown>;
isError: boolean;
_meta?: Record<string, unknown>;
}
/** #19 ADD-19 Rev 2 D-6: the namespaced `_meta` key a VIOLATION result's structured error
* object is carried under. Never populated on `structuredContent` (D-3′) — the reference
* MCP SDK client validates `structuredContent` against `outputSchema` whenever the tool
* declares one, regardless of `isError`, so a non-conforming error object there crashes the
* client. `_meta` is untouched by that validation and passes through the client's zod parse
* unstripped (`ResultSchema`/`RequestMetaSchema` are `z.looseObject`). */
declare const CONTRACT_VIOLATION_META_KEY = "dev.archstone/contract_violation";
/** ADD-24 D-11: the namespaced `_meta` key a `retired` (or otherwise `invocable:false`)
* tool's rejection is carried under — reuses `CONTRACT_VIOLATION_META_KEY`'s precedent
* (ADD-19 Rev 2 D-3′/D-6) verbatim: never `structuredContent`, so the reference SDK client's
* unconditional `structuredContent`-against-`outputSchema` validation never sees it. A
* distinct key (not `CONTRACT_VIOLATION_META_KEY`) so a client distinguishes "this call was
* blocked before any connector work" from "the provider's response violated the contract". */
declare const LIFECYCLE_BLOCKED_META_KEY = "dev.archstone/lifecycle_blocked";
/** ADD-56 D-2/OQ-56-B: the namespaced `_meta` key an unrecognized-lifecycle rejection is
* carried under — a DISTINCT key from `LIFECYCLE_BLOCKED_META_KEY`, mirroring
* `POLICY_DENIED_META_KEY` vs. `LIFECYCLE_BLOCKED_META_KEY` being genuinely distinct keys
* rather than one key with a varying `error` string inside it. `retired` (a governance
* refusal) and an unrecognized `lifecycle` (a compatibility refusal) are different facts with
* different remediations and must be trivially distinguishable to a client — see
* `exposure.ts`'s `Exposure.blockedReason` doc comment. */
declare const LIFECYCLE_UNEVALUATABLE_META_KEY = "dev.archstone/lifecycle_unevaluatable";
/** #43 ADD-43: the namespaced `_meta` key a POLICY denial is carried under — the third use of
* the ADD-19 Rev 2 D-3′/D-6 precedent, verbatim: never `structuredContent`, because the
* reference SDK client validates that against the tool's `outputSchema` unconditionally (not
* gated on `isError`) and an error object there crashes it. A distinct key from the other two
* so a client can tell "refused by policy before any connector work" from "blocked by
* lifecycle" from "the provider's response violated the contract" — the three are mutually
* exclusive on one call (BR-27). The object discloses the reason code and the capability id
* and NOTHING about the policy itself: no metadata.id, no allow/deny entry, no other
* principal's identifier (BR-30, Rule #7 — the MCP client is the untrusted side). */
declare const POLICY_DENIED_META_KEY = "dev.archstone/policy_denied";
/** Route an MCP tool call to the REST provider and format the result as MCP content. */
declare function callTool(registry: Registry, name: string, args: Record<string, unknown>, opts?: InvokeOptions): Promise<CallResult>;
/** Build an MCP Server that lists and invokes the registry's tools. */
declare function createMcpServer(registry: Registry, opts?: InvokeOptions): Server;
export { CONTRACT_VIOLATION_META_KEY as C, LIFECYCLE_BLOCKED_META_KEY as L, type McpToolDef as M, POLICY_DENIED_META_KEY as P, type CallResult as a, LIFECYCLE_UNEVALUATABLE_META_KEY as b, createMcpServer as c, callTool as d, toolDefinitions as t };