@codai/axiom-mcp 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,17 +1,18 @@
1
1
  import { createRequire } from "node:module";
2
- import { ApplyResultSchema, AxiomError, CheckReportSchema, DigestRefSchema, ErrorCodeSchema, JournalPhaseSchema, JournalSchema, ManifestBodySchema, ManifestBundleSchema, PlanSchema, ProfileSchema, RepoSnapshotSchema, TrustStateSchema, TrustStoreSchema, compareUtf8, isValidRelPath } from "@codai/axiom-schema";
2
+ import { ApplyResultSchema, AxiomError, CheckReportSchema, DigestRefSchema, ErrorCodeSchema, JournalPhaseSchema, JournalSchema, ManifestBodySchema, ManifestBundleSchema, PlanArtifactSchema, PlanSchema, ProfileSchema, RepoSnapshotSchema, TrustStateSchema, TrustStoreSchema, compareUtf8, isValidRelPath } from "@codai/axiom-schema";
3
3
  import { z } from "zod";
4
4
  import { createReadStream, realpath } from "node:fs";
5
5
  import { access, chmod, constants, lstat, mkdir, opendir, readFile, readdir, realpath as realpath$1, rename, stat, writeFile } from "node:fs/promises";
6
6
  import * as path from "node:path";
7
7
  import { promisify } from "node:util";
8
- import { TRUST_FILE_DEFAULT, TRUST_STATE_FILE, loadProfile, runChecks, verifyBundleSignatures } from "@codai/axiom-checks";
9
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { TRUST_FILE_DEFAULT, TRUST_STATE_FILE, TRUST_STATE_KEY_FILE, loadProfile, runChecks, trustStateMac, trustStateMacOk, verifyBundleSignatures } from "@codai/axiom-checks";
9
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
10
+ import "@modelcontextprotocol/server/stdio";
10
11
  import { webEmitter } from "@codai/axiom-emitters-web";
11
12
  import { compilePlan, createEmitterRegistry, diffManifests, verifyBundle } from "@codai/axiom-plan";
12
13
  import { appliedPath, apply, rollback } from "@codai/axiom-apply";
14
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
13
15
  import { canonicalDigestRef } from "@codai/axiom-canon";
14
- import { createHash } from "node:crypto";
15
16
  //#region src/jsonschema.ts
16
17
  const SCHEMA_KINDS = [
17
18
  "Plan",
@@ -167,6 +168,53 @@ async function resolveRoot(policy, requested) {
167
168
  } });
168
169
  }
169
170
  //#endregion
171
+ //#region src/adapter.ts
172
+ /**
173
+ * MCP SDK adapter (v2-architecture §5.4, D-19, S-405).
174
+ *
175
+ * The ONLY module under `packages/mcp/src` allowed to import `@modelcontextprotocol/*`
176
+ * (guard: `check-sdk-adapter`). Everything AXIOM needs from the SDK — server construction,
177
+ * the two serving entries, the result types — is re-exported here under names that do not
178
+ * change when the SDK does, so the next SDK major is a one-file diff.
179
+ *
180
+ * SDK 2.0.0 (2026-07-27) facts this seam encodes:
181
+ * - `@modelcontextprotocol/sdk` is split; the server surface is `@modelcontextprotocol/server`
182
+ * and stdio lives on its `./stdio` subpath (the root barrel is runtime-neutral).
183
+ * - A hand-connected `McpServer` + `StdioServerTransport` speaks ONLY the 2025 era. Serving the
184
+ * 2026-07-28 revision goes through the factory entries `serveStdio` / `createMcpHandler`,
185
+ * which pin one instance per connection (stdio) or build one per request (HTTP) and pass the
186
+ * factory the `era` they are about to serve.
187
+ * - `ttlMs` / `cacheScope` (SEP-2549) are stamped by the SDK on 2026-era cacheable results from
188
+ * `ServerOptions.cacheHints`; 2025-era responses never carry them.
189
+ */
190
+ /**
191
+ * Cache policy AXIOM advertises on 2026-era `tools/list` & co. The tool catalogue is static
192
+ * for the life of a process, so a shared 5-minute TTL is safe; `resources/read` is content
193
+ * addressed (`axiom://manifest/<sha>`) and therefore immutable → public, 1 day.
194
+ */
195
+ const CACHE_HINTS = {
196
+ "tools/list": {
197
+ ttlMs: 3e5,
198
+ cacheScope: "public"
199
+ },
200
+ "resources/templates/list": {
201
+ ttlMs: 3e5,
202
+ cacheScope: "public"
203
+ },
204
+ "server/discover": {
205
+ ttlMs: 3e5,
206
+ cacheScope: "public"
207
+ },
208
+ "resources/list": {
209
+ ttlMs: 1e4,
210
+ cacheScope: "private"
211
+ },
212
+ "resources/read": {
213
+ ttlMs: 864e5,
214
+ cacheScope: "public"
215
+ }
216
+ };
217
+ //#endregion
170
218
  //#region src/emitters.ts
171
219
  /** Template emitters available to `axiom_plan_compile` / `axiom compile` (D-13: optional sugar). */
172
220
  const EMITTERS = createEmitterRegistry([webEmitter]);
@@ -266,6 +314,251 @@ async function listStored(roots, sub) {
266
314
  return out;
267
315
  }
268
316
  //#endregion
