@codai/axiom-mcp 1.0.24 → 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/LICENSE +22 -0
- package/README.md +175 -423
- package/dist/axm-lazy-CR-FmFVd.js +6255 -0
- package/dist/axm-lazy-WdJHPy6W.js +2 -0
- package/dist/axm-lazy.js +12855 -0
- package/dist/cli-main.js +28458 -0
- package/dist/cli.js +19 -0
- package/dist/dist-FFqnyzCe.js +6141 -0
- package/dist/gate-lazy.js +9055 -0
- package/dist/http-lazy.js +15154 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +1294 -0
- package/dist/lib-CqHwM4m_.js +3884 -0
- package/dist/migrate-lazy.js +6472 -0
- package/package.json +74 -36
- package/spec/biome.json +6 -0
- package/spec/codai-tools.json +346 -0
- package/spec/tools.json +2288 -0
- package/dist/mcp-stdio.js +0 -368
- package/dist/postinstall.js +0 -31
- package/dist/server.js +0 -115
- package/dist/tools/fs-probe-write.js +0 -81
- package/scripts/prepublish.js +0 -52
package/dist/index.js
ADDED
|
@@ -0,0 +1,1294 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { ApplyResultSchema, AxiomError, CheckReportSchema, DigestRefSchema, ErrorCodeSchema, JournalPhaseSchema, JournalSchema, ManifestBodySchema, ManifestBundleSchema, PlanSchema, ProfileSchema, RepoSnapshotSchema, TrustStateSchema, TrustStoreSchema, compareUtf8, isValidRelPath } from "@codai/axiom-schema";
|
|
3
|
+
import { z } from "zod";
|
|
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
|
+
import * as path from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { TRUST_FILE_DEFAULT, TRUST_STATE_FILE, loadProfile, runChecks, verifyBundleSignatures } from "@codai/axiom-checks";
|
|
9
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { webEmitter } from "@codai/axiom-emitters-web";
|
|
11
|
+
import { compilePlan, createEmitterRegistry, diffManifests, verifyBundle } from "@codai/axiom-plan";
|
|
12
|
+
import { appliedPath, apply, rollback } from "@codai/axiom-apply";
|
|
13
|
+
import { canonicalDigestRef } from "@codai/axiom-canon";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
//#region src/jsonschema.ts
|
|
16
|
+
const SCHEMA_KINDS = [
|
|
17
|
+
"Plan",
|
|
18
|
+
"Manifest",
|
|
19
|
+
"ManifestBundle",
|
|
20
|
+
"CheckReport",
|
|
21
|
+
"ApplyResult",
|
|
22
|
+
"Profile",
|
|
23
|
+
"Journal",
|
|
24
|
+
"RepoSnapshot"
|
|
25
|
+
];
|
|
26
|
+
const BY_KIND = {
|
|
27
|
+
Plan: PlanSchema,
|
|
28
|
+
Manifest: ManifestBodySchema,
|
|
29
|
+
ManifestBundle: ManifestBundleSchema,
|
|
30
|
+
CheckReport: CheckReportSchema,
|
|
31
|
+
ApplyResult: ApplyResultSchema,
|
|
32
|
+
Profile: ProfileSchema,
|
|
33
|
+
Journal: JournalSchema,
|
|
34
|
+
RepoSnapshot: RepoSnapshotSchema
|
|
35
|
+
};
|
|
36
|
+
function isSchemaKind(v) {
|
|
37
|
+
return typeof v === "string" && SCHEMA_KINDS.includes(v);
|
|
38
|
+
}
|
|
39
|
+
/** Draft 2020-12 JSON Schema, byte-identical to `packages/schema/schemas/<kind>.schema.json`. */
|
|
40
|
+
function jsonSchemaFor(kind) {
|
|
41
|
+
const json = z.toJSONSchema(BY_KIND[kind], {
|
|
42
|
+
target: "draft-2020-12",
|
|
43
|
+
io: "input",
|
|
44
|
+
unrepresentable: "any"
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
$id: `https://axiom.dev/schemas/v2/${kind}.schema.json`,
|
|
48
|
+
title: kind,
|
|
49
|
+
...json
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Compact JSON Schema for tool input/output (spec/tools.json). */
|
|
53
|
+
function toolJsonSchema(schema) {
|
|
54
|
+
return z.toJSONSchema(schema, {
|
|
55
|
+
target: "draft-2020-12",
|
|
56
|
+
io: "input",
|
|
57
|
+
unrepresentable: "any"
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/log.ts
|
|
62
|
+
/**
|
|
63
|
+
* stderr-only JSON-lines logger. stdout belongs to the MCP transport; nothing in
|
|
64
|
+
* this package may write there except the transport itself (and CLI verbs).
|
|
65
|
+
*/
|
|
66
|
+
const LOG_LEVELS = [
|
|
67
|
+
"error",
|
|
68
|
+
"warn",
|
|
69
|
+
"info",
|
|
70
|
+
"debug"
|
|
71
|
+
];
|
|
72
|
+
function isLogLevel(v) {
|
|
73
|
+
return typeof v === "string" && LOG_LEVELS.includes(v);
|
|
74
|
+
}
|
|
75
|
+
const RANK = {
|
|
76
|
+
error: 0,
|
|
77
|
+
warn: 1,
|
|
78
|
+
info: 2,
|
|
79
|
+
debug: 3
|
|
80
|
+
};
|
|
81
|
+
function createLogger(opts = {}) {
|
|
82
|
+
const level = opts.level ?? "warn";
|
|
83
|
+
const write = opts.write ?? ((line) => void process.stderr.write(line));
|
|
84
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
85
|
+
const emit = (lvl, msg, fields) => {
|
|
86
|
+
if (RANK[lvl] > RANK[level]) return;
|
|
87
|
+
write(`${JSON.stringify({
|
|
88
|
+
level: lvl,
|
|
89
|
+
ts: now(),
|
|
90
|
+
msg,
|
|
91
|
+
...fields
|
|
92
|
+
})}\n`);
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
level,
|
|
96
|
+
error: (m, f) => emit("error", m, f),
|
|
97
|
+
warn: (m, f) => emit("warn", m, f),
|
|
98
|
+
info: (m, f) => emit("info", m, f),
|
|
99
|
+
debug: (m, f) => emit("debug", m, f)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const silentLogger = createLogger({ write: () => {} });
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/roots.ts
|
|
105
|
+
const realpathNative = promisify(realpath.native);
|
|
106
|
+
const IS_WIN32 = process.platform === "win32";
|
|
107
|
+
async function realDir(p, code) {
|
|
108
|
+
let real;
|
|
109
|
+
try {
|
|
110
|
+
real = await realpathNative(p);
|
|
111
|
+
} catch (cause) {
|
|
112
|
+
throw new AxiomError(code, `root does not exist: ${p}`, {
|
|
113
|
+
cause,
|
|
114
|
+
details: { root: p }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (IS_WIN32 && real.startsWith("\\\\?\\")) real = real.slice(4);
|
|
118
|
+
if (!(await stat(real)).isDirectory()) throw new AxiomError(code, `root is not a directory: ${p}`, { details: { root: p } });
|
|
119
|
+
return real;
|
|
120
|
+
}
|
|
121
|
+
function norm(p) {
|
|
122
|
+
const n = path.normalize(p).replace(/[\\/]+$/, "");
|
|
123
|
+
return IS_WIN32 ? n.toLowerCase() : n;
|
|
124
|
+
}
|
|
125
|
+
/** True iff `child` equals `parent` or lies inside it (case-insensitive on win32). */
|
|
126
|
+
function isSameOrInside(parent, child) {
|
|
127
|
+
const p = norm(parent);
|
|
128
|
+
const c = norm(child);
|
|
129
|
+
if (p === c) return true;
|
|
130
|
+
return c.startsWith(p.endsWith(path.sep) ? p : p + path.sep);
|
|
131
|
+
}
|
|
132
|
+
/** Build the policy from `--root <abs>` arguments. Each must exist and be a directory. */
|
|
133
|
+
async function createRootsPolicy(rootArgs) {
|
|
134
|
+
const roots = /* @__PURE__ */ new Set();
|
|
135
|
+
for (const r of rootArgs) {
|
|
136
|
+
if (!path.isAbsolute(r)) throw new AxiomError("ERR_ROOT_NOT_DIR", `--root must be absolute: ${r}`, { details: { root: r } });
|
|
137
|
+
roots.add(await realDir(r, "ERR_ROOT_NOT_DIR"));
|
|
138
|
+
}
|
|
139
|
+
return Object.freeze({ roots: Object.freeze(roots) });
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Resolve a tool's `root` argument against the allowlist. No env fallback, no cwd:
|
|
143
|
+
* none requested + one root → that root; none + many → ERR_ROOT_REQUIRED;
|
|
144
|
+
* requested → realpath, must equal or be inside an allowlisted root → else ERR_ROOT_NOT_ALLOWED.
|
|
145
|
+
*/
|
|
146
|
+
async function resolveRoot(policy, requested) {
|
|
147
|
+
if (requested === void 0 || requested === "") {
|
|
148
|
+
if (policy.roots.size === 1) {
|
|
149
|
+
const only = [...policy.roots][0];
|
|
150
|
+
return {
|
|
151
|
+
rootReal: only,
|
|
152
|
+
effectiveRoot: only
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
throw new AxiomError("ERR_ROOT_REQUIRED", policy.roots.size === 0 ? "server has no allowlisted roots (start with --root <dir>)" : "root is required when more than one root is allowlisted", { details: { roots: [...policy.roots] } });
|
|
156
|
+
}
|
|
157
|
+
if (!path.isAbsolute(requested)) throw new AxiomError("ERR_ROOT_NOT_ALLOWED", `root must be absolute: ${requested}`, { details: { root: requested } });
|
|
158
|
+
const real = await realDir(requested, "ERR_ROOT_NOT_ALLOWED");
|
|
159
|
+
for (const allowed of policy.roots) if (isSameOrInside(allowed, real)) return {
|
|
160
|
+
rootReal: real,
|
|
161
|
+
effectiveRoot: allowed
|
|
162
|
+
};
|
|
163
|
+
throw new AxiomError("ERR_ROOT_NOT_ALLOWED", `root is outside the allowlist: ${requested}`, { details: {
|
|
164
|
+
root: requested,
|
|
165
|
+
real,
|
|
166
|
+
roots: [...policy.roots]
|
|
167
|
+
} });
|
|
168
|
+
}
|
|
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
|
|
189
|
+
//#region src/store.ts
|
|
190
|
+
/** `<root>/.axiom/manifests/<hex>.json` and `<root>/.axiom/reports/<hex>.json`. */
|
|
191
|
+
function manifestsDir(root) {
|
|
192
|
+
return path.join(root, ".axiom", "manifests");
|
|
193
|
+
}
|
|
194
|
+
function reportsDir(root) {
|
|
195
|
+
return path.join(root, ".axiom", "reports");
|
|
196
|
+
}
|
|
197
|
+
function hexOf(ref) {
|
|
198
|
+
return ref.slice(7);
|
|
199
|
+
}
|
|
200
|
+
function toDigestRef(shaOrRef) {
|
|
201
|
+
const ref = shaOrRef.startsWith("sha256:") ? shaOrRef : `sha256:${shaOrRef}`;
|
|
202
|
+
const parsed = DigestRefSchema.safeParse(ref);
|
|
203
|
+
if (!parsed.success) throw new AxiomError("ERR_NOT_FOUND", `not a sha256 digest: ${shaOrRef}`);
|
|
204
|
+
return parsed.data;
|
|
205
|
+
}
|
|
206
|
+
async function writeJsonAtomic$1(file, value) {
|
|
207
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
208
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
209
|
+
await writeFile(tmp, JSON.stringify(value), "utf8");
|
|
210
|
+
await rename(tmp, file);
|
|
211
|
+
}
|
|
212
|
+
async function readJsonOrUndefined$1(file) {
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (err.code === "ENOENT") return void 0;
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function saveManifest(root, bundle) {
|
|
221
|
+
const file = path.join(manifestsDir(root), `${hexOf(bundle.manifestDigest)}.json`);
|
|
222
|
+
await writeJsonAtomic$1(file, bundle);
|
|
223
|
+
return file;
|
|
224
|
+
}
|
|
225
|
+
async function saveReport(root, report) {
|
|
226
|
+
const file = path.join(reportsDir(root), `${hexOf(report.manifestDigest)}.json`);
|
|
227
|
+
await writeJsonAtomic$1(file, report);
|
|
228
|
+
return file;
|
|
229
|
+
}
|
|
230
|
+
/** Search every root (allowlisted + seen) for a stored bundle. */
|
|
231
|
+
async function loadManifest(roots, ref) {
|
|
232
|
+
for (const root of roots) {
|
|
233
|
+
const raw = await readJsonOrUndefined$1(path.join(manifestsDir(root), `${hexOf(ref)}.json`));
|
|
234
|
+
if (raw !== void 0) return ManifestBundleSchema.parse(raw);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function loadReport(roots, ref) {
|
|
238
|
+
for (const root of roots) {
|
|
239
|
+
const raw = await readJsonOrUndefined$1(path.join(reportsDir(root), `${hexOf(ref)}.json`));
|
|
240
|
+
if (raw !== void 0) return CheckReportSchema.parse(raw);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function loadApplied(roots, ref) {
|
|
244
|
+
for (const root of roots) {
|
|
245
|
+
const raw = await readJsonOrUndefined$1(appliedPath(root, ref));
|
|
246
|
+
if (raw !== void 0) return ApplyResultSchema.parse(raw);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function listStored(roots, sub) {
|
|
250
|
+
const out = [];
|
|
251
|
+
for (const root of roots) {
|
|
252
|
+
let names;
|
|
253
|
+
try {
|
|
254
|
+
names = await readdir(path.join(root, ".axiom", sub));
|
|
255
|
+
} catch {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
for (const n of names) {
|
|
259
|
+
const m = /^([0-9a-f]{64})\.json$/.exec(n);
|
|
260
|
+
if (m?.[1] !== void 0) out.push({
|
|
261
|
+
root,
|
|
262
|
+
sha: m[1]
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
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
|
|
556
|
+
//#region src/tools.ts
|
|
557
|
+
/** Hard cap on any single `bundle`/`plan` argument, measured as UTF-8 JSON bytes (§(f) payload size). */
|
|
558
|
+
const BUNDLE_BYTES_MAX = 4194304;
|
|
559
|
+
/** Findings/errors echoed in the text summary. */
|
|
560
|
+
const SUMMARY_LIST_MAX = 20;
|
|
561
|
+
const READ = {
|
|
562
|
+
readOnlyHint: true,
|
|
563
|
+
destructiveHint: false,
|
|
564
|
+
idempotentHint: true,
|
|
565
|
+
openWorldHint: false
|
|
566
|
+
};
|
|
567
|
+
/** Writes only under `<root>/.axiom/` (CAS blobs, stored manifests) — never the working tree. */
|
|
568
|
+
const ACT = {
|
|
569
|
+
readOnlyHint: false,
|
|
570
|
+
destructiveHint: false,
|
|
571
|
+
idempotentHint: true,
|
|
572
|
+
openWorldHint: false
|
|
573
|
+
};
|
|
574
|
+
const WRITE = {
|
|
575
|
+
readOnlyHint: false,
|
|
576
|
+
destructiveHint: true,
|
|
577
|
+
idempotentHint: true,
|
|
578
|
+
openWorldHint: false
|
|
579
|
+
};
|
|
580
|
+
function riskClassOf(a) {
|
|
581
|
+
if (a.readOnlyHint) return "READ";
|
|
582
|
+
return a.destructiveHint ? "SENSITIVE" : "ACT";
|
|
583
|
+
}
|
|
584
|
+
function defineTool(def) {
|
|
585
|
+
return {
|
|
586
|
+
...def,
|
|
587
|
+
riskClass: riskClassOf(def.annotations)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const LooseObject = z.record(z.string(), z.unknown());
|
|
591
|
+
const RootArg = z.string().optional().describe("Absolute repository root; must equal or lie inside an allowlisted --root");
|
|
592
|
+
const ProfileArg = z.string().optional().describe("Profile name (builtin default|strict|permissive, or <root>/.axiom/profiles/<name>.json)");
|
|
593
|
+
/** Reject oversized payloads before any deeper parsing. */
|
|
594
|
+
function guardPayloadSize(label, value) {
|
|
595
|
+
const bytes = Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
|
|
596
|
+
if (bytes > 4194304) throw new AxiomError("ERR_BUNDLE_TOO_LARGE", `${label} is ${bytes} bytes; max ${BUNDLE_BYTES_MAX}`, { details: {
|
|
597
|
+
bytes,
|
|
598
|
+
max: BUNDLE_BYTES_MAX
|
|
599
|
+
} });
|
|
600
|
+
}
|
|
601
|
+
function parseBundle(raw) {
|
|
602
|
+
guardPayloadSize("bundle", raw);
|
|
603
|
+
const parsed = ManifestBundleSchema.safeParse(raw);
|
|
604
|
+
if (!parsed.success) throw new AxiomError("ERR_INVALID_MANIFEST", "bundle does not match ManifestBundleSchema", { details: { issues: parsed.error.issues.slice(0, 20).map((i) => ({
|
|
605
|
+
path: i.path.map(String).join("."),
|
|
606
|
+
message: i.message
|
|
607
|
+
})) } });
|
|
608
|
+
return parsed.data;
|
|
609
|
+
}
|
|
610
|
+
async function profileFor(ctx, bundle, name, rootReal) {
|
|
611
|
+
const searchDirs = rootReal === void 0 ? [] : [path.join(rootReal, ".axiom", "profiles")];
|
|
612
|
+
const profileName = name ?? bundle.manifest.profile;
|
|
613
|
+
ctx.log.debug("profile", {
|
|
614
|
+
name: profileName,
|
|
615
|
+
searchDirs
|
|
616
|
+
});
|
|
617
|
+
return loadProfile(profileName, { searchDirs });
|
|
618
|
+
}
|
|
619
|
+
async function checkBundle(ctx, bundle, profileName, rootReal) {
|
|
620
|
+
const opts = {
|
|
621
|
+
bundle,
|
|
622
|
+
profile: await profileFor(ctx, bundle, profileName, rootReal),
|
|
623
|
+
checks: bundle.manifest.checks,
|
|
624
|
+
...ctx.guards
|
|
625
|
+
};
|
|
626
|
+
if (rootReal !== void 0) {
|
|
627
|
+
opts.root = rootReal;
|
|
628
|
+
opts.casDir = path.join(rootReal, ".axiom", "cas");
|
|
629
|
+
}
|
|
630
|
+
const report = await runChecks(opts);
|
|
631
|
+
if (rootReal !== void 0) {
|
|
632
|
+
await saveReport(rootReal, report);
|
|
633
|
+
ctx.seenRoots.add(rootReal);
|
|
634
|
+
}
|
|
635
|
+
return report;
|
|
636
|
+
}
|
|
637
|
+
/** Optional root: explicit → allowlist check; absent → the single root if there is one, else none. */
|
|
638
|
+
async function optionalRoot(ctx, requested) {
|
|
639
|
+
if (requested !== void 0 && requested !== "") return (await resolveRoot(ctx.policy, requested)).rootReal;
|
|
640
|
+
if (ctx.policy.roots.size === 1) return (await resolveRoot(ctx.policy)).rootReal;
|
|
641
|
+
}
|
|
642
|
+
function countBy(items, key) {
|
|
643
|
+
const out = {};
|
|
644
|
+
for (const it of items) out[key(it)] = (out[key(it)] ?? 0) + 1;
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
const IssueSchema = z.object({
|
|
648
|
+
path: z.string(),
|
|
649
|
+
message: z.string(),
|
|
650
|
+
code: z.string().optional()
|
|
651
|
+
});
|
|
652
|
+
const PlanValidateOutput = z.object({
|
|
653
|
+
ok: z.boolean(),
|
|
654
|
+
planDigest: DigestRefSchema.optional(),
|
|
655
|
+
errors: z.array(IssueSchema)
|
|
656
|
+
});
|
|
657
|
+
const ManifestVerifyOutput = z.object({
|
|
658
|
+
ok: z.boolean(),
|
|
659
|
+
manifestDigest: DigestRefSchema.optional(),
|
|
660
|
+
canonical: z.boolean(),
|
|
661
|
+
signed: z.boolean(),
|
|
662
|
+
missing: z.array(z.string()),
|
|
663
|
+
errors: z.array(z.object({
|
|
664
|
+
code: ErrorCodeSchema,
|
|
665
|
+
message: z.string(),
|
|
666
|
+
path: z.string().optional()
|
|
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()
|
|
679
|
+
});
|
|
680
|
+
const RollbackOutput = z.object({
|
|
681
|
+
manifestDigest: DigestRefSchema,
|
|
682
|
+
status: z.literal("rolled-back"),
|
|
683
|
+
phase: JournalPhaseSchema,
|
|
684
|
+
steps: z.int().nonnegative(),
|
|
685
|
+
root: z.string()
|
|
686
|
+
});
|
|
687
|
+
const ManifestDiffOutput = z.object({
|
|
688
|
+
added: z.array(z.string()),
|
|
689
|
+
removed: z.array(z.string()),
|
|
690
|
+
changed: z.array(z.object({
|
|
691
|
+
path: z.string(),
|
|
692
|
+
from: z.string().nullable(),
|
|
693
|
+
to: z.string().nullable()
|
|
694
|
+
}))
|
|
695
|
+
});
|
|
696
|
+
const RootsListOutput = z.object({ roots: z.array(z.object({
|
|
697
|
+
path: z.string(),
|
|
698
|
+
writable: z.boolean(),
|
|
699
|
+
hasGit: z.boolean()
|
|
700
|
+
})) });
|
|
701
|
+
const Pos = z.object({
|
|
702
|
+
line: z.int().positive(),
|
|
703
|
+
column: z.int().positive()
|
|
704
|
+
});
|
|
705
|
+
const AxmParseOutput = z.object({
|
|
706
|
+
plan: PlanSchema.optional(),
|
|
707
|
+
diagnostics: z.array(z.object({
|
|
708
|
+
severity: z.enum(["error", "warning"]),
|
|
709
|
+
code: ErrorCodeSchema,
|
|
710
|
+
message: z.string(),
|
|
711
|
+
range: z.object({
|
|
712
|
+
start: Pos,
|
|
713
|
+
end: Pos
|
|
714
|
+
})
|
|
715
|
+
}))
|
|
716
|
+
});
|
|
717
|
+
const BundleOrRef = z.union([DigestRefSchema, LooseObject]).describe("A ManifestBundle object, or `sha256:<hex>` of a bundle stored under <root>/.axiom/manifests");
|
|
718
|
+
const TOOL_DEFS = [
|
|
719
|
+
defineTool({
|
|
720
|
+
name: "axiom_plan_validate",
|
|
721
|
+
title: "Validate a Plan",
|
|
722
|
+
description: "Validate a Plan against PlanSchema and, when all sources are inline, compute its planDigest. Read-only; touches no files.",
|
|
723
|
+
inputSchema: { plan: LooseObject.describe("Plan document (apiVersion axiom.dev/v2, kind Plan)") },
|
|
724
|
+
outputSchema: PlanValidateOutput,
|
|
725
|
+
annotations: READ,
|
|
726
|
+
async handler(_ctx, { plan }) {
|
|
727
|
+
guardPayloadSize("plan", plan);
|
|
728
|
+
const parsed = PlanSchema.safeParse(plan);
|
|
729
|
+
if (!parsed.success) return {
|
|
730
|
+
ok: false,
|
|
731
|
+
errors: parsed.error.issues.map((i) => {
|
|
732
|
+
const code = i.params?.code;
|
|
733
|
+
const out = {
|
|
734
|
+
path: i.path.map(String).join("."),
|
|
735
|
+
message: i.message
|
|
736
|
+
};
|
|
737
|
+
if (typeof code === "string") out.code = code;
|
|
738
|
+
return out;
|
|
739
|
+
})
|
|
740
|
+
};
|
|
741
|
+
try {
|
|
742
|
+
const { bundle } = await compilePlan(parsed.data, {
|
|
743
|
+
store: "inline",
|
|
744
|
+
emitters: EMITTERS
|
|
745
|
+
});
|
|
746
|
+
return {
|
|
747
|
+
ok: true,
|
|
748
|
+
planDigest: bundle.manifest.planDigest,
|
|
749
|
+
errors: []
|
|
750
|
+
};
|
|
751
|
+
} catch (err) {
|
|
752
|
+
if (err instanceof AxiomError && err.code === "ERR_BLOB_MISSING") return {
|
|
753
|
+
ok: true,
|
|
754
|
+
errors: []
|
|
755
|
+
};
|
|
756
|
+
throw err;
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
summarize: (o) => ({
|
|
760
|
+
ok: o.ok,
|
|
761
|
+
planDigest: o.planDigest,
|
|
762
|
+
errors: o.errors.slice(0, 20)
|
|
763
|
+
})
|
|
764
|
+
}),
|
|
765
|
+
defineTool({
|
|
766
|
+
name: "axiom_plan_compile",
|
|
767
|
+
title: "Compile a Plan into a ManifestBundle",
|
|
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.",
|
|
769
|
+
inputSchema: {
|
|
770
|
+
plan: LooseObject.describe("Plan document"),
|
|
771
|
+
store: z.enum(["inline", "cas"]).optional().describe("Blob transport; default inline"),
|
|
772
|
+
root: RootArg
|
|
773
|
+
},
|
|
774
|
+
outputSchema: ManifestBundleSchema,
|
|
775
|
+
annotations: ACT,
|
|
776
|
+
async handler(ctx, { plan, store, root }) {
|
|
777
|
+
guardPayloadSize("plan", plan);
|
|
778
|
+
const rootReal = root !== void 0 || store === "cas" ? (await resolveRoot(ctx.policy, root)).rootReal : void 0;
|
|
779
|
+
const opts = {
|
|
780
|
+
store: store ?? "inline",
|
|
781
|
+
emitters: EMITTERS
|
|
782
|
+
};
|
|
783
|
+
if (rootReal !== void 0) opts.root = rootReal;
|
|
784
|
+
const { bundle } = await compilePlan(plan, opts);
|
|
785
|
+
if (rootReal !== void 0) {
|
|
786
|
+
await saveManifest(rootReal, bundle);
|
|
787
|
+
ctx.seenRoots.add(rootReal);
|
|
788
|
+
}
|
|
789
|
+
ctx.log.info("compiled", {
|
|
790
|
+
manifestDigest: bundle.manifestDigest,
|
|
791
|
+
artifacts: bundle.manifest.artifacts.length
|
|
792
|
+
});
|
|
793
|
+
return bundle;
|
|
794
|
+
},
|
|
795
|
+
summarize: (b) => ({
|
|
796
|
+
manifestDigest: b.manifestDigest,
|
|
797
|
+
planDigest: b.manifest.planDigest,
|
|
798
|
+
name: b.manifest.name,
|
|
799
|
+
profile: b.manifest.profile,
|
|
800
|
+
artifacts: b.manifest.artifacts.length,
|
|
801
|
+
blobs: Object.keys(b.blobs).length
|
|
802
|
+
})
|
|
803
|
+
}),
|
|
804
|
+
defineTool({
|
|
805
|
+
name: "axiom_manifest_verify",
|
|
806
|
+
title: "Verify a ManifestBundle",
|
|
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
|
+
},
|
|
812
|
+
outputSchema: ManifestVerifyOutput,
|
|
813
|
+
annotations: READ,
|
|
814
|
+
async handler(ctx, { bundle, root }) {
|
|
815
|
+
guardPayloadSize("bundle", bundle);
|
|
816
|
+
const r = verifyBundle(bundle);
|
|
817
|
+
const out = {
|
|
818
|
+
ok: r.ok,
|
|
819
|
+
canonical: r.canonical,
|
|
820
|
+
signed: r.signed,
|
|
821
|
+
missing: r.missing,
|
|
822
|
+
errors: r.errors
|
|
823
|
+
};
|
|
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
|
+
}
|
|
836
|
+
return out;
|
|
837
|
+
},
|
|
838
|
+
summarize: (o) => ({
|
|
839
|
+
ok: o.ok,
|
|
840
|
+
manifestDigest: o.manifestDigest,
|
|
841
|
+
canonical: o.canonical,
|
|
842
|
+
signed: o.signed,
|
|
843
|
+
keyids: o.signatures?.keyids,
|
|
844
|
+
missing: o.missing.length,
|
|
845
|
+
errors: o.errors.slice(0, 20)
|
|
846
|
+
})
|
|
847
|
+
}),
|
|
848
|
+
defineTool({
|
|
849
|
+
name: "axiom_check",
|
|
850
|
+
title: "Run policy checks on a bundle",
|
|
851
|
+
description: "Evaluate the profile's predicates (plus the manifest's own checks) against the bundle. Repo facts are read from the root when one is available and the profile allows it. Verdict `error` means a provider could not run — never a silent pass.",
|
|
852
|
+
inputSchema: {
|
|
853
|
+
bundle: LooseObject.describe("ManifestBundle"),
|
|
854
|
+
profile: ProfileArg,
|
|
855
|
+
root: RootArg
|
|
856
|
+
},
|
|
857
|
+
outputSchema: CheckReportSchema,
|
|
858
|
+
annotations: READ,
|
|
859
|
+
async handler(ctx, { bundle, profile, root }) {
|
|
860
|
+
return checkBundle(ctx, parseBundle(bundle), profile, await optionalRoot(ctx, root));
|
|
861
|
+
},
|
|
862
|
+
summarize: summarizeReport
|
|
863
|
+
}),
|
|
864
|
+
defineTool({
|
|
865
|
+
name: "axiom_apply_dry_run",
|
|
866
|
+
title: "Dry-run apply (stage + diff, no writes to the tree)",
|
|
867
|
+
description: "Stage the bundle under <root>/.axiom/staging, run pre-apply checks and produce a unified diff against the current tree. Nothing outside .axiom/ is touched. Echo the returned manifestDigest as `confirmDigest` to axiom_apply.",
|
|
868
|
+
inputSchema: {
|
|
869
|
+
bundle: LooseObject.describe("ManifestBundle"),
|
|
870
|
+
root: RootArg,
|
|
871
|
+
profile: ProfileArg
|
|
872
|
+
},
|
|
873
|
+
outputSchema: ApplyResultSchema,
|
|
874
|
+
annotations: READ,
|
|
875
|
+
async handler(ctx, { bundle, root, profile }) {
|
|
876
|
+
const parsed = parseBundle(bundle);
|
|
877
|
+
const { rootReal } = await resolveRoot(ctx.policy, root);
|
|
878
|
+
return await apply({
|
|
879
|
+
bundle: parsed,
|
|
880
|
+
root: rootReal,
|
|
881
|
+
mode: "dry-run",
|
|
882
|
+
preChecks: () => checkBundle(ctx, parsed, profile, rootReal)
|
|
883
|
+
});
|
|
884
|
+
},
|
|
885
|
+
summarize: summarizeApply
|
|
886
|
+
}),
|
|
887
|
+
defineTool({
|
|
888
|
+
name: "axiom_apply",
|
|
889
|
+
title: "Apply a bundle to the filesystem (two-phase commit)",
|
|
890
|
+
description: "Transactionally write the bundle into the root: pre-image verification, staging, journal, atomic renames, scoped rollback on failure. Requires `confirmDigest === bundle.manifestDigest` (echo the digest you saw in dry-run). Idempotent: re-applying an applied digest is a no-op.",
|
|
891
|
+
inputSchema: {
|
|
892
|
+
bundle: LooseObject.describe("ManifestBundle"),
|
|
893
|
+
root: RootArg,
|
|
894
|
+
profile: ProfileArg,
|
|
895
|
+
confirmDigest: z.string().optional().describe("Must equal bundle.manifestDigest"),
|
|
896
|
+
mode: z.enum(["fs", "pr"]).optional().describe("fs (default) writes files; pr additionally creates a git branch and commits exactly the touched paths (no push, no PR creation)"),
|
|
897
|
+
branch: z.string().optional().describe("pr mode: branch name (default axiom/<name>/<digest12>)"),
|
|
898
|
+
commitMessage: z.string().optional().describe("pr mode: commit message (passed to git on stdin)")
|
|
899
|
+
},
|
|
900
|
+
outputSchema: ApplyResultSchema,
|
|
901
|
+
annotations: WRITE,
|
|
902
|
+
async handler(ctx, { bundle, root, profile, confirmDigest, mode, branch, commitMessage }) {
|
|
903
|
+
const parsed = parseBundle(bundle);
|
|
904
|
+
if (confirmDigest !== parsed.manifestDigest) throw new AxiomError("ERR_CONFIRM_DIGEST_MISMATCH", "confirmDigest must equal bundle.manifestDigest", { details: {
|
|
905
|
+
confirmDigest: confirmDigest ?? null,
|
|
906
|
+
manifestDigest: parsed.manifestDigest
|
|
907
|
+
} });
|
|
908
|
+
const { rootReal } = await resolveRoot(ctx.policy, root);
|
|
909
|
+
const profileDoc = await profileFor(ctx, parsed, profile, rootReal);
|
|
910
|
+
const result = await apply({
|
|
911
|
+
bundle: parsed,
|
|
912
|
+
root: rootReal,
|
|
913
|
+
mode: mode ?? "fs",
|
|
914
|
+
confirmDigest,
|
|
915
|
+
...branch === void 0 ? {} : { branch },
|
|
916
|
+
...commitMessage === void 0 ? {} : { commitMessage },
|
|
917
|
+
preChecks: () => checkBundle(ctx, parsed, profile, rootReal)
|
|
918
|
+
});
|
|
919
|
+
if (result.status === "applied" || result.status === "noop") {
|
|
920
|
+
await saveManifest(rootReal, parsed);
|
|
921
|
+
ctx.seenRoots.add(rootReal);
|
|
922
|
+
}
|
|
923
|
+
if (result.status === "applied" && profileWantsAntiRollback([...profileDoc.checks, ...parsed.manifest.checks])) await advanceTrustState(rootReal, parsed);
|
|
924
|
+
ctx.log.info("apply", {
|
|
925
|
+
manifestDigest: parsed.manifestDigest,
|
|
926
|
+
status: result.status,
|
|
927
|
+
root: rootReal
|
|
928
|
+
});
|
|
929
|
+
return result;
|
|
930
|
+
},
|
|
931
|
+
summarize: summarizeApply
|
|
932
|
+
}),
|
|
933
|
+
defineTool({
|
|
934
|
+
name: "axiom_rollback",
|
|
935
|
+
title: "Roll back an applied manifest",
|
|
936
|
+
description: "Replay the journal of a committed/committing manifest in reverse: restore backups, remove created files, drop the applied marker.",
|
|
937
|
+
inputSchema: {
|
|
938
|
+
root: RootArg,
|
|
939
|
+
manifestDigest: z.string().describe("`sha256:<hex>` (or bare hex) of the manifest to roll back")
|
|
940
|
+
},
|
|
941
|
+
outputSchema: RollbackOutput,
|
|
942
|
+
annotations: WRITE,
|
|
943
|
+
async handler(ctx, { root, manifestDigest }) {
|
|
944
|
+
const ref = toDigestRef(manifestDigest);
|
|
945
|
+
const { rootReal } = await resolveRoot(ctx.policy, root);
|
|
946
|
+
const journal = await rollback(rootReal, ref);
|
|
947
|
+
ctx.log.info("rollback", {
|
|
948
|
+
manifestDigest: ref,
|
|
949
|
+
root: rootReal,
|
|
950
|
+
phase: journal.phase
|
|
951
|
+
});
|
|
952
|
+
return {
|
|
953
|
+
manifestDigest: ref,
|
|
954
|
+
status: "rolled-back",
|
|
955
|
+
phase: journal.phase,
|
|
956
|
+
steps: journal.steps.length,
|
|
957
|
+
root: rootReal
|
|
958
|
+
};
|
|
959
|
+
},
|
|
960
|
+
summarize: (o) => o
|
|
961
|
+
}),
|
|
962
|
+
defineTool({
|
|
963
|
+
name: "axiom_manifest_diff",
|
|
964
|
+
title: "Diff two manifests",
|
|
965
|
+
description: "Compare two manifests by artifact path and digest. Each side is a ManifestBundle or a `sha256:<hex>` reference to a bundle stored under an allowlisted root.",
|
|
966
|
+
inputSchema: {
|
|
967
|
+
a: BundleOrRef,
|
|
968
|
+
b: BundleOrRef
|
|
969
|
+
},
|
|
970
|
+
outputSchema: ManifestDiffOutput,
|
|
971
|
+
annotations: READ,
|
|
972
|
+
async handler(ctx, { a, b }) {
|
|
973
|
+
const [ba, bb] = await Promise.all([resolveBundleOrRef(ctx, a, "a"), resolveBundleOrRef(ctx, b, "b")]);
|
|
974
|
+
return diffManifests(ba.manifest, bb.manifest);
|
|
975
|
+
},
|
|
976
|
+
summarize: (d) => ({
|
|
977
|
+
added: d.added.length,
|
|
978
|
+
removed: d.removed.length,
|
|
979
|
+
changed: d.changed.length,
|
|
980
|
+
sample: {
|
|
981
|
+
added: d.added.slice(0, 20),
|
|
982
|
+
removed: d.removed.slice(0, 20),
|
|
983
|
+
changed: d.changed.slice(0, 20)
|
|
984
|
+
}
|
|
985
|
+
})
|
|
986
|
+
}),
|
|
987
|
+
defineTool({
|
|
988
|
+
name: "axiom_axm_parse",
|
|
989
|
+
title: "Parse .axm source into a Plan",
|
|
990
|
+
description: "Parse .axm v2 text into a Plan with 1-based {line, column} diagnostics; `plan` is present only when error-free. Read-only.",
|
|
991
|
+
inputSchema: { source: z.string().describe(".axm source text") },
|
|
992
|
+
outputSchema: AxmParseOutput,
|
|
993
|
+
annotations: READ,
|
|
994
|
+
async handler(_ctx, { source }) {
|
|
995
|
+
guardPayloadSize("source", source);
|
|
996
|
+
const { parseAxm } = await import("./axm-lazy-WdJHPy6W.js");
|
|
997
|
+
const r = parseAxm(source);
|
|
998
|
+
return r.plan === void 0 ? { diagnostics: r.diagnostics } : r;
|
|
999
|
+
},
|
|
1000
|
+
summarize: (o) => ({
|
|
1001
|
+
ok: o.plan !== void 0,
|
|
1002
|
+
name: o.plan?.name,
|
|
1003
|
+
diagnostics: o.diagnostics.slice(0, 20)
|
|
1004
|
+
})
|
|
1005
|
+
}),
|
|
1006
|
+
defineTool({
|
|
1007
|
+
name: "axiom_roots_list",
|
|
1008
|
+
title: "List allowlisted roots",
|
|
1009
|
+
description: "The frozen set of roots this server may read and write, as given by --root at startup.",
|
|
1010
|
+
inputSchema: {},
|
|
1011
|
+
outputSchema: RootsListOutput,
|
|
1012
|
+
annotations: READ,
|
|
1013
|
+
async handler(ctx) {
|
|
1014
|
+
const roots = [];
|
|
1015
|
+
for (const p of ctx.policy.roots) {
|
|
1016
|
+
const [writable, hasGit] = await Promise.all([access(p, constants.W_OK).then(() => true, () => false), stat(path.join(p, ".git")).then(() => true, () => false)]);
|
|
1017
|
+
roots.push({
|
|
1018
|
+
path: p,
|
|
1019
|
+
writable,
|
|
1020
|
+
hasGit
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return { roots };
|
|
1024
|
+
},
|
|
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
|
+
})
|
|
1066
|
+
})
|
|
1067
|
+
];
|
|
1068
|
+
async function resolveBundleOrRef(ctx, v, label) {
|
|
1069
|
+
if (typeof v === "string") {
|
|
1070
|
+
const ref = toDigestRef(v);
|
|
1071
|
+
const found = await loadManifest(/* @__PURE__ */ new Set([...ctx.policy.roots, ...ctx.seenRoots]), ref);
|
|
1072
|
+
if (found === void 0) throw new AxiomError("ERR_NOT_FOUND", `${label}: no stored manifest for ${ref}`, { details: { ref } });
|
|
1073
|
+
return found;
|
|
1074
|
+
}
|
|
1075
|
+
return parseBundle(v);
|
|
1076
|
+
}
|
|
1077
|
+
function summarizeReport(r) {
|
|
1078
|
+
return {
|
|
1079
|
+
manifestDigest: r.manifestDigest,
|
|
1080
|
+
profile: r.profile,
|
|
1081
|
+
verdict: r.verdict,
|
|
1082
|
+
counts: countBy(r.findings, (f) => f.severity),
|
|
1083
|
+
findings: r.findings.slice(0, 20).map((f) => ({
|
|
1084
|
+
id: f.id,
|
|
1085
|
+
severity: f.severity,
|
|
1086
|
+
message: f.message,
|
|
1087
|
+
path: f.path
|
|
1088
|
+
})),
|
|
1089
|
+
providers: r.providers,
|
|
1090
|
+
durationMs: r.durationMs
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
function summarizeApply(r) {
|
|
1094
|
+
return {
|
|
1095
|
+
manifestDigest: r.manifestDigest,
|
|
1096
|
+
mode: r.mode,
|
|
1097
|
+
status: r.status,
|
|
1098
|
+
root: r.root,
|
|
1099
|
+
files: countBy(r.files, (f) => f.status),
|
|
1100
|
+
diffBytes: r.diff === void 0 ? void 0 : Buffer.byteLength(r.diff, "utf8"),
|
|
1101
|
+
journal: r.journal,
|
|
1102
|
+
error: r.error,
|
|
1103
|
+
sample: r.files.slice(0, 20).map((f) => ({
|
|
1104
|
+
path: f.path,
|
|
1105
|
+
op: f.op,
|
|
1106
|
+
status: f.status
|
|
1107
|
+
}))
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function toolByName(name) {
|
|
1111
|
+
return TOOL_DEFS.find((t) => t.name === name);
|
|
1112
|
+
}
|
|
1113
|
+
//#endregion
|
|
1114
|
+
//#region src/server.ts
|
|
1115
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
1116
|
+
const SERVER_NAME = "axiom";
|
|
1117
|
+
const SERVER_VERSION = pkg.version;
|
|
1118
|
+
function toStructuredError(err) {
|
|
1119
|
+
if (err instanceof AxiomError) return err.toJSON();
|
|
1120
|
+
if (err instanceof z.ZodError) return {
|
|
1121
|
+
code: "ERR_INVALID_PLAN",
|
|
1122
|
+
message: "input does not match schema",
|
|
1123
|
+
details: { issues: err.issues.slice(0, 20).map((i) => ({
|
|
1124
|
+
path: i.path.join("."),
|
|
1125
|
+
message: i.message
|
|
1126
|
+
})) }
|
|
1127
|
+
};
|
|
1128
|
+
return {
|
|
1129
|
+
code: "ERR_INTERNAL",
|
|
1130
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function errorResult(err) {
|
|
1134
|
+
const structured = toStructuredError(err);
|
|
1135
|
+
return {
|
|
1136
|
+
isError: true,
|
|
1137
|
+
content: [{
|
|
1138
|
+
type: "text",
|
|
1139
|
+
text: JSON.stringify(structured)
|
|
1140
|
+
}],
|
|
1141
|
+
structuredContent: structured
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
/** Wrap a tool handler: never throws; AxiomError → isError result with the closed code. */
|
|
1145
|
+
function wrapHandler(def, ctx) {
|
|
1146
|
+
return async (input) => {
|
|
1147
|
+
try {
|
|
1148
|
+
const output = await def.handler(ctx, input);
|
|
1149
|
+
const structured = def.outputSchema.parse(output);
|
|
1150
|
+
return {
|
|
1151
|
+
content: [{
|
|
1152
|
+
type: "text",
|
|
1153
|
+
text: JSON.stringify(def.summarize(structured))
|
|
1154
|
+
}],
|
|
1155
|
+
structuredContent: structured
|
|
1156
|
+
};
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
ctx.log.warn("tool failed", {
|
|
1159
|
+
tool: def.name,
|
|
1160
|
+
error: toStructuredError(err)
|
|
1161
|
+
});
|
|
1162
|
+
return errorResult(err);
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function json(uri, value) {
|
|
1167
|
+
return { contents: [{
|
|
1168
|
+
uri,
|
|
1169
|
+
mimeType: "application/json",
|
|
1170
|
+
text: JSON.stringify(value, null, 2)
|
|
1171
|
+
}] };
|
|
1172
|
+
}
|
|
1173
|
+
function notFound(uri) {
|
|
1174
|
+
throw new AxiomError("ERR_NOT_FOUND", `resource not found: ${uri}`);
|
|
1175
|
+
}
|
|
1176
|
+
function createServer(policy, opts = {}) {
|
|
1177
|
+
const log = opts.log ?? silentLogger;
|
|
1178
|
+
const ctx = {
|
|
1179
|
+
policy,
|
|
1180
|
+
log,
|
|
1181
|
+
seenRoots: /* @__PURE__ */ new Set()
|
|
1182
|
+
};
|
|
1183
|
+
if (opts.guards !== void 0) ctx.guards = opts.guards;
|
|
1184
|
+
const server = new McpServer({
|
|
1185
|
+
name: SERVER_NAME,
|
|
1186
|
+
version: SERVER_VERSION
|
|
1187
|
+
}, { capabilities: {
|
|
1188
|
+
tools: {},
|
|
1189
|
+
resources: {}
|
|
1190
|
+
} });
|
|
1191
|
+
for (const def of opts.tools ?? TOOL_DEFS) server.registerTool(def.name, {
|
|
1192
|
+
title: def.title,
|
|
1193
|
+
description: def.description,
|
|
1194
|
+
inputSchema: def.inputSchema,
|
|
1195
|
+
outputSchema: def.outputSchema,
|
|
1196
|
+
annotations: {
|
|
1197
|
+
title: def.title,
|
|
1198
|
+
...def.annotations
|
|
1199
|
+
}
|
|
1200
|
+
}, wrapHandler(def, ctx));
|
|
1201
|
+
const allRoots = () => /* @__PURE__ */ new Set([...policy.roots, ...ctx.seenRoots]);
|
|
1202
|
+
const listOf = (sub, scheme) => async () => {
|
|
1203
|
+
return { resources: (await listStored(allRoots(), sub)).map((i) => ({
|
|
1204
|
+
uri: `axiom://${scheme}/${i.sha}`,
|
|
1205
|
+
name: i.sha,
|
|
1206
|
+
mimeType: "application/json"
|
|
1207
|
+
})) };
|
|
1208
|
+
};
|
|
1209
|
+
server.registerResource("manifest", new ResourceTemplate("axiom://manifest/{sha}", { list: listOf("manifests", "manifest") }), {
|
|
1210
|
+
title: "Stored ManifestBundle",
|
|
1211
|
+
mimeType: "application/json"
|
|
1212
|
+
}, async (uri, { sha }) => {
|
|
1213
|
+
const found = await loadManifest(allRoots(), toDigestRef(String(sha)));
|
|
1214
|
+
return found === void 0 ? notFound(uri.href) : json(uri.href, found);
|
|
1215
|
+
});
|
|
1216
|
+
server.registerResource("report", new ResourceTemplate("axiom://report/{sha}", { list: listOf("reports", "report") }), {
|
|
1217
|
+
title: "Last CheckReport for a manifest",
|
|
1218
|
+
mimeType: "application/json"
|
|
1219
|
+
}, async (uri, { sha }) => {
|
|
1220
|
+
const found = await loadReport(allRoots(), toDigestRef(String(sha)));
|
|
1221
|
+
return found === void 0 ? notFound(uri.href) : json(uri.href, found);
|
|
1222
|
+
});
|
|
1223
|
+
server.registerResource("applied", new ResourceTemplate("axiom://applied/{sha}", { list: listOf("applied", "applied") }), {
|
|
1224
|
+
title: "ApplyResult of an applied manifest",
|
|
1225
|
+
mimeType: "application/json"
|
|
1226
|
+
}, async (uri, { sha }) => {
|
|
1227
|
+
const found = await loadApplied(allRoots(), toDigestRef(String(sha)));
|
|
1228
|
+
return found === void 0 ? notFound(uri.href) : json(uri.href, found);
|
|
1229
|
+
});
|
|
1230
|
+
server.registerResource("profile", new ResourceTemplate("axiom://profile/{name}", { list: async () => ({ resources: [
|
|
1231
|
+
"default",
|
|
1232
|
+
"strict",
|
|
1233
|
+
"permissive"
|
|
1234
|
+
].map((n) => ({
|
|
1235
|
+
uri: `axiom://profile/${n}`,
|
|
1236
|
+
name: n,
|
|
1237
|
+
mimeType: "application/json"
|
|
1238
|
+
})) }) }), {
|
|
1239
|
+
title: "Resolved check profile",
|
|
1240
|
+
mimeType: "application/json"
|
|
1241
|
+
}, async (uri, { name }) => {
|
|
1242
|
+
const searchDirs = [...policy.roots].map((r) => `${r}/.axiom/profiles`);
|
|
1243
|
+
return json(uri.href, await loadProfile(String(name), { searchDirs }));
|
|
1244
|
+
});
|
|
1245
|
+
server.registerResource("schema", new ResourceTemplate("axiom://schema/{kind}", {
|
|
1246
|
+
list: async () => ({ resources: SCHEMA_KINDS.map((k) => ({
|
|
1247
|
+
uri: `axiom://schema/${k}`,
|
|
1248
|
+
name: k,
|
|
1249
|
+
mimeType: "application/schema+json"
|
|
1250
|
+
})) }),
|
|
1251
|
+
complete: { kind: (v) => SCHEMA_KINDS.filter((k) => k.toLowerCase().startsWith(v.toLowerCase())) }
|
|
1252
|
+
}), {
|
|
1253
|
+
title: "JSON Schema (draft 2020-12)",
|
|
1254
|
+
mimeType: "application/schema+json"
|
|
1255
|
+
}, async (uri, { kind }) => {
|
|
1256
|
+
const k = String(kind);
|
|
1257
|
+
if (!isSchemaKind(k)) notFound(uri.href);
|
|
1258
|
+
return { contents: [{
|
|
1259
|
+
uri: uri.href,
|
|
1260
|
+
mimeType: "application/schema+json",
|
|
1261
|
+
text: JSON.stringify(jsonSchemaFor(k), null, 2)
|
|
1262
|
+
}] };
|
|
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()));
|
|
1268
|
+
log.info("server created", {
|
|
1269
|
+
name: SERVER_NAME,
|
|
1270
|
+
version: SERVER_VERSION,
|
|
1271
|
+
roots: [...policy.roots]
|
|
1272
|
+
});
|
|
1273
|
+
return server;
|
|
1274
|
+
}
|
|
1275
|
+
//#endregion
|
|
1276
|
+
//#region src/spec.ts
|
|
1277
|
+
function buildToolsSpec() {
|
|
1278
|
+
return TOOL_DEFS.map((t) => ({
|
|
1279
|
+
name: t.name,
|
|
1280
|
+
description: t.description,
|
|
1281
|
+
riskClass: t.riskClass,
|
|
1282
|
+
annotations: t.annotations,
|
|
1283
|
+
inputSchema: toolJsonSchema(z.object(t.inputSchema)),
|
|
1284
|
+
outputSchema: toolJsonSchema(t.outputSchema)
|
|
1285
|
+
}));
|
|
1286
|
+
}
|
|
1287
|
+
/** Stable text form (2-space JSON + trailing newline) used both by the generator and the parity test. */
|
|
1288
|
+
function renderToolsSpec() {
|
|
1289
|
+
return `${JSON.stringify(buildToolsSpec(), null, 2)}\n`;
|
|
1290
|
+
}
|
|
1291
|
+
//#endregion
|
|
1292
|
+
export { BUNDLE_BYTES_MAX, LOG_LEVELS, SCHEMA_KINDS, SERVER_NAME, SERVER_VERSION, SUMMARY_LIST_MAX, TOOL_DEFS, buildToolsSpec, createLogger, createRootsPolicy, createServer, isLogLevel, isSameOrInside, isSchemaKind, jsonSchemaFor, renderToolsSpec, resolveRoot, riskClassOf, toStructuredError, toolByName };
|
|
1293
|
+
|
|
1294
|
+
//# sourceMappingURL=index.js.map
|