@codai/axiom-mcp 2.0.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/README.md +104 -21
- package/dist/axm-lazy-LAnrfjUO.js +6284 -0
- package/dist/axm-lazy-Xin1nh7Q.js +6284 -0
- package/dist/axm-lazy.js +196 -7
- package/dist/cli-main.js +3661 -14919
- package/dist/dist-Bt9Wvd_P.js +6379 -0
- package/dist/dist-D4M2Lyji.js +6378 -0
- package/dist/gate-lazy.js +654 -43
- package/dist/gc-lazy.js +6885 -0
- package/dist/http-lazy.js +23527 -0
- package/dist/index.d.ts +141 -5
- package/dist/index.js +974 -50
- package/dist/lib-CqHwM4m_.js +3884 -0
- package/dist/mcp-lazy.js +23050 -0
- package/dist/migrate-lazy.js +6513 -0
- package/dist/verify-tree-lazy.js +6893 -0
- package/package.json +15 -8
- package/spec/codai-tools.json +348 -2
- package/spec/tools.json +1721 -72
package/dist/index.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { ApplyResultSchema, AxiomError, CheckReportSchema, DigestRefSchema, ErrorCodeSchema, JournalPhaseSchema, JournalSchema, ManifestBodySchema, ManifestBundleSchema, PlanSchema, ProfileSchema } 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
|
-
import { realpath } from "node:fs";
|
|
5
|
-
import { access, constants, mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { createReadStream, realpath } from "node:fs";
|
|
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 { loadProfile, runChecks } from "@codai/axiom-checks";
|
|
9
|
-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/
|
|
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";
|
|
11
|
+
import { webEmitter } from "@codai/axiom-emitters-web";
|
|
12
|
+
import { compilePlan, createEmitterRegistry, diffManifests, verifyBundle } from "@codai/axiom-plan";
|
|
10
13
|
import { appliedPath, apply, rollback } from "@codai/axiom-apply";
|
|
11
|
-
import {
|
|
14
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
15
|
+
import { canonicalDigestRef } from "@codai/axiom-canon";
|
|
12
16
|
//#region src/jsonschema.ts
|
|
13
17
|
const SCHEMA_KINDS = [
|
|
14
18
|
"Plan",
|
|
@@ -17,7 +21,8 @@ const SCHEMA_KINDS = [
|
|
|
17
21
|
"CheckReport",
|
|
18
22
|
"ApplyResult",
|
|
19
23
|
"Profile",
|
|
20
|
-
"Journal"
|
|
24
|
+
"Journal",
|
|
25
|
+
"RepoSnapshot"
|
|
21
26
|
];
|
|
22
27
|
const BY_KIND = {
|
|
23
28
|
Plan: PlanSchema,
|
|
@@ -26,7 +31,8 @@ const BY_KIND = {
|
|
|
26
31
|
CheckReport: CheckReportSchema,
|
|
27
32
|
ApplyResult: ApplyResultSchema,
|
|
28
33
|
Profile: ProfileSchema,
|
|
29
|
-
Journal: JournalSchema
|
|
34
|
+
Journal: JournalSchema,
|
|
35
|
+
RepoSnapshot: RepoSnapshotSchema
|
|
30
36
|
};
|
|
31
37
|
function isSchemaKind(v) {
|
|
32
38
|
return typeof v === "string" && SCHEMA_KINDS.includes(v);
|
|
@@ -162,6 +168,72 @@ async function resolveRoot(policy, requested) {
|
|
|
162
168
|
} });
|
|
163
169
|
}
|
|
164
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
|
|
218
|
+
//#region src/emitters.ts
|
|
219
|
+
/** Template emitters available to `axiom_plan_compile` / `axiom compile` (D-13: optional sugar). */
|
|
220
|
+
const EMITTERS = createEmitterRegistry([webEmitter]);
|
|
221
|
+
/** Flat, sorted `emitter@version: template — description` rows for the CLI and the resource. */
|
|
222
|
+
function emitterCatalogue(registry = EMITTERS) {
|
|
223
|
+
const rows = [];
|
|
224
|
+
for (const id of registry.list()) {
|
|
225
|
+
const e = registry.get(id);
|
|
226
|
+
if (e === void 0) continue;
|
|
227
|
+
for (const template of Object.keys(e.templates).sort()) rows.push({
|
|
228
|
+
emitter: e.id,
|
|
229
|
+
version: e.version,
|
|
230
|
+
template,
|
|
231
|
+
description: e.templates[template]?.description ?? ""
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return rows;
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
165
237
|
//#region src/store.ts
|
|
166
238
|
/** `<root>/.axiom/manifests/<hex>.json` and `<root>/.axiom/reports/<hex>.json`. */
|
|
167
239
|
function manifestsDir(root) {
|
|
@@ -179,13 +251,13 @@ function toDigestRef(shaOrRef) {
|
|
|
179
251
|
if (!parsed.success) throw new AxiomError("ERR_NOT_FOUND", `not a sha256 digest: ${shaOrRef}`);
|
|
180
252
|
return parsed.data;
|
|
181
253
|
}
|
|
182
|
-
async function writeJsonAtomic(file, value) {
|
|
254
|
+
async function writeJsonAtomic$1(file, value) {
|
|
183
255
|
await mkdir(path.dirname(file), { recursive: true });
|
|
184
256
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
185
257
|
await writeFile(tmp, JSON.stringify(value), "utf8");
|
|
186
258
|
await rename(tmp, file);
|
|
187
259
|
}
|
|
188
|
-
async function readJsonOrUndefined(file) {
|
|
260
|
+
async function readJsonOrUndefined$1(file) {
|
|
189
261
|
try {
|
|
190
262
|
return JSON.parse(await readFile(file, "utf8"));
|
|
191
263
|
} catch (err) {
|
|
@@ -195,30 +267,30 @@ async function readJsonOrUndefined(file) {
|
|
|
195
267
|
}
|
|
196
268
|
async function saveManifest(root, bundle) {
|
|
197
269
|
const file = path.join(manifestsDir(root), `${hexOf(bundle.manifestDigest)}.json`);
|
|
198
|
-
await writeJsonAtomic(file, bundle);
|
|
270
|
+
await writeJsonAtomic$1(file, bundle);
|
|
199
271
|
return file;
|
|
200
272
|
}
|
|
201
273
|
async function saveReport(root, report) {
|
|
202
274
|
const file = path.join(reportsDir(root), `${hexOf(report.manifestDigest)}.json`);
|
|
203
|
-
await writeJsonAtomic(file, report);
|
|
275
|
+
await writeJsonAtomic$1(file, report);
|
|
204
276
|
return file;
|
|
205
277
|
}
|
|
206
278
|
/** Search every root (allowlisted + seen) for a stored bundle. */
|
|
207
279
|
async function loadManifest(roots, ref) {
|
|
208
280
|
for (const root of roots) {
|
|
209
|
-
const raw = await readJsonOrUndefined(path.join(manifestsDir(root), `${hexOf(ref)}.json`));
|
|
281
|
+
const raw = await readJsonOrUndefined$1(path.join(manifestsDir(root), `${hexOf(ref)}.json`));
|
|
210
282
|
if (raw !== void 0) return ManifestBundleSchema.parse(raw);
|
|
211
283
|
}
|
|
212
284
|
}
|
|
213
285
|
async function loadReport(roots, ref) {
|
|
214
286
|
for (const root of roots) {
|
|
215
|
-
const raw = await readJsonOrUndefined(path.join(reportsDir(root), `${hexOf(ref)}.json`));
|
|
287
|
+
const raw = await readJsonOrUndefined$1(path.join(reportsDir(root), `${hexOf(ref)}.json`));
|
|
216
288
|
if (raw !== void 0) return CheckReportSchema.parse(raw);
|
|
217
289
|
}
|
|
218
290
|
}
|
|
219
291
|
async function loadApplied(roots, ref) {
|
|
220
292
|
for (const root of roots) {
|
|
221
|
-
const raw = await readJsonOrUndefined(appliedPath(root, ref));
|
|
293
|
+
const raw = await readJsonOrUndefined$1(appliedPath(root, ref));
|
|
222
294
|
if (raw !== void 0) return ApplyResultSchema.parse(raw);
|
|
223
295
|
}
|
|
224
296
|
}
|
|
@@ -242,6 +314,584 @@ async function listStored(roots, sub) {
|
|
|
242
314
|
return out;
|
|
243
315
|
}
|
|
244
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
|
|
562
|
+
//#region src/keys.ts
|
|
563
|
+
/**
|
|
564
|
+
* Key material and trust-store I/O for the CLI/MCP layer (D-16).
|
|
565
|
+
*
|
|
566
|
+
* Private keys are read ONLY from `AXIOM_SIGNING_KEY` (base64 PKCS#8 or raw seed) or
|
|
567
|
+
* `--key-file <path>`; they are never written to stdout and never stored under a root.
|
|
568
|
+
*/
|
|
569
|
+
function trustFilePath(root, rel = TRUST_FILE_DEFAULT) {
|
|
570
|
+
return path.join(root, ...rel.split("/"));
|
|
571
|
+
}
|
|
572
|
+
function trustStatePath(root) {
|
|
573
|
+
return path.join(root, ...TRUST_STATE_FILE.split("/"));
|
|
574
|
+
}
|
|
575
|
+
function trustStateKeyPath(root) {
|
|
576
|
+
return path.join(root, ...TRUST_STATE_KEY_FILE.split("/"));
|
|
577
|
+
}
|
|
578
|
+
async function writeJsonAtomic(file, value, mode) {
|
|
579
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
580
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
581
|
+
await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, {
|
|
582
|
+
encoding: "utf8",
|
|
583
|
+
mode
|
|
584
|
+
});
|
|
585
|
+
if (mode !== void 0) await chmod(tmp, mode).catch(() => void 0);
|
|
586
|
+
await rename(tmp, file);
|
|
587
|
+
}
|
|
588
|
+
async function readJsonOrUndefined(file) {
|
|
589
|
+
try {
|
|
590
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
591
|
+
} catch (err) {
|
|
592
|
+
if (err.code === "ENOENT") return void 0;
|
|
593
|
+
throw err;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/** Verify a bundle's detached signatures against the root's trust store; `undefined` when no store. */
|
|
597
|
+
async function verifyBundleAgainstRoot(root, bundle, rel = TRUST_FILE_DEFAULT) {
|
|
598
|
+
const store = await loadTrustStore(root, rel);
|
|
599
|
+
if (store === void 0) return void 0;
|
|
600
|
+
const v = verifyBundleSignatures(bundle, store, bundle.manifest.counter);
|
|
601
|
+
const report = {
|
|
602
|
+
trustFile: rel,
|
|
603
|
+
keyids: v.keyids,
|
|
604
|
+
findings: v.findings.map((f) => ({
|
|
605
|
+
id: f.id,
|
|
606
|
+
message: f.message
|
|
607
|
+
})),
|
|
608
|
+
ok: v.keyids.length > 0 && v.findings.length === 0
|
|
609
|
+
};
|
|
610
|
+
if (!report.ok) report.code = (bundle.signatures ?? []).length === 0 ? "ERR_SIGNATURE_MISSING" : "ERR_SIGNATURE_INVALID";
|
|
611
|
+
return report;
|
|
612
|
+
}
|
|
613
|
+
async function loadTrustStore(root, rel = TRUST_FILE_DEFAULT) {
|
|
614
|
+
const raw = await readJsonOrUndefined(trustFilePath(root, rel));
|
|
615
|
+
if (raw === void 0) return void 0;
|
|
616
|
+
const parsed = TrustStoreSchema.safeParse(raw);
|
|
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) } });
|
|
618
|
+
return parsed.data;
|
|
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
|
+
}
|
|
648
|
+
async function loadTrustState(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
|
+
}
|
|
656
|
+
if (raw === void 0) return void 0;
|
|
657
|
+
const parsed = TrustStateSchema.safeParse(raw);
|
|
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" } });
|
|
661
|
+
return parsed.data;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Advance `lastCounter` to `bundle.manifest.counter` after a successful apply.
|
|
665
|
+
* Monotonic: never moves backwards; a no-op when the bundle has no counter.
|
|
666
|
+
* Write-temp + rename so a crash leaves either the old or the new state.
|
|
667
|
+
*/
|
|
668
|
+
async function advanceTrustState(root, bundle) {
|
|
669
|
+
const counter = bundle.manifest.counter;
|
|
670
|
+
if (counter === void 0) return void 0;
|
|
671
|
+
const cur = await loadTrustState(root);
|
|
672
|
+
if (cur !== void 0 && cur.lastCounter >= counter) return cur;
|
|
673
|
+
const keyHex = await ensureStateKey(root);
|
|
674
|
+
const unsigned = {
|
|
675
|
+
version: 1,
|
|
676
|
+
lastCounter: counter,
|
|
677
|
+
manifestDigest: bundle.manifestDigest
|
|
678
|
+
};
|
|
679
|
+
const next = {
|
|
680
|
+
...unsigned,
|
|
681
|
+
mac: trustStateMac(unsigned, keyHex)
|
|
682
|
+
};
|
|
683
|
+
await writeJsonAtomic(trustStatePath(root), next);
|
|
684
|
+
return next;
|
|
685
|
+
}
|
|
686
|
+
/** Does the resolved profile (plus plan checks) enable antiRollback on requireSigned? */
|
|
687
|
+
function profileWantsAntiRollback(checks) {
|
|
688
|
+
return checks.some((c) => c.predicate === "manifest.requireSigned" && typeof c.params === "object" && c.params !== null && c.params.antiRollback === true);
|
|
689
|
+
}
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/snapshot.ts
|
|
692
|
+
/**
|
|
693
|
+
* `axiom_repo_snapshot` — deterministic, content-addressed inventory of a root (S-304).
|
|
694
|
+
*
|
|
695
|
+
* Successor of the v1 reverse-IR, which guessed "service types" from directory names and
|
|
696
|
+
* hashed nothing. A RepoSnapshot records what is actually there — relative path, size,
|
|
697
|
+
* sha256, mode, kind — sorted by `compareUtf8`, with no timestamps and no absolute paths, so
|
|
698
|
+
* the same tree yields the same `snapshotDigest` on every machine (invariant 1). Agents use
|
|
699
|
+
* it to build Plans against real pre-image digests and to diff two states of a tree.
|
|
700
|
+
*
|
|
701
|
+
* Read-only: never follows symlinks, never leaves the root, never spawns a process.
|
|
702
|
+
*/
|
|
703
|
+
const SNAPSHOT_MAX_FILES_DEFAULT = 2e4;
|
|
704
|
+
const SNAPSHOT_MAX_FILES_CAP = 5e4;
|
|
705
|
+
const SNAPSHOT_MAX_BYTES_DEFAULT = 67108864;
|
|
706
|
+
/** Never inventoried, whatever `.gitignore` says. */
|
|
707
|
+
const ALWAYS_SKIP = /* @__PURE__ */ new Set([".git", ".axiom"]);
|
|
708
|
+
function globToRegExp(glob) {
|
|
709
|
+
let re = "^";
|
|
710
|
+
for (let i = 0; i < glob.length; i++) {
|
|
711
|
+
const c = glob[i];
|
|
712
|
+
if (c === "*") {
|
|
713
|
+
if (glob[i + 1] === "*") {
|
|
714
|
+
i++;
|
|
715
|
+
if (glob[i + 1] === "/") {
|
|
716
|
+
i++;
|
|
717
|
+
re += "(?:.*/)?";
|
|
718
|
+
} else re += ".*";
|
|
719
|
+
} else re += "[^/]*";
|
|
720
|
+
} else if (c === "?") re += "[^/]";
|
|
721
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
722
|
+
}
|
|
723
|
+
return new RegExp(`${re}$`);
|
|
724
|
+
}
|
|
725
|
+
function matcherOf(globs, whenEmpty) {
|
|
726
|
+
if (globs === void 0 || globs.length === 0) return () => whenEmpty;
|
|
727
|
+
const res = globs.map(globToRegExp);
|
|
728
|
+
return (rel) => res.some((r) => r.test(rel));
|
|
729
|
+
}
|
|
730
|
+
/** A glob must itself be a contained relative path once wildcards are removed (`..` → rejected). */
|
|
731
|
+
function validateGlob(g, label) {
|
|
732
|
+
const probe = g.replace(/\*+/g, "x").replace(/\?/g, "x").replace(/\/+$/, "");
|
|
733
|
+
if (probe.length === 0 || !isValidRelPath(probe)) throw new AxiomError("ERR_CONTAINMENT", `${label} glob must be a contained relative path: ${g}`, { details: { glob: g } });
|
|
734
|
+
}
|
|
735
|
+
/** Root `.gitignore` → matchers, same rough translation the repo facts use (negations dropped). */
|
|
736
|
+
async function gitignoreMatcher(root) {
|
|
737
|
+
let text;
|
|
738
|
+
try {
|
|
739
|
+
text = await readFile(path.join(root, ".gitignore"), "utf8");
|
|
740
|
+
} catch {
|
|
741
|
+
return () => false;
|
|
742
|
+
}
|
|
743
|
+
const globs = [];
|
|
744
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
745
|
+
const l = raw.trim();
|
|
746
|
+
if (l.length === 0 || l.startsWith("#") || l.startsWith("!")) continue;
|
|
747
|
+
let pat = l.startsWith("/") ? l.slice(1) : l.includes("/") ? l : `**/${l}`;
|
|
748
|
+
if (pat.endsWith("/")) pat = pat.slice(0, -1);
|
|
749
|
+
globs.push(pat, `${pat}/**`);
|
|
750
|
+
}
|
|
751
|
+
return matcherOf(globs, false);
|
|
752
|
+
}
|
|
753
|
+
function sha256File(abs) {
|
|
754
|
+
return new Promise((resolve, reject) => {
|
|
755
|
+
const h = createHash("sha256");
|
|
756
|
+
createReadStream(abs).on("data", (chunk) => h.update(chunk)).on("error", reject).on("end", () => resolve(h.digest("hex")));
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
function modeOf(mode) {
|
|
760
|
+
return process.platform !== "win32" && (mode & 64) !== 0 ? "0755" : "0644";
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Inventory `rootReal` (already realpath'd and authorised by the caller).
|
|
764
|
+
* The walk visits entries in `compareUtf8` order of their relative path (directories keyed with
|
|
765
|
+
* a trailing `/`), which is exactly the final sort order — so a truncated `files` is the first
|
|
766
|
+
* N paths of the full sorted inventory, deterministically.
|
|
767
|
+
*/
|
|
768
|
+
async function snapshotRoot(root, opts = {}) {
|
|
769
|
+
if (opts.followSymlinks === true) throw new AxiomError("ERR_UNSUPPORTED_OP", "followSymlinks is not supported (symlinks are recorded, never followed)");
|
|
770
|
+
const rootReal = await realpath$1(root);
|
|
771
|
+
const maxFiles = Math.min(opts.maxFiles ?? 2e4, SNAPSHOT_MAX_FILES_CAP);
|
|
772
|
+
const maxBytes = opts.maxBytes ?? 67108864;
|
|
773
|
+
if (maxFiles < 1 || maxBytes < 0) throw new AxiomError("ERR_INVALID_PLAN", "maxFiles must be ≥ 1 and maxBytes ≥ 0", { details: {
|
|
774
|
+
maxFiles,
|
|
775
|
+
maxBytes
|
|
776
|
+
} });
|
|
777
|
+
for (const g of opts.include ?? []) validateGlob(g, "include");
|
|
778
|
+
for (const g of opts.exclude ?? []) validateGlob(g, "exclude");
|
|
779
|
+
const include = matcherOf(opts.include, true);
|
|
780
|
+
const exclude = matcherOf(opts.exclude, false);
|
|
781
|
+
const ignored = opts.respectGitignore === false ? () => false : await gitignoreMatcher(rootReal);
|
|
782
|
+
const withDigest = opts.withContentDigest !== false;
|
|
783
|
+
const files = [];
|
|
784
|
+
let bytes = 0;
|
|
785
|
+
let truncated = false;
|
|
786
|
+
const visit = async (dirAbs, dirRel) => {
|
|
787
|
+
let entries;
|
|
788
|
+
try {
|
|
789
|
+
const dir = await opendir(dirAbs);
|
|
790
|
+
entries = [];
|
|
791
|
+
for await (const e of dir) entries.push(e);
|
|
792
|
+
} catch {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const sortKey = (d) => d.isDirectory() ? `${d.name}/` : d.name;
|
|
796
|
+
entries.sort((a, b) => compareUtf8(sortKey(a), sortKey(b)));
|
|
797
|
+
for (const e of entries) {
|
|
798
|
+
if (truncated) return;
|
|
799
|
+
const rel = dirRel === "" ? e.name : `${dirRel}/${e.name}`;
|
|
800
|
+
if (!isValidRelPath(rel)) continue;
|
|
801
|
+
if (e.isDirectory()) {
|
|
802
|
+
if (ALWAYS_SKIP.has(e.name) || ignored(rel) || exclude(rel)) continue;
|
|
803
|
+
await visit(path.join(dirAbs, e.name), rel);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (ignored(rel) || !include(rel) || exclude(rel)) continue;
|
|
807
|
+
const abs = path.join(dirAbs, e.name);
|
|
808
|
+
let entry;
|
|
809
|
+
if (e.isSymbolicLink()) entry = await symlinkEntry(rootReal, abs, rel, withDigest);
|
|
810
|
+
else if (e.isFile()) entry = await fileEntry(abs, rel, withDigest);
|
|
811
|
+
if (entry === void 0) continue;
|
|
812
|
+
if (files.length >= maxFiles || bytes + entry.bytes > maxBytes) {
|
|
813
|
+
truncated = true;
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
files.push(entry);
|
|
817
|
+
bytes += entry.bytes;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
await visit(rootReal, "");
|
|
821
|
+
files.sort((a, b) => compareUtf8(a.path, b.path));
|
|
822
|
+
const body = {
|
|
823
|
+
files,
|
|
824
|
+
truncated,
|
|
825
|
+
counts: {
|
|
826
|
+
files: files.length,
|
|
827
|
+
bytes
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
return {
|
|
831
|
+
apiVersion: "axiom.dev/v2",
|
|
832
|
+
kind: "RepoSnapshot",
|
|
833
|
+
root: { kind: "relative" },
|
|
834
|
+
snapshotDigest: canonicalDigestRef(body),
|
|
835
|
+
body
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
async function fileEntry(abs, rel, withDigest) {
|
|
839
|
+
let st;
|
|
840
|
+
try {
|
|
841
|
+
st = await lstat(abs);
|
|
842
|
+
} catch {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (!st.isFile()) return void 0;
|
|
846
|
+
const entry = {
|
|
847
|
+
path: rel,
|
|
848
|
+
bytes: st.size,
|
|
849
|
+
mode: modeOf(st.mode),
|
|
850
|
+
kind: "file"
|
|
851
|
+
};
|
|
852
|
+
if (withDigest) try {
|
|
853
|
+
entry.sha256 = await sha256File(abs);
|
|
854
|
+
} catch {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
return entry;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* A symlink is recorded as `kind: "symlink"`. Its target is hashed only when it resolves to a
|
|
861
|
+
* regular file *inside* the root; anything else (outside, dangling, directory) gets
|
|
862
|
+
* `bytes: 0` and no digest — the link is inventoried, its target is not disclosed.
|
|
863
|
+
*/
|
|
864
|
+
async function symlinkEntry(rootReal, abs, rel, withDigest) {
|
|
865
|
+
const entry = {
|
|
866
|
+
path: rel,
|
|
867
|
+
bytes: 0,
|
|
868
|
+
mode: "0644",
|
|
869
|
+
kind: "symlink"
|
|
870
|
+
};
|
|
871
|
+
let target;
|
|
872
|
+
try {
|
|
873
|
+
target = await realpath$1(abs);
|
|
874
|
+
} catch {
|
|
875
|
+
return entry;
|
|
876
|
+
}
|
|
877
|
+
if (!isSameOrInside(rootReal, target)) return entry;
|
|
878
|
+
let st;
|
|
879
|
+
try {
|
|
880
|
+
st = await lstat(target);
|
|
881
|
+
} catch {
|
|
882
|
+
return entry;
|
|
883
|
+
}
|
|
884
|
+
if (!st.isFile()) return entry;
|
|
885
|
+
entry.bytes = st.size;
|
|
886
|
+
entry.mode = modeOf(st.mode);
|
|
887
|
+
if (withDigest) try {
|
|
888
|
+
entry.sha256 = await sha256File(target);
|
|
889
|
+
} catch {
|
|
890
|
+
entry.bytes = 0;
|
|
891
|
+
}
|
|
892
|
+
return entry;
|
|
893
|
+
}
|
|
894
|
+
//#endregion
|
|
245
895
|
//#region src/tools.ts
|
|
246
896
|
/** Hard cap on any single `bundle`/`plan` argument, measured as UTF-8 JSON bytes (§(f) payload size). */
|
|
247
897
|
const BUNDLE_BYTES_MAX = 4194304;
|
|
@@ -305,18 +955,20 @@ async function profileFor(ctx, bundle, name, rootReal) {
|
|
|
305
955
|
});
|
|
306
956
|
return loadProfile(profileName, { searchDirs });
|
|
307
957
|
}
|
|
308
|
-
async function checkBundle(ctx, bundle, profileName, rootReal) {
|
|
958
|
+
async function checkBundle(ctx, bundle, profileName, rootReal, signal) {
|
|
309
959
|
const opts = {
|
|
310
960
|
bundle,
|
|
311
961
|
profile: await profileFor(ctx, bundle, profileName, rootReal),
|
|
312
962
|
checks: bundle.manifest.checks,
|
|
313
963
|
...ctx.guards
|
|
314
964
|
};
|
|
965
|
+
if (signal !== void 0) opts.signal = signal;
|
|
315
966
|
if (rootReal !== void 0) {
|
|
316
967
|
opts.root = rootReal;
|
|
317
968
|
opts.casDir = path.join(rootReal, ".axiom", "cas");
|
|
318
969
|
}
|
|
319
970
|
const report = await runChecks(opts);
|
|
971
|
+
if (signal?.aborted) throw new AxiomError("ERR_TASK_CANCELLED", "check cancelled", { details: { manifestDigest: bundle.manifestDigest } });
|
|
320
972
|
if (rootReal !== void 0) {
|
|
321
973
|
await saveReport(rootReal, report);
|
|
322
974
|
ctx.seenRoots.add(rootReal);
|
|
@@ -353,7 +1005,20 @@ const ManifestVerifyOutput = z.object({
|
|
|
353
1005
|
code: ErrorCodeSchema,
|
|
354
1006
|
message: z.string(),
|
|
355
1007
|
path: z.string().optional()
|
|
356
|
-
}))
|
|
1008
|
+
})),
|
|
1009
|
+
/** Present only when a root with `.axiom/trust/keys.json` was available (D-16). */
|
|
1010
|
+
signatures: z.object({
|
|
1011
|
+
trustFile: z.string(),
|
|
1012
|
+
/** Trusted keyids whose signature verified over this manifest. */
|
|
1013
|
+
keyids: z.array(z.string()),
|
|
1014
|
+
findings: z.array(z.object({
|
|
1015
|
+
id: z.string(),
|
|
1016
|
+
message: z.string()
|
|
1017
|
+
})),
|
|
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()
|
|
1021
|
+
}).optional()
|
|
357
1022
|
});
|
|
358
1023
|
const RollbackOutput = z.object({
|
|
359
1024
|
manifestDigest: DigestRefSchema,
|
|
@@ -393,6 +1058,43 @@ const AxmParseOutput = z.object({
|
|
|
393
1058
|
}))
|
|
394
1059
|
});
|
|
395
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");
|
|
396
1098
|
const TOOL_DEFS = [
|
|
397
1099
|
defineTool({
|
|
398
1100
|
name: "axiom_plan_validate",
|
|
@@ -417,7 +1119,10 @@ const TOOL_DEFS = [
|
|
|
417
1119
|
})
|
|
418
1120
|
};
|
|
419
1121
|
try {
|
|
420
|
-
const { bundle } = await compilePlan(parsed.data, {
|
|
1122
|
+
const { bundle } = await compilePlan(parsed.data, {
|
|
1123
|
+
store: "inline",
|
|
1124
|
+
emitters: EMITTERS
|
|
1125
|
+
});
|
|
421
1126
|
return {
|
|
422
1127
|
ok: true,
|
|
423
1128
|
planDigest: bundle.manifest.planDigest,
|
|
@@ -440,7 +1145,7 @@ const TOOL_DEFS = [
|
|
|
440
1145
|
defineTool({
|
|
441
1146
|
name: "axiom_plan_compile",
|
|
442
1147
|
title: "Compile a Plan into a ManifestBundle",
|
|
443
|
-
description: "Compile a Plan into a content-addressed ManifestBundle (sorted artifacts, sha256 digests, in-toto planDigest). `store: cas` writes blobs under <root>/.axiom/cas instead of inlining them. When a root is given the bundle is stored under <root>/.axiom/manifests/<hex>.json so later tools can reference it by digest.",
|
|
1148
|
+
description: "Compile a Plan into a content-addressed ManifestBundle (sorted artifacts, sha256 digests, in-toto planDigest). `store: cas` writes blobs under <root>/.axiom/cas instead of inlining them. When a root is given the bundle is stored under <root>/.axiom/manifests/<hex>.json so later tools can reference it by digest. `template` sources are rendered by the built-in `web` emitter (see `axiom emitters`); its version is recorded in toolchain.emitters.",
|
|
444
1149
|
inputSchema: {
|
|
445
1150
|
plan: LooseObject.describe("Plan document"),
|
|
446
1151
|
store: z.enum(["inline", "cas"]).optional().describe("Blob transport; default inline"),
|
|
@@ -450,37 +1155,21 @@ const TOOL_DEFS = [
|
|
|
450
1155
|
annotations: ACT,
|
|
451
1156
|
async handler(ctx, { plan, store, root }) {
|
|
452
1157
|
guardPayloadSize("plan", plan);
|
|
453
|
-
|
|
454
|
-
const opts = { store: store ?? "inline" };
|
|
455
|
-
if (rootReal !== void 0) opts.root = rootReal;
|
|
456
|
-
const { bundle } = await compilePlan(plan, opts);
|
|
457
|
-
if (rootReal !== void 0) {
|
|
458
|
-
await saveManifest(rootReal, bundle);
|
|
459
|
-
ctx.seenRoots.add(rootReal);
|
|
460
|
-
}
|
|
461
|
-
ctx.log.info("compiled", {
|
|
462
|
-
manifestDigest: bundle.manifestDigest,
|
|
463
|
-
artifacts: bundle.manifest.artifacts.length
|
|
464
|
-
});
|
|
465
|
-
return bundle;
|
|
1158
|
+
return compileToBundle(ctx, plan, store, root);
|
|
466
1159
|
},
|
|
467
|
-
summarize:
|
|
468
|
-
manifestDigest: b.manifestDigest,
|
|
469
|
-
planDigest: b.manifest.planDigest,
|
|
470
|
-
name: b.manifest.name,
|
|
471
|
-
profile: b.manifest.profile,
|
|
472
|
-
artifacts: b.manifest.artifacts.length,
|
|
473
|
-
blobs: Object.keys(b.blobs).length
|
|
474
|
-
})
|
|
1160
|
+
summarize: summarizeBundle
|
|
475
1161
|
}),
|
|
476
1162
|
defineTool({
|
|
477
1163
|
name: "axiom_manifest_verify",
|
|
478
1164
|
title: "Verify a ManifestBundle",
|
|
479
|
-
description: "Structural and content-address verification: schema, recomputed manifestDigest, every inline blob hashes to its key, attestation subject matches. Never writes.",
|
|
480
|
-
inputSchema: {
|
|
1165
|
+
description: "Structural and content-address verification: schema, recomputed manifestDigest, every inline blob hashes to its key, attestation subject matches. When a root with .axiom/trust/keys.json is available, detached DSSE signatures are verified and the trusted keyids are reported under `signatures`. Never writes.",
|
|
1166
|
+
inputSchema: {
|
|
1167
|
+
bundle: LooseObject.describe("ManifestBundle"),
|
|
1168
|
+
root: RootArg
|
|
1169
|
+
},
|
|
481
1170
|
outputSchema: ManifestVerifyOutput,
|
|
482
1171
|
annotations: READ,
|
|
483
|
-
async handler(
|
|
1172
|
+
async handler(ctx, { bundle, root }) {
|
|
484
1173
|
guardPayloadSize("bundle", bundle);
|
|
485
1174
|
const r = verifyBundle(bundle);
|
|
486
1175
|
const out = {
|
|
@@ -491,12 +1180,25 @@ const TOOL_DEFS = [
|
|
|
491
1180
|
errors: r.errors
|
|
492
1181
|
};
|
|
493
1182
|
if (r.manifestDigest !== void 0) out.manifestDigest = r.manifestDigest;
|
|
1183
|
+
if (r.ok) {
|
|
1184
|
+
const rootReal = await optionalRoot(ctx, root);
|
|
1185
|
+
if (rootReal !== void 0) {
|
|
1186
|
+
const sig = await verifyBundleAgainstRoot(rootReal, parseBundle(bundle));
|
|
1187
|
+
if (sig !== void 0) {
|
|
1188
|
+
out.signatures = sig;
|
|
1189
|
+
out.signed = sig.keyids.length > 0;
|
|
1190
|
+
if (!sig.ok) out.ok = false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
494
1194
|
return out;
|
|
495
1195
|
},
|
|
496
1196
|
summarize: (o) => ({
|
|
497
1197
|
ok: o.ok,
|
|
498
1198
|
manifestDigest: o.manifestDigest,
|
|
499
1199
|
canonical: o.canonical,
|
|
1200
|
+
signed: o.signed,
|
|
1201
|
+
keyids: o.signatures?.keyids,
|
|
500
1202
|
missing: o.missing.length,
|
|
501
1203
|
errors: o.errors.slice(0, 20)
|
|
502
1204
|
})
|
|
@@ -517,6 +1219,135 @@ const TOOL_DEFS = [
|
|
|
517
1219
|
},
|
|
518
1220
|
summarize: summarizeReport
|
|
519
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
|
+
}),
|
|
520
1351
|
defineTool({
|
|
521
1352
|
name: "axiom_apply_dry_run",
|
|
522
1353
|
title: "Dry-run apply (stage + diff, no writes to the tree)",
|
|
@@ -562,6 +1393,7 @@ const TOOL_DEFS = [
|
|
|
562
1393
|
manifestDigest: parsed.manifestDigest
|
|
563
1394
|
} });
|
|
564
1395
|
const { rootReal } = await resolveRoot(ctx.policy, root);
|
|
1396
|
+
const profileDoc = await profileFor(ctx, parsed, profile, rootReal);
|
|
565
1397
|
const result = await apply({
|
|
566
1398
|
bundle: parsed,
|
|
567
1399
|
root: rootReal,
|
|
@@ -575,6 +1407,7 @@ const TOOL_DEFS = [
|
|
|
575
1407
|
await saveManifest(rootReal, parsed);
|
|
576
1408
|
ctx.seenRoots.add(rootReal);
|
|
577
1409
|
}
|
|
1410
|
+
if (result.status === "applied" && profileWantsAntiRollback([...profileDoc.checks, ...parsed.manifest.checks])) await advanceTrustState(rootReal, parsed);
|
|
578
1411
|
ctx.log.info("apply", {
|
|
579
1412
|
manifestDigest: parsed.manifestDigest,
|
|
580
1413
|
status: result.status,
|
|
@@ -677,6 +1510,46 @@ const TOOL_DEFS = [
|
|
|
677
1510
|
return { roots };
|
|
678
1511
|
},
|
|
679
1512
|
summarize: (o) => o
|
|
1513
|
+
}),
|
|
1514
|
+
defineTool({
|
|
1515
|
+
name: "axiom_repo_snapshot",
|
|
1516
|
+
title: "Snapshot a root",
|
|
1517
|
+
description: "Deterministic, content-addressed inventory of a root: every regular file (and symlink) as { path, bytes, sha256, mode, kind }, sorted by code point, with snapshotDigest = sha256(JCS(body)). No timestamps, no absolute paths — the same tree gives the same digest on every machine. Honours the root .gitignore, always skips .git/ and .axiom/, never follows symlinks, never leaves the root. Use it to build Plans against real pre-image digests, or diff two snapshots with `axiom snapshot-diff`.",
|
|
1518
|
+
inputSchema: {
|
|
1519
|
+
root: RootArg,
|
|
1520
|
+
include: z.array(z.string().min(1)).optional().describe("Relative globs (*, **, ?) to keep; default everything"),
|
|
1521
|
+
exclude: z.array(z.string().min(1)).optional().describe("Relative globs to drop"),
|
|
1522
|
+
maxFiles: z.int().min(1).max(SNAPSHOT_MAX_FILES_CAP).default(SNAPSHOT_MAX_FILES_DEFAULT).describe(`Stop after this many files (cap ${SNAPSHOT_MAX_FILES_CAP}); sets truncated`),
|
|
1523
|
+
maxBytes: z.int().nonnegative().default(SNAPSHOT_MAX_BYTES_DEFAULT).describe("Stop once the summed size would exceed this; sets truncated"),
|
|
1524
|
+
followSymlinks: z.literal(false).default(false).describe("Always false; symlinks are recorded, never followed"),
|
|
1525
|
+
respectGitignore: z.boolean().default(true),
|
|
1526
|
+
withContentDigest: z.boolean().default(true).describe("false → sizes only, no sha256")
|
|
1527
|
+
},
|
|
1528
|
+
outputSchema: RepoSnapshotSchema,
|
|
1529
|
+
annotations: READ,
|
|
1530
|
+
async handler(ctx, input) {
|
|
1531
|
+
const { rootReal } = await resolveRoot(ctx.policy, input.root);
|
|
1532
|
+
const opts = {
|
|
1533
|
+
maxFiles: input.maxFiles,
|
|
1534
|
+
maxBytes: input.maxBytes,
|
|
1535
|
+
respectGitignore: input.respectGitignore,
|
|
1536
|
+
withContentDigest: input.withContentDigest
|
|
1537
|
+
};
|
|
1538
|
+
if (input.include !== void 0) opts.include = input.include;
|
|
1539
|
+
if (input.exclude !== void 0) opts.exclude = input.exclude;
|
|
1540
|
+
const snap = await snapshotRoot(rootReal, opts);
|
|
1541
|
+
ctx.log.debug("snapshot", {
|
|
1542
|
+
root: rootReal,
|
|
1543
|
+
files: snap.body.counts.files
|
|
1544
|
+
});
|
|
1545
|
+
return snap;
|
|
1546
|
+
},
|
|
1547
|
+
summarize: (o) => ({
|
|
1548
|
+
snapshotDigest: o.snapshotDigest,
|
|
1549
|
+
counts: o.body.counts,
|
|
1550
|
+
truncated: o.body.truncated,
|
|
1551
|
+
paths: o.body.files.slice(0, 20).map((f) => f.path)
|
|
1552
|
+
})
|
|
680
1553
|
})
|
|
681
1554
|
];
|
|
682
1555
|
async function resolveBundleOrRef(ctx, v, label) {
|
|
@@ -688,6 +1561,47 @@ async function resolveBundleOrRef(ctx, v, label) {
|
|
|
688
1561
|
}
|
|
689
1562
|
return parseBundle(v);
|
|
690
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
|
+
}
|
|
691
1605
|
function summarizeReport(r) {
|
|
692
1606
|
return {
|
|
693
1607
|
manifestDigest: r.manifestDigest,
|
|
@@ -792,20 +1706,25 @@ function createServer(policy, opts = {}) {
|
|
|
792
1706
|
const ctx = {
|
|
793
1707
|
policy,
|
|
794
1708
|
log,
|
|
795
|
-
seenRoots: /* @__PURE__ */ new Set()
|
|
1709
|
+
seenRoots: opts.seenRoots ?? /* @__PURE__ */ new Set(),
|
|
1710
|
+
tasks: opts.tasks ?? new TaskStore(),
|
|
1711
|
+
planSessions: opts.planSessions ?? new PlanSessionStore()
|
|
796
1712
|
};
|
|
797
1713
|
if (opts.guards !== void 0) ctx.guards = opts.guards;
|
|
798
1714
|
const server = new McpServer({
|
|
799
1715
|
name: SERVER_NAME,
|
|
800
1716
|
version: SERVER_VERSION
|
|
801
|
-
}, {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1717
|
+
}, {
|
|
1718
|
+
capabilities: {
|
|
1719
|
+
tools: { listChanged: false },
|
|
1720
|
+
resources: { listChanged: false }
|
|
1721
|
+
},
|
|
1722
|
+
cacheHints: CACHE_HINTS
|
|
1723
|
+
});
|
|
805
1724
|
for (const def of opts.tools ?? TOOL_DEFS) server.registerTool(def.name, {
|
|
806
1725
|
title: def.title,
|
|
807
1726
|
description: def.description,
|
|
808
|
-
inputSchema: def.inputSchema,
|
|
1727
|
+
inputSchema: z.object(def.inputSchema),
|
|
809
1728
|
outputSchema: def.outputSchema,
|
|
810
1729
|
annotations: {
|
|
811
1730
|
title: def.title,
|
|
@@ -875,9 +1794,14 @@ function createServer(policy, opts = {}) {
|
|
|
875
1794
|
text: JSON.stringify(jsonSchemaFor(k), null, 2)
|
|
876
1795
|
}] };
|
|
877
1796
|
});
|
|
1797
|
+
server.registerResource("emitters", "axiom://emitters", {
|
|
1798
|
+
title: "Template emitters available to axiom_plan_compile",
|
|
1799
|
+
mimeType: "application/json"
|
|
1800
|
+
}, async (uri) => json(uri.href, emitterCatalogue()));
|
|
878
1801
|
log.info("server created", {
|
|
879
1802
|
name: SERVER_NAME,
|
|
880
1803
|
version: SERVER_VERSION,
|
|
1804
|
+
era: opts.era ?? "legacy",
|
|
881
1805
|
roots: [...policy.roots]
|
|
882
1806
|
});
|
|
883
1807
|
return server;
|
|
@@ -899,6 +1823,6 @@ function renderToolsSpec() {
|
|
|
899
1823
|
return `${JSON.stringify(buildToolsSpec(), null, 2)}\n`;
|
|
900
1824
|
}
|
|
901
1825
|
//#endregion
|
|
902
|
-
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 };
|
|
903
1827
|
|
|
904
1828
|
//# sourceMappingURL=index.js.map
|