317
+ //#region src/tasks.ts
318
+ /**
319
+ * S-406 / D-24 — tool-level tasks and chunked plan sessions.
320
+ *
321
+ * MCP SDK v2 ships the `io.modelcontextprotocol/tasks` wire vocabulary but no runtime (the
322
+ * `tasks/*` methods are excluded from `setRequestHandler`, and the 2026-07-28 `tools/call` codec
323
+ * rejects a `CreateTaskResult`). AXIOM therefore models long-running work as ordinary tools:
324
+ * `axiom_check_start` → `{ taskId, status: "working", pollIntervalMs, ttlMs }`, then
325
+ * `axiom_task_get` until `status` is terminal, `axiom_task_cancel` to abort. This works on every
326
+ * client that can call a tool (Copilot, codai agent-core, SDK v1 and v2) and on both wire eras.
327
+ *
328
+ * State is per factory (shared by every server instance a `serverFactory` builds, like
329
+ * `seenRoots`), never persisted: a restarted server has no tasks, and a poll for an unknown id is
330
+ * `ERR_TASK_NOT_FOUND`. Terminal tasks are kept for `ttlMs` after finishing, then dropped.
331
+ *
332
+ * The same store hosts chunked plan sessions (`axiom_plan_begin` → `axiom_plan_add`* →
333
+ * `axiom_plan_seal`): artifacts accumulate server-side so a Plan whose JSON would exceed the
334
+ * per-call 4 MiB payload cap can still be compiled. Sealing feeds the assembled Plan to the very
335
+ * same `compilePlan`, so the digest is identical to a one-shot compile (property test).
336
+ */
337
+ const TASK_STATUSES = [
338
+ "working",
339
+ "completed",
340
+ "failed",
341
+ "cancelled"
342
+ ];
343
+ /** How long a finished task stays pollable. */
344
+ const TASK_TTL_MS = 6e5;
345
+ /** Advisory poll interval returned to clients. */
346
+ const TASK_POLL_INTERVAL_MS = 2e3;
347
+ /** Concurrent `working` tasks per server process; the (n+1)th `axiom_check_start` is refused. */
348
+ const TASK_MAX_WORKING = 8;
349
+ /** Idle sessions (no `add`/`seal`) are dropped after this. */
350
+ const PLAN_SESSION_TTL_MS = 18e5;
351
+ /** Artifacts a session may hold — same bound as `PlanSchema.artifacts.max`. */
352
+ const PLAN_SESSION_MAX_ARTIFACTS = 2e3;
353
+ /** Summed UTF-8 JSON bytes of every chunk a session accepts (64 MiB, the `default` profile's `maxTotalBytes`). */
354
+ const PLAN_SESSION_MAX_BYTES = 67108864;
355
+ const TaskStatusSchema = z.enum(TASK_STATUSES);
356
+ const TaskErrorSchema = z.object({
357
+ code: z.string(),
358
+ message: z.string(),
359
+ details: z.record(z.string(), z.unknown()).optional()
360
+ });
361
+ var TaskStore = class {
362
+ tasks = /* @__PURE__ */ new Map();
363
+ now;
364
+ ttlMs;
365
+ pollIntervalMs;
366
+ maxWorking;
367
+ constructor(opts = {}) {
368
+ this.now = opts.now ?? Date.now;
369
+ this.ttlMs = opts.ttlMs ?? 6e5;
370
+ this.pollIntervalMs = opts.pollIntervalMs ?? 2e3;
371
+ this.maxWorking = opts.maxWorking ?? 8;
372
+ }
373
+ /**
374
+ * Start `work` in the background. The returned record is `working` until the promise settles;
375
+ * a rejection becomes `failed` (AxiomError → its closed code, anything else → `ERR_INTERNAL`),
376
+ * a rejection after `cancel()` stays `cancelled`.
377
+ */
378
+ start(tool, work, meta = {}) {
379
+ this.sweep();
380
+ const working = [...this.tasks.values()].filter((t) => t.status === "working").length;
381
+ if (working >= this.maxWorking) throw new AxiomError("ERR_EBUSY", `too many running tasks (${working}/${this.maxWorking})`, { details: {
382
+ working,
383
+ max: this.maxWorking
384
+ } });
385
+ const controller = new AbortController();
386
+ const rec = {
387
+ taskId: randomUUID(),
388
+ tool,
389
+ status: "working",
390
+ createdAt: this.now(),
391
+ controller
392
+ };
393
+ if (meta.root !== void 0) rec.root = meta.root;
394
+ this.tasks.set(rec.taskId, rec);
395
+ work(controller.signal).then((result) => {
396
+ if (rec.status !== "working") return;
397
+ rec.status = "completed";
398
+ rec.result = result;
399
+ rec.finishedAt = this.now();
400
+ }, (err) => {
401
+ if (rec.status !== "working") return;
402
+ rec.status = controller.signal.aborted ? "cancelled" : "failed";
403
+ rec.error = toTaskError(err);
404
+ rec.finishedAt = this.now();
405
+ });
406
+ return rec;
407
+ }
408
+ get(taskId) {
409
+ this.sweep();
410
+ const rec = this.tasks.get(taskId);
411
+ if (rec === void 0) throw new AxiomError("ERR_TASK_NOT_FOUND", `no task ${taskId}`, { details: { taskId } });
412
+ return rec;
413
+ }
414
+ /** Abort a `working` task; terminal tasks are left as they are (idempotent). */
415
+ cancel(taskId) {
416
+ const rec = this.get(taskId);
417
+ if (rec.status === "working") {
418
+ rec.status = "cancelled";
419
+ rec.finishedAt = this.now();
420
+ rec.error = {
421
+ code: "ERR_TASK_CANCELLED",
422
+ message: "cancelled via axiom_task_cancel"
423
+ };
424
+ rec.controller.abort();
425
+ }
426
+ return rec;
427
+ }
428
+ describe(rec) {
429
+ return {
430
+ taskId: rec.taskId,
431
+ tool: rec.tool,
432
+ status: rec.status,
433
+ pollIntervalMs: this.pollIntervalMs,
434
+ ttlMs: this.ttlMs,
435
+ elapsedMs: Math.max(0, (rec.finishedAt ?? this.now()) - rec.createdAt)
436
+ };
437
+ }
438
+ /** Drop terminal tasks older than `ttlMs`. */
439
+ sweep() {
440
+ const cutoff = this.now() - this.ttlMs;
441
+ for (const [id, t] of this.tasks) if (t.finishedAt !== void 0 && t.finishedAt < cutoff) this.tasks.delete(id);
442
+ }
443
+ /** Abort everything still running (server shutdown). */
444
+ abortAll() {
445
+ for (const t of this.tasks.values()) if (t.status === "working") this.cancel(t.taskId);
446
+ }
447
+ get size() {
448
+ return this.tasks.size;
449
+ }
450
+ };
451
+ function toTaskError(err) {
452
+ if (err instanceof AxiomError) {
453
+ const j = err.toJSON();
454
+ const out = {
455
+ code: j.code,
456
+ message: j.message
457
+ };
458
+ if (j.details !== void 0) out.details = j.details;
459
+ return out;
460
+ }
461
+ return {
462
+ code: "ERR_INTERNAL",
463
+ message: err instanceof Error ? err.message : String(err)
464
+ };
465
+ }
466
+ var PlanSessionStore = class {
467
+ sessions = /* @__PURE__ */ new Map();
468
+ now;
469
+ ttlMs;
470
+ maxOpen;
471
+ maxArtifacts;
472
+ maxBytes;
473
+ constructor(opts = {}) {
474
+ this.now = opts.now ?? Date.now;
475
+ this.ttlMs = opts.ttlMs ?? 18e5;
476
+ this.maxOpen = opts.maxOpen ?? 16;
477
+ this.maxArtifacts = opts.maxArtifacts ?? 2e3;
478
+ this.maxBytes = opts.maxBytes ?? 67108864;
479
+ }
480
+ begin(header) {
481
+ this.sweep();
482
+ if (this.sessions.size >= this.maxOpen) throw new AxiomError("ERR_EBUSY", `too many open plan sessions (${this.sessions.size}/${this.maxOpen})`, { details: {
483
+ open: this.sessions.size,
484
+ max: this.maxOpen
485
+ } });
486
+ const t = this.now();
487
+ const s = {
488
+ sessionId: randomUUID(),
489
+ header,
490
+ artifacts: [],
491
+ paths: /* @__PURE__ */ new Set(),
492
+ bytes: 0,
493
+ createdAt: t,
494
+ touchedAt: t,
495
+ sealed: false
496
+ };
497
+ this.sessions.set(s.sessionId, s);
498
+ return s;
499
+ }
500
+ get(sessionId) {
501
+ this.sweep();
502
+ const s = this.sessions.get(sessionId);
503
+ if (s === void 0) throw new AxiomError("ERR_TASK_NOT_FOUND", `no plan session ${sessionId}`, { details: { sessionId } });
504
+ return s;
505
+ }
506
+ /** Append already-validated artifacts; duplicates (within or across chunks) are `ERR_INVALID_PLAN`. */
507
+ add(sessionId, artifacts, chunkBytes) {
508
+ const s = this.get(sessionId);
509
+ if (s.sealed) throw new AxiomError("ERR_PLAN_SESSION_STATE", "plan session is already sealed", { details: { sessionId } });
510
+ if (s.artifacts.length + artifacts.length > this.maxArtifacts) throw new AxiomError("ERR_PLAN_SESSION_STATE", `session would hold ${s.artifacts.length + artifacts.length} artifacts; max ${this.maxArtifacts}`, { details: {
511
+ sessionId,
512
+ have: s.artifacts.length,
513
+ adding: artifacts.length
514
+ } });
515
+ if (s.bytes + chunkBytes > this.maxBytes) throw new AxiomError("ERR_PLAN_SESSION_STATE", `session would hold ${s.bytes + chunkBytes} bytes; max ${this.maxBytes}`, { details: {
516
+ sessionId,
517
+ have: s.bytes,
518
+ adding: chunkBytes,
519
+ max: this.maxBytes
520
+ } });
521
+ for (const a of artifacts) if (s.paths.has(a.path)) throw new AxiomError("ERR_INVALID_PLAN", "duplicate artifact path across chunks", {
522
+ path: a.path,
523
+ details: { sessionId }
524
+ });
525
+ for (const a of artifacts) {
526
+ s.paths.add(a.path);
527
+ s.artifacts.push(a);
528
+ }
529
+ s.bytes += chunkBytes;
530
+ s.touchedAt = this.now();
531
+ return s;
532
+ }
533
+ /** Mark sealed and return the assembled Plan input; the session is dropped. */
534
+ seal(sessionId) {
535
+ const s = this.get(sessionId);
536
+ if (s.sealed) throw new AxiomError("ERR_PLAN_SESSION_STATE", "plan session is already sealed", { details: { sessionId } });
537
+ s.sealed = true;
538
+ this.sessions.delete(sessionId);
539
+ return {
540
+ session: s,
541
+ plan: {
542
+ apiVersion: "axiom.dev/v2",
543
+ kind: "Plan",
544
+ ...s.header,
545
+ artifacts: s.artifacts
546
+ }
547
+ };
548
+ }
549
+ /** Drop an unsealed session without compiling. Unknown ids are a no-op. */
550
+ abandon(sessionId) {
551
+ return this.sessions.delete(sessionId);
552
+ }
553
+ sweep() {
554
+ const cutoff = this.now() - this.ttlMs;
555
+ for (const [id, s] of this.sessions) if (s.touchedAt < cutoff) this.sessions.delete(id);
556
+ }
557
+ get size() {
558
+ return this.sessions.size;
559
+ }
560
+ };
561
+ //#endregion
269
562
  //#region src/keys.ts
