@codai/axiom-mcp 2.0.0 → 2.1.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 +57 -6
- package/dist/axm-lazy-CR-FmFVd.js +6255 -0
- package/dist/axm-lazy.js +81 -2
- package/dist/cli-main.js +2315 -264
- package/dist/dist-FFqnyzCe.js +6141 -0
- package/dist/gate-lazy.js +77 -1
- package/dist/http-lazy.js +15154 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +411 -21
- package/dist/lib-CqHwM4m_.js +3884 -0
- package/dist/migrate-lazy.js +6472 -0
- package/package.json +9 -7
- package/spec/codai-tools.json +65 -2
- package/spec/tools.json +324 -2
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { GuardOptions } from "@codai/axiom-checks";
|
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
//#region src/jsonschema.d.ts
|
|
7
|
-
export declare const SCHEMA_KINDS: readonly ["Plan", "Manifest", "ManifestBundle", "CheckReport", "ApplyResult", "Profile", "Journal"];
|
|
7
|
+
export declare const SCHEMA_KINDS: readonly ["Plan", "Manifest", "ManifestBundle", "CheckReport", "ApplyResult", "Profile", "Journal", "RepoSnapshot"];
|
|
8
8
|
type SchemaKind = (typeof SCHEMA_KINDS)[number];
|
|
9
9
|
export declare function isSchemaKind(v: unknown): v is SchemaKind;
|
|
10
10
|
/** Draft 2020-12 JSON Schema, byte-identical to `packages/schema/schemas/<kind>.schema.json`. */
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
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, 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";
|
|
8
|
+
import { TRUST_FILE_DEFAULT, TRUST_STATE_FILE, loadProfile, runChecks, verifyBundleSignatures } from "@codai/axiom-checks";
|
|
9
9
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { webEmitter } from "@codai/axiom-emitters-web";
|
|
11
|
+
import { compilePlan, createEmitterRegistry, diffManifests, verifyBundle } from "@codai/axiom-plan";
|
|
10
12
|
import { appliedPath, apply, rollback } from "@codai/axiom-apply";
|
|
11
|
-
import {
|
|
13
|
+
import { canonicalDigestRef } from "@codai/axiom-canon";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
12
15
|
//#region src/jsonschema.ts
|
|
13
16
|
const SCHEMA_KINDS = [
|
|
14
17
|
"Plan",
|
|
@@ -17,7 +20,8 @@ const SCHEMA_KINDS = [
|
|
|
17
20
|
"CheckReport",
|
|
18
21
|
"ApplyResult",
|
|
19
22
|
"Profile",
|
|
20
|
-
"Journal"
|
|
23
|
+
"Journal",
|
|
24
|
+
"RepoSnapshot"
|
|
21
25
|
];
|
|
22
26
|
const BY_KIND = {
|
|
23
27
|
Plan: PlanSchema,
|
|
@@ -26,7 +30,8 @@ const BY_KIND = {
|
|
|
26
30
|
CheckReport: CheckReportSchema,
|
|
27
31
|
ApplyResult: ApplyResultSchema,
|
|
28
32
|
Profile: ProfileSchema,
|
|
29
|
-
Journal: JournalSchema
|
|
33
|
+
Journal: JournalSchema,
|
|
34
|
+
RepoSnapshot: RepoSnapshotSchema
|
|
30
35
|
};
|
|
31
36
|
function isSchemaKind(v) {
|
|
32
37
|
return typeof v === "string" && SCHEMA_KINDS.includes(v);
|
|
@@ -162,6 +167,25 @@ async function resolveRoot(policy, requested) {
|
|
|
162
167
|
} });
|
|
163
168
|
}
|
|
164
169
|
//#endregion
|
|
170
|
+
//#region src/emitters.ts
|
|
171
|
+
/** Template emitters available to `axiom_plan_compile` / `axiom compile` (D-13: optional sugar). */
|
|
172
|
+
const EMITTERS = createEmitterRegistry([webEmitter]);
|
|
173
|
+
/** Flat, sorted `emitter@version: template — description` rows for the CLI and the resource. */
|
|
174
|
+
function emitterCatalogue(registry = EMITTERS) {
|
|
175
|
+
const rows = [];
|
|
176
|
+
for (const id of registry.list()) {
|
|
177
|
+
const e = registry.get(id);
|
|
178
|
+
if (e === void 0) continue;
|
|
179
|
+
for (const template of Object.keys(e.templates).sort()) rows.push({
|
|
180
|
+
emitter: e.id,
|
|
181
|
+
version: e.version,
|
|
182
|
+
template,
|
|
183
|
+
description: e.templates[template]?.description ?? ""
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return rows;
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
165
189
|
//#region src/store.ts
|
|
166
190
|
/** `<root>/.axiom/manifests/<hex>.json` and `<root>/.axiom/reports/<hex>.json`. */
|
|
167
191
|
function manifestsDir(root) {
|
|
@@ -179,13 +203,13 @@ function toDigestRef(shaOrRef) {
|
|
|
179
203
|
if (!parsed.success) throw new AxiomError("ERR_NOT_FOUND", `not a sha256 digest: ${shaOrRef}`);
|
|
180
204
|
return parsed.data;
|
|
181
205
|
}
|
|
182
|
-
async function writeJsonAtomic(file, value) {
|
|
206
|
+
async function writeJsonAtomic$1(file, value) {
|
|
183
207
|
await mkdir(path.dirname(file), { recursive: true });
|
|
184
208
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
185
209
|
await writeFile(tmp, JSON.stringify(value), "utf8");
|
|
186
210
|
await rename(tmp, file);
|
|
187
211
|
}
|
|
188
|
-
async function readJsonOrUndefined(file) {
|
|
212
|
+
async function readJsonOrUndefined$1(file) {
|
|
189
213
|
try {
|
|
190
214
|
return JSON.parse(await readFile(file, "utf8"));
|
|
191
215
|
} catch (err) {
|
|
@@ -195,30 +219,30 @@ async function readJsonOrUndefined(file) {
|
|
|
195
219
|
}
|
|
196
220
|
async function saveManifest(root, bundle) {
|
|
197
221
|
const file = path.join(manifestsDir(root), `${hexOf(bundle.manifestDigest)}.json`);
|
|
198
|
-
await writeJsonAtomic(file, bundle);
|
|
222
|
+
await writeJsonAtomic$1(file, bundle);
|
|
199
223
|
return file;
|
|
200
224
|
}
|
|
201
225
|
async function saveReport(root, report) {
|
|
202
226
|
const file = path.join(reportsDir(root), `${hexOf(report.manifestDigest)}.json`);
|
|
203
|
-
await writeJsonAtomic(file, report);
|
|
227
|
+
await writeJsonAtomic$1(file, report);
|
|
204
228
|
return file;
|
|
205
229
|
}
|
|
206
230
|
/** Search every root (allowlisted + seen) for a stored bundle. */
|
|
207
231
|
async function loadManifest(roots, ref) {
|
|
208
232
|
for (const root of roots) {
|
|
209
|
-
const raw = await readJsonOrUndefined(path.join(manifestsDir(root), `${hexOf(ref)}.json`));
|
|
233
|
+
const raw = await readJsonOrUndefined$1(path.join(manifestsDir(root), `${hexOf(ref)}.json`));
|
|
210
234
|
if (raw !== void 0) return ManifestBundleSchema.parse(raw);
|
|
211
235
|
}
|
|
212
236
|
}
|
|
213
237
|
async function loadReport(roots, ref) {
|
|
214
238
|
for (const root of roots) {
|
|
215
|
-
const raw = await readJsonOrUndefined(path.join(reportsDir(root), `${hexOf(ref)}.json`));
|
|
239
|
+
const raw = await readJsonOrUndefined$1(path.join(reportsDir(root), `${hexOf(ref)}.json`));
|
|
216
240
|
if (raw !== void 0) return CheckReportSchema.parse(raw);
|
|
217
241
|
}
|
|
218
242
|
}
|
|
219
243
|
async function loadApplied(roots, ref) {
|
|
220
244
|
for (const root of roots) {
|
|
221
|
-
const raw = await readJsonOrUndefined(appliedPath(root, ref));
|
|
245
|
+
const raw = await readJsonOrUndefined$1(appliedPath(root, ref));
|
|
222
246
|
if (raw !== void 0) return ApplyResultSchema.parse(raw);
|
|
223
247
|
}
|
|
224
248
|
}
|
|
@@ -242,6 +266,293 @@ async function listStored(roots, sub) {
|
|
|
242
266
|
return out;
|
|
243
267
|
}
|
|
244
268
|
//#endregion
|
|
269
|
+
//#region src/keys.ts
|
|
270
|
+
/**
|
|
271
|
+
* Key material and trust-store I/O for the CLI/MCP layer (D-16).
|
|
272
|
+
*
|
|
273
|
+
* Private keys are read ONLY from `AXIOM_SIGNING_KEY` (base64 PKCS#8 or raw seed) or
|
|
274
|
+
* `--key-file <path>`; they are never written to stdout and never stored under a root.
|
|
275
|
+
*/
|
|
276
|
+
function trustFilePath(root, rel = TRUST_FILE_DEFAULT) {
|
|
277
|
+
return path.join(root, ...rel.split("/"));
|
|
278
|
+
}
|
|
279
|
+
function trustStatePath(root) {
|
|
280
|
+
return path.join(root, ...TRUST_STATE_FILE.split("/"));
|
|
281
|
+
}
|
|
282
|
+
async function writeJsonAtomic(file, value, mode) {
|
|
283
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
284
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
285
|
+
await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, {
|
|
286
|
+
encoding: "utf8",
|
|
287
|
+
mode
|
|
288
|
+
});
|
|
289
|
+
if (mode !== void 0) await chmod(tmp, mode).catch(() => void 0);
|
|
290
|
+
await rename(tmp, file);
|
|
291
|
+
}
|
|
292
|
+
async function readJsonOrUndefined(file) {
|
|
293
|
+
try {
|
|
294
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (err.code === "ENOENT") return void 0;
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Verify a bundle's detached signatures against the root's trust store; `undefined` when no store. */
|
|
301
|
+
async function verifyBundleAgainstRoot(root, bundle, rel = TRUST_FILE_DEFAULT) {
|
|
302
|
+
const store = await loadTrustStore(root, rel);
|
|
303
|
+
if (store === void 0) return void 0;
|
|
304
|
+
const v = verifyBundleSignatures(bundle, store, bundle.manifest.counter);
|
|
305
|
+
return {
|
|
306
|
+
trustFile: rel,
|
|
307
|
+
keyids: v.keyids,
|
|
308
|
+
findings: v.findings.map((f) => ({
|
|
309
|
+
id: f.id,
|
|
310
|
+
message: f.message
|
|
311
|
+
})),
|
|
312
|
+
ok: v.keyids.length > 0 && v.findings.length === 0
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
async function loadTrustStore(root, rel = TRUST_FILE_DEFAULT) {
|
|
316
|
+
const raw = await readJsonOrUndefined(trustFilePath(root, rel));
|
|
317
|
+
if (raw === void 0) return void 0;
|
|
318
|
+
const parsed = TrustStoreSchema.safeParse(raw);
|
|
319
|
+
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
|
+
return parsed.data;
|
|
321
|
+
}
|
|
322
|
+
async function loadTrustState(root) {
|
|
323
|
+
const raw = await readJsonOrUndefined(trustStatePath(root));
|
|
324
|
+
if (raw === void 0) return void 0;
|
|
325
|
+
const parsed = TrustStateSchema.safeParse(raw);
|
|
326
|
+
if (!parsed.success) throw new AxiomError("ERR_JOURNAL_CORRUPT", `${TRUST_STATE_FILE} is invalid`);
|
|
327
|
+
return parsed.data;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Advance `lastCounter` to `bundle.manifest.counter` after a successful apply.
|
|
331
|
+
* Monotonic: never moves backwards; a no-op when the bundle has no counter.
|
|
332
|
+
* Write-temp + rename so a crash leaves either the old or the new state.
|
|
333
|
+
*/
|
|
334
|
+
async function advanceTrustState(root, bundle) {
|
|
335
|
+
const counter = bundle.manifest.counter;
|
|
336
|
+
if (counter === void 0) return void 0;
|
|
337
|
+
const cur = await loadTrustState(root);
|
|
338
|
+
if (cur !== void 0 && cur.lastCounter >= counter) return cur;
|
|
339
|
+
const next = {
|
|
340
|
+
version: 1,
|
|
341
|
+
lastCounter: counter,
|
|
342
|
+
manifestDigest: bundle.manifestDigest
|
|
343
|
+
};
|
|
344
|
+
await writeJsonAtomic(trustStatePath(root), next);
|
|
345
|
+
return next;
|
|
346
|
+
}
|
|
347
|
+
/** Does the resolved profile (plus plan checks) enable antiRollback on requireSigned? */
|
|
348
|
+
function profileWantsAntiRollback(checks) {
|
|
349
|
+
return checks.some((c) => c.predicate === "manifest.requireSigned" && typeof c.params === "object" && c.params !== null && c.params.antiRollback === true);
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/snapshot.ts
|
|
353
|
+
/**
|
|
354
|
+
* `axiom_repo_snapshot` — deterministic, content-addressed inventory of a root (S-304).
|
|
355
|
+
*
|
|
356
|
+
* Successor of the v1 reverse-IR, which guessed "service types" from directory names and
|
|
357
|
+
* hashed nothing. A RepoSnapshot records what is actually there — relative path, size,
|
|
358
|
+
* sha256, mode, kind — sorted by `compareUtf8`, with no timestamps and no absolute paths, so
|
|
359
|
+
* the same tree yields the same `snapshotDigest` on every machine (invariant 1). Agents use
|
|
360
|
+
* it to build Plans against real pre-image digests and to diff two states of a tree.
|
|
361
|
+
*
|
|
362
|
+
* Read-only: never follows symlinks, never leaves the root, never spawns a process.
|
|
363
|
+
*/
|
|
364
|
+
const SNAPSHOT_MAX_FILES_DEFAULT = 2e4;
|
|
365
|
+
const SNAPSHOT_MAX_FILES_CAP = 5e4;
|
|
366
|
+
const SNAPSHOT_MAX_BYTES_DEFAULT = 67108864;
|
|
367
|
+
/** Never inventoried, whatever `.gitignore` says. */
|
|
368
|
+
const ALWAYS_SKIP = /* @__PURE__ */ new Set([".git", ".axiom"]);
|
|
369
|
+
function globToRegExp(glob) {
|
|
370
|
+
let re = "^";
|
|
371
|
+
for (let i = 0; i < glob.length; i++) {
|
|
372
|
+
const c = glob[i];
|
|
373
|
+
if (c === "*") {
|
|
374
|
+
if (glob[i + 1] === "*") {
|
|
375
|
+
i++;
|
|
376
|
+
if (glob[i + 1] === "/") {
|
|
377
|
+
i++;
|
|
378
|
+
re += "(?:.*/)?";
|
|
379
|
+
} else re += ".*";
|
|
380
|
+
} else re += "[^/]*";
|
|
381
|
+
} else if (c === "?") re += "[^/]";
|
|
382
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
383
|
+
}
|
|
384
|
+
return new RegExp(`${re}$`);
|
|
385
|
+
}
|
|
386
|
+
function matcherOf(globs, whenEmpty) {
|
|
387
|
+
if (globs === void 0 || globs.length === 0) return () => whenEmpty;
|
|
388
|
+
const res = globs.map(globToRegExp);
|
|
389
|
+
return (rel) => res.some((r) => r.test(rel));
|
|
390
|
+
}
|
|
391
|
+
/** A glob must itself be a contained relative path once wildcards are removed (`..` → rejected). */
|
|
392
|
+
function validateGlob(g, label) {
|
|
393
|
+
const probe = g.replace(/\*+/g, "x").replace(/\?/g, "x").replace(/\/+$/, "");
|
|
394
|
+
if (probe.length === 0 || !isValidRelPath(probe)) throw new AxiomError("ERR_CONTAINMENT", `${label} glob must be a contained relative path: ${g}`, { details: { glob: g } });
|
|
395
|
+
}
|
|
396
|
+
/** Root `.gitignore` → matchers, same rough translation the repo facts use (negations dropped). */
|
|
397
|
+
async function gitignoreMatcher(root) {
|
|
398
|
+
let text;
|
|
399
|
+
try {
|
|
400
|
+
text = await readFile(path.join(root, ".gitignore"), "utf8");
|
|
401
|
+
} catch {
|
|
402
|
+
return () => false;
|
|
403
|
+
}
|
|
404
|
+
const globs = [];
|
|
405
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
406
|
+
const l = raw.trim();
|
|
407
|
+
if (l.length === 0 || l.startsWith("#") || l.startsWith("!")) continue;
|
|
408
|
+
let pat = l.startsWith("/") ? l.slice(1) : l.includes("/") ? l : `**/${l}`;
|
|
409
|
+
if (pat.endsWith("/")) pat = pat.slice(0, -1);
|
|
410
|
+
globs.push(pat, `${pat}/**`);
|
|
411
|
+
}
|
|
412
|
+
return matcherOf(globs, false);
|
|
413
|
+
}
|
|
414
|
+
function sha256File(abs) {
|
|
415
|
+
return new Promise((resolve, reject) => {
|
|
416
|
+
const h = createHash("sha256");
|
|
417
|
+
createReadStream(abs).on("data", (chunk) => h.update(chunk)).on("error", reject).on("end", () => resolve(h.digest("hex")));
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
function modeOf(mode) {
|
|
421
|
+
return process.platform !== "win32" && (mode & 64) !== 0 ? "0755" : "0644";
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Inventory `rootReal` (already realpath'd and authorised by the caller).
|
|
425
|
+
* The walk visits entries in `compareUtf8` order of their relative path (directories keyed with
|
|
426
|
+
* a trailing `/`), which is exactly the final sort order — so a truncated `files` is the first
|
|
427
|
+
* N paths of the full sorted inventory, deterministically.
|
|
428
|
+
*/
|
|
429
|
+
async function snapshotRoot(root, opts = {}) {
|
|
430
|
+
if (opts.followSymlinks === true) throw new AxiomError("ERR_UNSUPPORTED_OP", "followSymlinks is not supported (symlinks are recorded, never followed)");
|
|
431
|
+
const rootReal = await realpath$1(root);
|
|
432
|
+
const maxFiles = Math.min(opts.maxFiles ?? 2e4, SNAPSHOT_MAX_FILES_CAP);
|
|
433
|
+
const maxBytes = opts.maxBytes ?? 67108864;
|
|
434
|
+
if (maxFiles < 1 || maxBytes < 0) throw new AxiomError("ERR_INVALID_PLAN", "maxFiles must be ≥ 1 and maxBytes ≥ 0", { details: {
|
|
435
|
+
maxFiles,
|
|
436
|
+
maxBytes
|
|
437
|
+
} });
|
|
438
|
+
for (const g of opts.include ?? []) validateGlob(g, "include");
|
|
439
|
+
for (const g of opts.exclude ?? []) validateGlob(g, "exclude");
|
|
440
|
+
const include = matcherOf(opts.include, true);
|
|
441
|
+
const exclude = matcherOf(opts.exclude, false);
|
|
442
|
+
const ignored = opts.respectGitignore === false ? () => false : await gitignoreMatcher(rootReal);
|
|
443
|
+
const withDigest = opts.withContentDigest !== false;
|
|
444
|
+
const files = [];
|
|
445
|
+
let bytes = 0;
|
|
446
|
+
let truncated = false;
|
|
447
|
+
const visit = async (dirAbs, dirRel) => {
|
|
448
|
+
let entries;
|
|
449
|
+
try {
|
|
450
|
+
const dir = await opendir(dirAbs);
|
|
451
|
+
entries = [];
|
|
452
|
+
for await (const e of dir) entries.push(e);
|
|
453
|
+
} catch {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const sortKey = (d) => d.isDirectory() ? `${d.name}/` : d.name;
|
|
457
|
+
entries.sort((a, b) => compareUtf8(sortKey(a), sortKey(b)));
|
|
458
|
+
for (const e of entries) {
|
|
459
|
+
if (truncated) return;
|
|
460
|
+
const rel = dirRel === "" ? e.name : `${dirRel}/${e.name}`;
|
|
461
|
+
if (!isValidRelPath(rel)) continue;
|
|
462
|
+
if (e.isDirectory()) {
|
|
463
|
+
if (ALWAYS_SKIP.has(e.name) || ignored(rel) || exclude(rel)) continue;
|
|
464
|
+
await visit(path.join(dirAbs, e.name), rel);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (ignored(rel) || !include(rel) || exclude(rel)) continue;
|
|
468
|
+
const abs = path.join(dirAbs, e.name);
|
|
469
|
+
let entry;
|
|
470
|
+
if (e.isSymbolicLink()) entry = await symlinkEntry(rootReal, abs, rel, withDigest);
|
|
471
|
+
else if (e.isFile()) entry = await fileEntry(abs, rel, withDigest);
|
|
472
|
+
if (entry === void 0) continue;
|
|
473
|
+
if (files.length >= maxFiles || bytes + entry.bytes > maxBytes) {
|
|
474
|
+
truncated = true;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
files.push(entry);
|
|
478
|
+
bytes += entry.bytes;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
await visit(rootReal, "");
|
|
482
|
+
files.sort((a, b) => compareUtf8(a.path, b.path));
|
|
483
|
+
const body = {
|
|
484
|
+
files,
|
|
485
|
+
truncated,
|
|
486
|
+
counts: {
|
|
487
|
+
files: files.length,
|
|
488
|
+
bytes
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
return {
|
|
492
|
+
apiVersion: "axiom.dev/v2",
|
|
493
|
+
kind: "RepoSnapshot",
|
|
494
|
+
root: { kind: "relative" },
|
|
495
|
+
snapshotDigest: canonicalDigestRef(body),
|
|
496
|
+
body
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
async function fileEntry(abs, rel, withDigest) {
|
|
500
|
+
let st;
|
|
501
|
+
try {
|
|
502
|
+
st = await lstat(abs);
|
|
503
|
+
} catch {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (!st.isFile()) return void 0;
|
|
507
|
+
const entry = {
|
|
508
|
+
path: rel,
|
|
509
|
+
bytes: st.size,
|
|
510
|
+
mode: modeOf(st.mode),
|
|
511
|
+
kind: "file"
|
|
512
|
+
};
|
|
513
|
+
if (withDigest) try {
|
|
514
|
+
entry.sha256 = await sha256File(abs);
|
|
515
|
+
} catch {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
return entry;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* A symlink is recorded as `kind: "symlink"`. Its target is hashed only when it resolves to a
|
|
522
|
+
* regular file *inside* the root; anything else (outside, dangling, directory) gets
|
|
523
|
+
* `bytes: 0` and no digest — the link is inventoried, its target is not disclosed.
|
|
524
|
+
*/
|
|
525
|
+
async function symlinkEntry(rootReal, abs, rel, withDigest) {
|
|
526
|
+
const entry = {
|
|
527
|
+
path: rel,
|
|
528
|
+
bytes: 0,
|
|
529
|
+
mode: "0644",
|
|
530
|
+
kind: "symlink"
|
|
531
|
+
};
|
|
532
|
+
let target;
|
|
533
|
+
try {
|
|
534
|
+
target = await realpath$1(abs);
|
|
535
|
+
} catch {
|
|
536
|
+
return entry;
|
|
537
|
+
}
|
|
538
|
+
if (!isSameOrInside(rootReal, target)) return entry;
|
|
539
|
+
let st;
|
|
540
|
+
try {
|
|
541
|
+
st = await lstat(target);
|
|
542
|
+
} catch {
|
|
543
|
+
return entry;
|
|
544
|
+
}
|
|
545
|
+
if (!st.isFile()) return entry;
|
|
546
|
+
entry.bytes = st.size;
|
|
547
|
+
entry.mode = modeOf(st.mode);
|
|
548
|
+
if (withDigest) try {
|
|
549
|
+
entry.sha256 = await sha256File(target);
|
|
550
|
+
} catch {
|
|
551
|
+
entry.bytes = 0;
|
|
552
|
+
}
|
|
553
|
+
return entry;
|
|
554
|
+
}
|
|
555
|
+
//#endregion
|
|
245
556
|
//#region src/tools.ts
|
|
246
557
|
/** Hard cap on any single `bundle`/`plan` argument, measured as UTF-8 JSON bytes (§(f) payload size). */
|
|
247
558
|
const BUNDLE_BYTES_MAX = 4194304;
|
|
@@ -353,7 +664,18 @@ const ManifestVerifyOutput = z.object({
|
|
|
353
664
|
code: ErrorCodeSchema,
|
|
354
665
|
message: z.string(),
|
|
355
666
|
path: z.string().optional()
|
|
356
|
-
}))
|
|
667
|
+
})),
|
|
668
|
+
/** Present only when a root with `.axiom/trust/keys.json` was available (D-16). */
|
|
669
|
+
signatures: z.object({
|
|
670
|
+
trustFile: z.string(),
|
|
671
|
+
/** Trusted keyids whose signature verified over this manifest. */
|
|
672
|
+
keyids: z.array(z.string()),
|
|
673
|
+
findings: z.array(z.object({
|
|
674
|
+
id: z.string(),
|
|
675
|
+
message: z.string()
|
|
676
|
+
})),
|
|
677
|
+
ok: z.boolean()
|
|
678
|
+
}).optional()
|
|
357
679
|
});
|
|
358
680
|
const RollbackOutput = z.object({
|
|
359
681
|
manifestDigest: DigestRefSchema,
|
|
@@ -417,7 +739,10 @@ const TOOL_DEFS = [
|
|
|
417
739
|
})
|
|
418
740
|
};
|
|
419
741
|
try {
|
|
420
|
-
const { bundle } = await compilePlan(parsed.data, {
|
|
742
|
+
const { bundle } = await compilePlan(parsed.data, {
|
|
743
|
+
store: "inline",
|
|
744
|
+
emitters: EMITTERS
|
|
745
|
+
});
|
|
421
746
|
return {
|
|
422
747
|
ok: true,
|
|
423
748
|
planDigest: bundle.manifest.planDigest,
|
|
@@ -440,7 +765,7 @@ const TOOL_DEFS = [
|
|
|
440
765
|
defineTool({
|
|
441
766
|
name: "axiom_plan_compile",
|
|
442
767
|
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.",
|
|
768
|
+
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
769
|
inputSchema: {
|
|
445
770
|
plan: LooseObject.describe("Plan document"),
|
|
446
771
|
store: z.enum(["inline", "cas"]).optional().describe("Blob transport; default inline"),
|
|
@@ -451,7 +776,10 @@ const TOOL_DEFS = [
|
|
|
451
776
|
async handler(ctx, { plan, store, root }) {
|
|
452
777
|
guardPayloadSize("plan", plan);
|
|
453
778
|
const rootReal = root !== void 0 || store === "cas" ? (await resolveRoot(ctx.policy, root)).rootReal : void 0;
|
|
454
|
-
const opts = {
|
|
779
|
+
const opts = {
|
|
780
|
+
store: store ?? "inline",
|
|
781
|
+
emitters: EMITTERS
|
|
782
|
+
};
|
|
455
783
|
if (rootReal !== void 0) opts.root = rootReal;
|
|
456
784
|
const { bundle } = await compilePlan(plan, opts);
|
|
457
785
|
if (rootReal !== void 0) {
|
|
@@ -476,11 +804,14 @@ const TOOL_DEFS = [
|
|
|
476
804
|
defineTool({
|
|
477
805
|
name: "axiom_manifest_verify",
|
|
478
806
|
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: {
|
|
807
|
+
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.",
|
|
808
|
+
inputSchema: {
|
|
809
|
+
bundle: LooseObject.describe("ManifestBundle"),
|
|
810
|
+
root: RootArg
|
|
811
|
+
},
|
|
481
812
|
outputSchema: ManifestVerifyOutput,
|
|
482
813
|
annotations: READ,
|
|
483
|
-
async handler(
|
|
814
|
+
async handler(ctx, { bundle, root }) {
|
|
484
815
|
guardPayloadSize("bundle", bundle);
|
|
485
816
|
const r = verifyBundle(bundle);
|
|
486
817
|
const out = {
|
|
@@ -491,12 +822,25 @@ const TOOL_DEFS = [
|
|
|
491
822
|
errors: r.errors
|
|
492
823
|
};
|
|
493
824
|
if (r.manifestDigest !== void 0) out.manifestDigest = r.manifestDigest;
|
|
825
|
+
if (r.ok) {
|
|
826
|
+
const rootReal = await optionalRoot(ctx, root);
|
|
827
|
+
if (rootReal !== void 0) {
|
|
828
|
+
const sig = await verifyBundleAgainstRoot(rootReal, parseBundle(bundle));
|
|
829
|
+
if (sig !== void 0) {
|
|
830
|
+
out.signatures = sig;
|
|
831
|
+
out.signed = sig.keyids.length > 0;
|
|
832
|
+
if (!sig.ok) out.ok = false;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
494
836
|
return out;
|
|
495
837
|
},
|
|
496
838
|
summarize: (o) => ({
|
|
497
839
|
ok: o.ok,
|
|
498
840
|
manifestDigest: o.manifestDigest,
|
|
499
841
|
canonical: o.canonical,
|
|
842
|
+
signed: o.signed,
|
|
843
|
+
keyids: o.signatures?.keyids,
|
|
500
844
|
missing: o.missing.length,
|
|
501
845
|
errors: o.errors.slice(0, 20)
|
|
502
846
|
})
|
|
@@ -562,6 +906,7 @@ const TOOL_DEFS = [
|
|
|
562
906
|
manifestDigest: parsed.manifestDigest
|
|
563
907
|
} });
|
|
564
908
|
const { rootReal } = await resolveRoot(ctx.policy, root);
|
|
909
|
+
const profileDoc = await profileFor(ctx, parsed, profile, rootReal);
|
|
565
910
|
const result = await apply({
|
|
566
911
|
bundle: parsed,
|
|
567
912
|
root: rootReal,
|
|
@@ -575,6 +920,7 @@ const TOOL_DEFS = [
|
|
|
575
920
|
await saveManifest(rootReal, parsed);
|
|
576
921
|
ctx.seenRoots.add(rootReal);
|
|
577
922
|
}
|
|
923
|
+
if (result.status === "applied" && profileWantsAntiRollback([...profileDoc.checks, ...parsed.manifest.checks])) await advanceTrustState(rootReal, parsed);
|
|
578
924
|
ctx.log.info("apply", {
|
|
579
925
|
manifestDigest: parsed.manifestDigest,
|
|
580
926
|
status: result.status,
|
|
@@ -677,6 +1023,46 @@ const TOOL_DEFS = [
|
|
|
677
1023
|
return { roots };
|
|
678
1024
|
},
|
|
679
1025
|
summarize: (o) => o
|
|
1026
|
+
}),
|
|
1027
|
+
defineTool({
|
|
1028
|
+
name: "axiom_repo_snapshot",
|
|
1029
|
+
title: "Snapshot a root",
|
|
1030
|
+
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`.",
|
|
1031
|
+
inputSchema: {
|
|
1032
|
+
root: RootArg,
|
|
1033
|
+
include: z.array(z.string().min(1)).optional().describe("Relative globs (*, **, ?) to keep; default everything"),
|
|
1034
|
+
exclude: z.array(z.string().min(1)).optional().describe("Relative globs to drop"),
|
|
1035
|
+
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`),
|
|
1036
|
+
maxBytes: z.int().nonnegative().default(SNAPSHOT_MAX_BYTES_DEFAULT).describe("Stop once the summed size would exceed this; sets truncated"),
|
|
1037
|
+
followSymlinks: z.literal(false).default(false).describe("Always false; symlinks are recorded, never followed"),
|
|
1038
|
+
respectGitignore: z.boolean().default(true),
|
|
1039
|
+
withContentDigest: z.boolean().default(true).describe("false → sizes only, no sha256")
|
|
1040
|
+
},
|
|
1041
|
+
outputSchema: RepoSnapshotSchema,
|
|
1042
|
+
annotations: READ,
|
|
1043
|
+
async handler(ctx, input) {
|
|
1044
|
+
const { rootReal } = await resolveRoot(ctx.policy, input.root);
|
|
1045
|
+
const opts = {
|
|
1046
|
+
maxFiles: input.maxFiles,
|
|
1047
|
+
maxBytes: input.maxBytes,
|
|
1048
|
+
respectGitignore: input.respectGitignore,
|
|
1049
|
+
withContentDigest: input.withContentDigest
|
|
1050
|
+
};
|
|
1051
|
+
if (input.include !== void 0) opts.include = input.include;
|
|
1052
|
+
if (input.exclude !== void 0) opts.exclude = input.exclude;
|
|
1053
|
+
const snap = await snapshotRoot(rootReal, opts);
|
|
1054
|
+
ctx.log.debug("snapshot", {
|
|
1055
|
+
root: rootReal,
|
|
1056
|
+
files: snap.body.counts.files
|
|
1057
|
+
});
|
|
1058
|
+
return snap;
|
|
1059
|
+
},
|
|
1060
|
+
summarize: (o) => ({
|
|
1061
|
+
snapshotDigest: o.snapshotDigest,
|
|
1062
|
+
counts: o.body.counts,
|
|
1063
|
+
truncated: o.body.truncated,
|
|
1064
|
+
paths: o.body.files.slice(0, 20).map((f) => f.path)
|
|
1065
|
+
})
|
|
680
1066
|
})
|
|
681
1067
|
];
|
|
682
1068
|
async function resolveBundleOrRef(ctx, v, label) {
|
|
@@ -875,6 +1261,10 @@ function createServer(policy, opts = {}) {
|
|
|
875
1261
|
text: JSON.stringify(jsonSchemaFor(k), null, 2)
|
|
876
1262
|
}] };
|
|
877
1263
|
});
|
|
1264
|
+
server.registerResource("emitters", "axiom://emitters", {
|
|
1265
|
+
title: "Template emitters available to axiom_plan_compile",
|
|
1266
|
+
mimeType: "application/json"
|
|
1267
|
+
}, async (uri) => json(uri.href, emitterCatalogue()));
|
|
878
1268
|
log.info("server created", {
|
|
879
1269
|
name: SERVER_NAME,
|
|
880
1270
|
version: SERVER_VERSION,
|