270
563
  /**
271
564
  * Key material and trust-store I/O for the CLI/MCP layer (D-16).
@@ -279,6 +572,9 @@ function trustFilePath(root, rel = TRUST_FILE_DEFAULT) {
279
572
  function trustStatePath(root) {
280
573
  return path.join(root, ...TRUST_STATE_FILE.split("/"));
281
574
  }
575
+ function trustStateKeyPath(root) {
576
+ return path.join(root, ...TRUST_STATE_KEY_FILE.split("/"));
577
+ }
282
578
  async function writeJsonAtomic(file, value, mode) {
283
579
  await mkdir(path.dirname(file), { recursive: true });
284
580
  const tmp = `${file}.tmp-${process.pid}`;
@@ -302,7 +598,7 @@ async function verifyBundleAgainstRoot(root, bundle, rel = TRUST_FILE_DEFAULT) {
302
598
  const store = await loadTrustStore(root, rel);
303
599
  if (store === void 0) return void 0;
304
600
  const v = verifyBundleSignatures(bundle, store, bundle.manifest.counter);
305
- return {
601
+ const report = {
306
602
  trustFile: rel,
307
603
  keyids: v.keyids,
308
604
  findings: v.findings.map((f) => ({
@@ -311,6 +607,8 @@ async function verifyBundleAgainstRoot(root, bundle, rel = TRUST_FILE_DEFAULT) {
311
607
  })),
312
608
  ok: v.keyids.length > 0 && v.findings.length === 0
313
609
  };
610
+ if (!report.ok) report.code = (bundle.signatures ?? []).length === 0 ? "ERR_SIGNATURE_MISSING" : "ERR_SIGNATURE_INVALID";
611
+ return report;
314
612
  }
315
613
  async function loadTrustStore(root, rel = TRUST_FILE_DEFAULT) {
316
614
  const raw = await readJsonOrUndefined(trustFilePath(root, rel));
@@ -319,11 +617,47 @@ async function loadTrustStore(root, rel = TRUST_FILE_DEFAULT) {
319
617
  if (!parsed.success) throw new AxiomError("ERR_INVALID_PROFILE", `trust store ${rel} is invalid`, { details: { issues: parsed.error.issues.slice(0, 10).map((i) => i.message) } });
320
618
  return parsed.data;
321
619
  }
620
+ async function loadStateKey(root) {
621
+ let text;
622
+ try {
623
+ text = await readFile(trustStateKeyPath(root), "utf8");
624
+ } catch (err) {
625
+ if (err.code === "ENOENT") return void 0;
626
+ throw err;
627
+ }
628
+ const hex = text.trim();
629
+ if (!/^[0-9a-f]{64}$/.test(hex)) throw new AxiomError("ERR_TRUST_STATE_CORRUPT", `${TRUST_STATE_KEY_FILE} is not 32 hex bytes`);
630
+ return hex;
631
+ }
632
+ /** Create `.axiom/trust/state.key` (32 random bytes, hex, 0600) when absent; return the key. */
633
+ async function ensureStateKey(root) {
634
+ const existing = await loadStateKey(root);
635
+ if (existing !== void 0) return existing;
636
+ const hex = randomBytes(32).toString("hex");
637
+ const file = trustStateKeyPath(root);
638
+ await mkdir(path.dirname(file), { recursive: true });
639
+ const tmp = `${file}.tmp-${process.pid}`;
640
+ await writeFile(tmp, `${hex}\n`, {
641
+ encoding: "utf8",
642
+ mode: 384
643
+ });
644
+ await chmod(tmp, 384).catch(() => void 0);
645
+ await rename(tmp, file);
646
+ return hex;
647
+ }
322
648
  async function loadTrustState(root) {
323
- const raw = await readJsonOrUndefined(trustStatePath(root));
649
+ let raw;
650
+ try {
651
+ raw = await readJsonOrUndefined(trustStatePath(root));
652
+ } catch (err) {
653
+ if (err instanceof SyntaxError) throw new AxiomError("ERR_TRUST_STATE_CORRUPT", `${TRUST_STATE_FILE} is not valid JSON`, { cause: err });
654
+ throw err;
655
+ }
324
656
  if (raw === void 0) return void 0;
325
657
  const parsed = TrustStateSchema.safeParse(raw);
326
- if (!parsed.success) throw new AxiomError("ERR_JOURNAL_CORRUPT", `${TRUST_STATE_FILE} is invalid`);
658
+ if (!parsed.success) throw new AxiomError("ERR_TRUST_STATE_CORRUPT", `${TRUST_STATE_FILE} is invalid`, { details: { issues: parsed.error.issues.slice(0, 5).map((i) => i.message) } });
659
+ const keyHex = await loadStateKey(root);
660
+ if (keyHex !== void 0 && !trustStateMacOk(parsed.data, keyHex)) throw new AxiomError("ERR_TRUST_STATE_CORRUPT", `${TRUST_STATE_FILE} failed its MAC (edited by hand, or ${TRUST_STATE_KEY_FILE} rotated)`, { details: { reason: parsed.data.mac === void 0 ? "NO_MAC" : "BAD_MAC" } });
327
661
  return parsed.data;
328
662
  }
329
663
  /**
@@ -336,11 +670,16 @@ async function advanceTrustState(root, bundle) {
336
670
  if (counter === void 0) return void 0;
337
671
  const cur = await loadTrustState(root);
338
672
  if (cur !== void 0 && cur.lastCounter >= counter) return cur;
339
- const next = {
673
+ const keyHex = await ensureStateKey(root);
674
+ const unsigned = {
340
675
  version: 1,
341
676
  lastCounter: counter,
342
677
  manifestDigest: bundle.manifestDigest
343
678
  };
679
+ const next = {
680
+ ...unsigned,
681
+ mac: trustStateMac(unsigned, keyHex)
682
+ };
344
683
  await writeJsonAtomic(trustStatePath(root), next);
345
684
  return next;
346
685
  }
@@ -616,18 +955,20 @@ async function profileFor(ctx, bundle, name, rootReal) {
616
955
  });
617
956
  return loadProfile(profileName, { searchDirs });
618
957
  }
619
- async function checkBundle(ctx, bundle, profileName, rootReal) {
958
+ async function checkBundle(ctx, bundle, profileName, rootReal, signal) {
620
959
  const opts = {
621
960
  bundle,
622
961
  profile: await profileFor(ctx, bundle, profileName, rootReal),
623
962
  checks: bundle.manifest.checks,
624
963
  ...ctx.guards
625
964
  };
965
+ if (signal !== void 0) opts.signal = signal;
626
966
  if (rootReal !== void 0) {
627
967
  opts.root = rootReal;
628
968
  opts.casDir = path.join(rootReal, ".axiom", "cas");
629
969
  }
630
970
  const report = await runChecks(opts);
971
+ if (signal?.aborted) throw new AxiomError("ERR_TASK_CANCELLED", "check cancelled", { details: { manifestDigest: bundle.manifestDigest } });
631
972
  if (rootReal !== void 0) {
632
973
  await saveReport(rootReal, report);
633
974
  ctx.seenRoots.add(rootReal);
@@ -674,7 +1015,9 @@ const ManifestVerifyOutput = z.object({
674
1015
  id: z.string(),
675
1016
  message: z.string()
676
1017
  })),
677
- ok: z.boolean()
1018
+ ok: z.boolean(),
1019
+ /** `ERR_SIGNATURE_MISSING` (no signatures at all) or `ERR_SIGNATURE_INVALID`; absent when `ok`. */
1020
+ code: z.enum(["ERR_SIGNATURE_MISSING", "ERR_SIGNATURE_INVALID"]).optional()
678
1021
  }).optional()
679
1022
  });
680
1023
  const RollbackOutput = z.object({
@@ -715,6 +1058,43 @@ const AxmParseOutput = z.object({
715
1058
  }))
716
1059
  });
717
1060
  const BundleOrRef = z.union([DigestRefSchema, LooseObject]).describe("A ManifestBundle object, or `sha256:<hex>` of a bundle stored under <root>/.axiom/manifests");
1061
+ const TaskDescriptorOutput = z.object({
1062
+ taskId: z.string(),
1063
+ tool: z.string(),
1064
+ status: TaskStatusSchema,
1065
+ pollIntervalMs: z.int().positive(),
1066
+ ttlMs: z.int().positive(),
1067
+ elapsedMs: z.int().nonnegative()
1068
+ });
1069
+ /** `axiom_task_get`: the descriptor plus, once terminal, the result or the error. */
1070
+ const TaskGetOutput = TaskDescriptorOutput.extend({
1071
+ /** Present iff `status === "completed"`; for `axiom_check` tasks a `CheckReport`. */
1072
+ result: CheckReportSchema.optional(),
1073
+ /** Present iff `status` is `failed` or `cancelled`. */
1074
+ error: TaskErrorSchema.optional()
1075
+ });
1076
+ const PlanHeaderShape = {
1077
+ name: PlanSchema.shape.name,
1078
+ intent: PlanSchema.shape.intent,
1079
+ profile: PlanSchema.shape.profile.optional(),
1080
+ capabilities: PlanSchema.shape.capabilities.optional(),
1081
+ checks: PlanSchema.shape.checks.optional(),
1082
+ counter: PlanSchema.shape.counter,
1083
+ metadata: PlanSchema.shape.metadata.optional()
1084
+ };
1085
+ const PlanSessionOutput = z.object({
1086
+ sessionId: z.string(),
1087
+ artifacts: z.int().nonnegative(),
1088
+ bytes: z.int().nonnegative(),
1089
+ /** Remaining budget before `axiom_plan_add` refuses with `ERR_PLAN_SESSION_STATE`. */
1090
+ limits: z.object({
1091
+ maxArtifacts: z.int().positive(),
1092
+ maxBytes: z.int().positive()
1093
+ }),
1094
+ ttlMs: z.int().positive()
1095
+ });
1096
+ const SessionIdArg = z.string().min(1).describe("sessionId returned by axiom_plan_begin");
1097
+ const TaskIdArg = z.string().min(1).describe("taskId returned by axiom_check_start");
718
1098
  const TOOL_DEFS = [
719
1099
  defineTool({
720
1100
  name: "axiom_plan_validate",
@@ -775,31 +1155,9 @@ const TOOL_DEFS = [
775
1155
  annotations: ACT,
776
1156
  async handler(ctx, { plan, store, root }) {
777
1157
  guardPayloadSize("plan", plan);
778
- const rootReal = root !== void 0 || store === "cas" ? (await resolveRoot(ctx.policy, root)).rootReal : void 0;
779
- const opts = {
780
- store: store ?? "inline",
781
- emitters: EMITTERS
782
- };
783
- if (rootReal !== void 0) opts.root = rootReal;
784
- const { bundle } = await compilePlan(plan, opts);
785
- if (rootReal !== void 0) {
786
- await saveManifest(rootReal, bundle);
787
- ctx.seenRoots.add(rootReal);
788
- }
789
- ctx.log.info("compiled", {
790
- manifestDigest: bundle.manifestDigest,
791
- artifacts: bundle.manifest.artifacts.length
792
- });
793
- return bundle;
1158
+ return compileToBundle(ctx, plan, store, root);
794
1159
  },
795
- summarize: (b) => ({
796
- manifestDigest: b.manifestDigest,
797
- planDigest: b.manifest.planDigest,
798
- name: b.manifest.name,
799
- profile: b.manifest.profile,
800
- artifacts: b.manifest.artifacts.length,
801
- blobs: Object.keys(b.blobs).length
802
- })
1160
+ summarize: summarizeBundle
803
1161
  }),
804
1162
  defineTool({
805
1163
  name: "axiom_manifest_verify",
@@ -861,6 +1219,135 @@ const TOOL_DEFS = [
861
1219
  },
862
1220
  summarize: summarizeReport
863
1221
  }),
1222
+ defineTool({
1223
+ name: "axiom_check_start",
1224
+ title: "Start policy checks as a background task",
1225
+ description: "Same evaluation as axiom_check, but returned immediately as a task so long `guard.external` suites (up to 15 min per guard) outlive the client's per-call timeout. Poll axiom_task_get with the returned taskId every pollIntervalMs until status is completed|failed|cancelled; the CheckReport is in `result`. Finished tasks are pollable for ttlMs, then forgotten. Tasks live in this server process only (D-24 — tool-level tasks, not the io.modelcontextprotocol/tasks wire extension).",
1226
+ inputSchema: {
1227
+ bundle: LooseObject.describe("ManifestBundle"),
1228
+ profile: ProfileArg,
1229
+ root: RootArg
1230
+ },
1231
+ outputSchema: TaskDescriptorOutput,
1232
+ annotations: READ,
1233
+ async handler(ctx, { bundle, profile, root }) {
1234
+ const parsed = parseBundle(bundle);
1235
+ const rootReal = await optionalRoot(ctx, root);
1236
+ const meta = {};
1237
+ if (rootReal !== void 0) meta.root = rootReal;
1238
+ const rec = ctx.tasks.start("axiom_check", (signal) => checkBundle(ctx, parsed, profile, rootReal, signal), meta);
1239
+ ctx.log.info("task started", {
1240
+ taskId: rec.taskId,
1241
+ manifestDigest: parsed.manifestDigest,
1242
+ root: rootReal
1243
+ });
1244
+ return ctx.tasks.describe(rec);
1245
+ },
1246
+ summarize: (o) => o
1247
+ }),
1248
+ defineTool({
1249
+ name: "axiom_task_get",
1250
+ title: "Poll a task",
1251
+ description: "Status of a task created by axiom_check_start. While `working` only the descriptor is returned; once terminal, `result` (completed) or `error` (failed|cancelled) is attached. Unknown or expired taskId → ERR_TASK_NOT_FOUND.",
1252
+ inputSchema: { taskId: TaskIdArg },
1253
+ outputSchema: TaskGetOutput,
1254
+ annotations: READ,
1255
+ async handler(ctx, { taskId }) {
1256
+ const rec = ctx.tasks.get(taskId);
1257
+ const out = ctx.tasks.describe(rec);
1258
+ if (rec.status === "completed") out.result = CheckReportSchema.parse(rec.result);
1259
+ else if (rec.error !== void 0) out.error = rec.error;
1260
+ return out;
1261
+ },
1262
+ summarize: (o) => ({
1263
+ taskId: o.taskId,
1264
+ status: o.status,
1265
+ elapsedMs: o.elapsedMs,
1266
+ pollIntervalMs: o.pollIntervalMs,
1267
+ result: o.result === void 0 ? void 0 : summarizeReport(o.result),
1268
+ error: o.error
1269
+ })
1270
+ }),
1271
+ defineTool({
1272
+ name: "axiom_task_cancel",
1273
+ title: "Cancel a task",
1274
+ description: "Abort a `working` task: every running guard process tree is killed and the task ends `cancelled` with error ERR_TASK_CANCELLED. Idempotent — a terminal task is returned unchanged.",
1275
+ inputSchema: { taskId: TaskIdArg },
1276
+ outputSchema: TaskDescriptorOutput,
1277
+ annotations: ACT,
1278
+ async handler(ctx, { taskId }) {
1279
+ const rec = ctx.tasks.cancel(taskId);
1280
+ ctx.log.info("task cancelled", {
1281
+ taskId,
1282
+ status: rec.status
1283
+ });
1284
+ return ctx.tasks.describe(rec);
1285
+ },
1286
+ summarize: (o) => o
1287
+ }),
1288
+ defineTool({
1289
+ name: "axiom_plan_begin",
1290
+ title: "Open a chunked plan session",
1291
+ description: "Start assembling a Plan whose JSON would exceed the 4 MiB per-call cap: send the header here (everything except `artifacts`), append artifacts in chunks with axiom_plan_add, then axiom_plan_seal compiles the whole. The sealed bundle's manifestDigest is identical to a one-shot axiom_plan_compile of the same Plan. Sessions are in-memory, expire after 30 min idle, and hold at most 2000 artifacts / 64 MiB.",
1292
+ inputSchema: PlanHeaderShape,
1293
+ outputSchema: PlanSessionOutput,
1294
+ annotations: ACT,
1295
+ async handler(ctx, header) {
1296
+ const s = ctx.planSessions.begin(header);
1297
+ ctx.log.debug("plan session opened", {
1298
+ sessionId: s.sessionId,
1299
+ name: header.name
1300
+ });
1301
+ return describeSession(ctx.planSessions, s);
1302
+ },
1303
+ summarize: (o) => o
1304
+ }),
1305
+ defineTool({
1306
+ name: "axiom_plan_add",
1307
+ title: "Append artifacts to a plan session",
1308
+ description: "Add a chunk of Plan artifacts (each ≤ 4 MiB call, inline content ≤ 256 KiB per artifact as in a Plan) to an open session. Paths must be unique across every chunk (ERR_INVALID_PLAN otherwise); a sealed or over-budget session is ERR_PLAN_SESSION_STATE.",
1309
+ inputSchema: {
1310
+ sessionId: SessionIdArg,
1311
+ artifacts: z.array(LooseObject).min(1).describe("Plan artifacts, same shape as Plan.artifacts[]")
1312
+ },
1313
+ outputSchema: PlanSessionOutput,
1314
+ annotations: ACT,
1315
+ async handler(ctx, { sessionId, artifacts }) {
1316
+ guardPayloadSize("artifacts", artifacts);
1317
+ const parsed = z.array(PlanArtifactSchema).safeParse(artifacts);
1318
+ if (!parsed.success) throw new AxiomError("ERR_INVALID_PLAN", "artifacts do not match PlanArtifactSchema", { details: { issues: parsed.error.issues.slice(0, 20).map((i) => ({
1319
+ path: i.path.map(String).join("."),
1320
+ message: i.message
1321
+ })) } });
1322
+ const bytes = Buffer.byteLength(JSON.stringify(artifacts), "utf8");
1323
+ const s = ctx.planSessions.add(sessionId, parsed.data, bytes);
1324
+ return describeSession(ctx.planSessions, s);
1325
+ },
1326
+ summarize: (o) => o
1327
+ }),
1328
+ defineTool({
1329
+ name: "axiom_plan_seal",
1330
+ title: "Seal a plan session and compile it",
1331
+ description: "Assemble the session's header + every added artifact into one Plan and compile it exactly like axiom_plan_compile (same options, same digest). The session is consumed whether or not compilation succeeds. Bundles whose inline blobs exceed 4 MiB need `store: cas` (and therefore a root).",
1332
+ inputSchema: {
1333
+ sessionId: SessionIdArg,
1334
+ store: z.enum(["inline", "cas"]).optional().describe("Blob transport; default inline"),
1335
+ root: RootArg
1336
+ },
1337
+ outputSchema: ManifestBundleSchema,
1338
+ annotations: ACT,
1339
+ async handler(ctx, { sessionId, store, root }) {
1340
+ const { plan, session } = ctx.planSessions.seal(sessionId);
1341
+ const bundle = await compileToBundle(ctx, plan, store, root);
1342
+ ctx.log.info("plan session sealed", {
1343
+ sessionId,
1344
+ artifacts: session.artifacts.length,
1345
+ manifestDigest: bundle.manifestDigest
1346
+ });
1347
+ return bundle;
1348
+ },
1349
+ summarize: summarizeBundle
1350
+ }),
864
1351
  defineTool({
865
1352
  name: "axiom_apply_dry_run",
866
1353
  title: "Dry-run apply (stage + diff, no writes to the tree)",
@@ -1074,6 +1561,47 @@ async function resolveBundleOrRef(ctx, v, label) {
1074
1561
  }
1075
1562
  return parseBundle(v);
1076
1563
  }
1564
+ /** Shared by `axiom_plan_compile` and `axiom_plan_seal` so both produce byte-identical bundles. */
1565
+ async function compileToBundle(ctx, plan, store, root) {
1566
+ const rootReal = root !== void 0 || store === "cas" ? (await resolveRoot(ctx.policy, root)).rootReal : void 0;
1567
+ const opts = {
1568
+ store: store ?? "inline",
1569
+ emitters: EMITTERS
1570
+ };
1571
+ if (rootReal !== void 0) opts.root = rootReal;
1572
+ const { bundle } = await compilePlan(plan, opts);
1573
+ if (rootReal !== void 0) {
1574
+ await saveManifest(rootReal, bundle);
1575
+ ctx.seenRoots.add(rootReal);
1576
+ }
1577
+ ctx.log.info("compiled", {
1578
+ manifestDigest: bundle.manifestDigest,
1579
+ artifacts: bundle.manifest.artifacts.length
1580
+ });
1581
+ return bundle;
1582
+ }
1583
+ function summarizeBundle(b) {
1584
+ return {
1585
+ manifestDigest: b.manifestDigest,
1586
+ planDigest: b.manifest.planDigest,
1587
+ name: b.manifest.name,
1588
+ profile: b.manifest.profile,
1589
+ artifacts: b.manifest.artifacts.length,
1590
+ blobs: Object.keys(b.blobs).length
1591
+ };
1592
+ }
1593
+ function describeSession(store, s) {
1594
+ return {
1595
+ sessionId: s.sessionId,
1596
+ artifacts: s.artifacts.length,
1597
+ bytes: s.bytes,
1598
+ limits: {
1599
+ maxArtifacts: store.maxArtifacts,
1600
+ maxBytes: store.maxBytes
1601
+ },
1602
+ ttlMs: store.ttlMs
1603
+ };
1604
+ }
1077
1605
  function summarizeReport(r) {
1078
1606
  return {
1079
1607
  manifestDigest: r.manifestDigest,
@@ -1178,20 +1706,25 @@ function createServer(policy, opts = {}) {
1178
1706
  const ctx = {
1179
1707
  policy,
1180
1708
  log,
1181
- seenRoots: /* @__PURE__ */ new Set()
1709
+ seenRoots: opts.seenRoots ?? /* @__PURE__ */ new Set(),
1710
+ tasks: opts.tasks ?? new TaskStore(),
1711
+ planSessions: opts.planSessions ?? new PlanSessionStore()
1182
1712
  };
1183
1713
  if (opts.guards !== void 0) ctx.guards = opts.guards;
1184
1714
  const server = new McpServer({
1185
1715
  name: SERVER_NAME,
1186
1716
  version: SERVER_VERSION
1187
- }, { capabilities: {
1188
- tools: {},
1189
- resources: {}
1190
- } });
1717
+ }, {
1718
+ capabilities: {
1719
+ tools: { listChanged: false },
1720
+ resources: { listChanged: false }
1721
+ },
1722
+ cacheHints: CACHE_HINTS
1723
+ });
1191
1724
  for (const def of opts.tools ?? TOOL_DEFS) server.registerTool(def.name, {
1192
1725
  title: def.title,
1193
1726
  description: def.description,
1194
- inputSchema: def.inputSchema,
1727
+ inputSchema: z.object(def.inputSchema),
1195
1728
  outputSchema: def.outputSchema,
1196
1729
  annotations: {
1197
1730
  title: def.title,
@@ -1268,6 +1801,7 @@ function createServer(policy, opts = {}) {
1268
1801
  log.info("server created", {
1269
1802
  name: SERVER_NAME,
1270
1803
  version: SERVER_VERSION,
1804
+ era: opts.era ?? "legacy",
1271
1805
  roots: [...policy.roots]
1272
1806
  });
1273
1807
  return server;
@@ -1289,6 +1823,6 @@ function renderToolsSpec() {
1289
1823
  return `${JSON.stringify(buildToolsSpec(), null, 2)}\n`;
1290
1824
  }
1291
1825
  //#endregion
1292
- export { BUNDLE_BYTES_MAX, LOG_LEVELS, SCHEMA_KINDS, SERVER_NAME, SERVER_VERSION, SUMMARY_LIST_MAX, TOOL_DEFS, buildToolsSpec, createLogger, createRootsPolicy, createServer, isLogLevel, isSameOrInside, isSchemaKind, jsonSchemaFor, renderToolsSpec, resolveRoot, riskClassOf, toStructuredError, toolByName };
1826
+ export { BUNDLE_BYTES_MAX, LOG_LEVELS, PLAN_SESSION_MAX_ARTIFACTS, PLAN_SESSION_MAX_BYTES, PLAN_SESSION_TTL_MS, PlanSessionStore, SCHEMA_KINDS, SERVER_NAME, SERVER_VERSION, SUMMARY_LIST_MAX, TASK_MAX_WORKING, TASK_POLL_INTERVAL_MS, TASK_STATUSES, TASK_TTL_MS, TOOL_DEFS, TaskStore, buildToolsSpec, createLogger, createRootsPolicy, createServer, isLogLevel, isSameOrInside, isSchemaKind, jsonSchemaFor, renderToolsSpec, resolveRoot, riskClassOf, toStructuredError, toolByName };
1293
1827
 
1294
1828
  //# sourceMappingURL=index.js.map