@aarmos/bridge 0.1.10 → 0.1.11

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.
Files changed (45) hide show
  1. package/dist/bridge.js +7 -129
  2. package/dist/chunk-GMH5B5YX.js +871 -0
  3. package/dist/index.js +1500 -219
  4. package/dist/verify-O6X7MFEE.js +409 -0
  5. package/package.json +4 -3
  6. package/dist/bridge.js.map +0 -1
  7. package/dist/commands/attest.js +0 -62
  8. package/dist/commands/attest.js.map +0 -1
  9. package/dist/commands/demo.js +0 -171
  10. package/dist/commands/demo.js.map +0 -1
  11. package/dist/commands/dev.js +0 -56
  12. package/dist/commands/dev.js.map +0 -1
  13. package/dist/commands/pack.js +0 -96
  14. package/dist/commands/pack.js.map +0 -1
  15. package/dist/commands/playbook.js +0 -55
  16. package/dist/commands/playbook.js.map +0 -1
  17. package/dist/commands/run.js +0 -290
  18. package/dist/commands/run.js.map +0 -1
  19. package/dist/commands/simulate.js +0 -180
  20. package/dist/commands/simulate.js.map +0 -1
  21. package/dist/commands/verify.js +0 -87
  22. package/dist/commands/verify.js.map +0 -1
  23. package/dist/index.js.map +0 -1
  24. package/dist/lib/attest.js +0 -167
  25. package/dist/lib/attest.js.map +0 -1
  26. package/dist/lib/http-broker.js +0 -82
  27. package/dist/lib/http-broker.js.map +0 -1
  28. package/dist/lib/llm-test.js +0 -174
  29. package/dist/lib/llm-test.js.map +0 -1
  30. package/dist/lib/mcp-broker.js +0 -85
  31. package/dist/lib/mcp-broker.js.map +0 -1
  32. package/dist/lib/pack.js +0 -84
  33. package/dist/lib/pack.js.map +0 -1
  34. package/dist/lib/playbook.js +0 -237
  35. package/dist/lib/playbook.js.map +0 -1
  36. package/dist/lib/scope-templates.js +0 -152
  37. package/dist/lib/scope-templates.js.map +0 -1
  38. package/dist/rpc/router.js +0 -349
  39. package/dist/rpc/router.js.map +0 -1
  40. package/dist/session.js +0 -100
  41. package/dist/session.js.map +0 -1
  42. package/dist/watchers/files.js +0 -31
  43. package/dist/watchers/files.js.map +0 -1
  44. package/dist/watchers/git.js +0 -39
  45. package/dist/watchers/git.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,240 +1,1521 @@
1
1
  #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+ import {
4
+ createSession,
5
+ handleRpc,
6
+ startBridge
7
+ } from "./chunk-GMH5B5YX.js";
8
+
9
+ // src/index.ts
2
10
  import { Command } from "commander";
3
- import { startDev } from "./commands/dev.js";
4
- import { runDemo } from "./commands/demo.js";
5
- import { startRun } from "./commands/run.js";
6
- import { runSimulate } from "./commands/simulate.js";
7
- import { playbookKeygen, playbookSign, playbookVerify } from "./commands/playbook.js";
8
- import { packInstall, packList, packVerify } from "./commands/pack.js";
9
- import { runAttest, runAttestVerify } from "./commands/attest.js";
10
- const program = new Command();
11
- program
12
- .name("aarmos")
13
- .description("Aarmos CLI bridge — native host for the Aarmos PWA")
14
- .version("0.0.0-dev");
15
- // Repeatable-option collector. Commander calls the coerce function once per
16
- // occurrence with the previous accumulator as the second arg.
17
- function collect(value, prev) {
18
- return [...prev, value];
19
- }
20
- program
21
- .command("dev")
22
- .description("Start the local bridge and pair with the PWA")
23
- .option("-p, --port <port>", "WebSocket port to bind on 127.0.0.1", "7423")
24
- .option("--pwa <url>", "PWA base URL to open for pairing", process.env.AARMOS_PWA_URL ?? "https://www.aarmos.io")
25
- .option("--no-open", "Do not auto-open the PWA in the browser")
26
- .option("--allow-shell <bins>", "Comma-separated additional shell binaries to allow (in addition to defaults)", "")
27
- .option("--allow-sql <alias=dsn>", "Register a read-only Postgres target. Repeatable. Example: --allow-sql prod=postgres://readonly@…/db", collect, [])
28
- .option("--allow-read <path>", "Allow fs.scopedRead / fs.scopedList inside this absolute path (outside the workspace). Repeatable.", collect, [])
29
- .action(async (opts) => {
30
- const sqlAllow = opts.allowSql.map((raw) => {
31
- const eq = raw.indexOf("=");
32
- if (eq < 0) {
33
- throw new Error(`--allow-sql expects <alias>=<dsn>, got: ${raw}`);
11
+
12
+ // src/commands/dev.ts
13
+ import crypto from "crypto";
14
+ import path from "path";
15
+ import open from "open";
16
+ async function startDev(opts) {
17
+ const workspace = path.resolve(process.cwd());
18
+ const pairingToken = crypto.randomBytes(24).toString("base64url");
19
+ const { port, url: wsUrl } = await startBridge({
20
+ port: opts.port,
21
+ workspace,
22
+ pairingToken,
23
+ extraShellAllow: opts.extraShellAllow,
24
+ sqlAllow: opts.sqlAllow,
25
+ readAllow: opts.readAllow,
26
+ allowedOrigins: buildAllowedOrigins(opts.pwaUrl)
27
+ });
28
+ const pairUrl = new URL(opts.pwaUrl);
29
+ pairUrl.searchParams.set("bridge", "1");
30
+ pairUrl.searchParams.set("port", String(port));
31
+ pairUrl.searchParams.set("token", pairingToken);
32
+ console.log(`
33
+ aarmos bridge ready`);
34
+ console.log(` workspace : ${workspace}`);
35
+ console.log(` ws : ${wsUrl}`);
36
+ if (opts.sqlAllow.length > 0) {
37
+ console.log(` sql : ${opts.sqlAllow.map((s) => s.alias).join(", ")} (read-only)`);
38
+ }
39
+ if (opts.readAllow.length > 0) {
40
+ console.log(` read : ${opts.readAllow.join(", ")}`);
41
+ }
42
+ console.log(` pair url : ${pairUrl.toString()}
43
+ `);
44
+ if (opts.autoOpen) {
45
+ try {
46
+ await open(pairUrl.toString());
47
+ } catch {
48
+ console.warn("[aarmos] could not auto-open browser; paste the pair url above");
49
+ }
50
+ }
51
+ }
52
+ function buildAllowedOrigins(pwaUrl) {
53
+ const url = new URL(pwaUrl);
54
+ const set = /* @__PURE__ */ new Set([url.origin]);
55
+ set.add("https://www.aarmos.io");
56
+ set.add("http://localhost:8080");
57
+ set.add("http://127.0.0.1:8080");
58
+ return [...set];
59
+ }
60
+
61
+ // src/commands/demo.ts
62
+ import path2 from "path";
63
+ import fs from "fs/promises";
64
+ async function runDemo(opts) {
65
+ const workspace = path2.resolve(opts.workspace ?? process.cwd());
66
+ const session = createSession({ workspace, extraShellAllow: [] });
67
+ log("");
68
+ log(` aarmos demo`);
69
+ log(` workspace : ${workspace}`);
70
+ log(
71
+ ` mode : ${opts.apply ? "PREVIEW + APPLY" : "PREVIEW ONLY (pass --apply to commit)"}`
72
+ );
73
+ log("");
74
+ await ensureSampleFiles(workspace);
75
+ const todoPath = "notes/todo.md";
76
+ const existingTodo = await safeRead(path2.join(workspace, todoPath));
77
+ const newTodo = (existingTodo ?? "# Todo\n\n") + `- [ ] Added by \`aarmos demo\` at ${(/* @__PURE__ */ new Date()).toISOString()}
78
+ `;
79
+ const proposals = [
80
+ {
81
+ label: "Append a checklist item to notes/todo.md",
82
+ method: "fs.write",
83
+ params: { path: todoPath, data: newTodo, encoding: "utf8" }
84
+ },
85
+ {
86
+ label: "Inspect the workspace with `git status`",
87
+ method: "shell.exec",
88
+ params: { bin: "git", args: ["status", "--short"], cwd: "." }
89
+ },
90
+ {
91
+ label: "Say hello via `node src/hello.js aarmos`",
92
+ method: "shell.exec",
93
+ params: { bin: "node", args: ["src/hello.js", "aarmos"], cwd: "." }
94
+ }
95
+ ];
96
+ log(" proposed batch (dry-run previews):");
97
+ log("");
98
+ const collected = [];
99
+ for (let i = 0; i < proposals.length; i++) {
100
+ const p = proposals[i];
101
+ const res = await call(session, p.method, p.params);
102
+ log(` [${i + 1}] ${p.label}`);
103
+ if (isPreview(res)) {
104
+ renderPreview(res.action);
105
+ collected.push({ kind: res.action.kind, params: res.action.params });
106
+ } else {
107
+ log(` (no preview) ${JSON.stringify(res)}`);
108
+ }
109
+ log("");
110
+ }
111
+ if (!opts.apply) {
112
+ log(" nothing was written. rerun with --apply to commit the batch.");
113
+ log("");
114
+ return;
115
+ }
116
+ log(" applying batch via apply.batch \u2026");
117
+ const applyRes = await call(session, "apply.batch", { actions: collected });
118
+ log(` applied ${applyRes.applied}/${collected.length} action(s):`);
119
+ applyRes.results.forEach((r, i) => {
120
+ if (r.ok) {
121
+ log(` \u2713 [${i + 1}] ${r.kind}`);
122
+ if (r.kind === "shell.exec" && r.result && typeof r.result === "object") {
123
+ const sh = r.result;
124
+ if (sh.stdout?.trim()) indented(sh.stdout.trim());
125
+ if (sh.stderr?.trim()) indented(sh.stderr.trim());
126
+ }
127
+ } else {
128
+ log(` \u2717 [${i + 1}] ${r.kind}: ${r.error}`);
129
+ }
130
+ });
131
+ log("");
132
+ }
133
+ function isPreview(x) {
134
+ return !!x && typeof x === "object" && x.preview === true;
135
+ }
136
+ async function call(session, method, params) {
137
+ const res = await handleRpc(session, { id: 1, type: "rpc", method, params });
138
+ if ("type" in res && res.type === "error") throw new Error(res.error);
139
+ if ("type" in res && res.type === "result") return res.result;
140
+ throw new Error("unexpected rpc envelope");
141
+ }
142
+ function renderPreview(action) {
143
+ const pv = action.preview;
144
+ if (action.kind === "fs.write") {
145
+ const before = String(pv.before ?? "");
146
+ const after = String(pv.after ?? "");
147
+ log(` path : ${pv.path}`);
148
+ log(` existed : ${pv.existed} bytes: ${pv.bytesBefore} \u2192 ${pv.bytesAfter}`);
149
+ log(` diff :`);
150
+ for (const line of unifiedDiff(before, after)) log(` ${line}`);
151
+ } else {
152
+ log(` command : ${pv.commandLine}`);
153
+ log(` cwd : ${pv.cwd}`);
154
+ }
155
+ }
156
+ function unifiedDiff(before, after) {
157
+ const a = before.split("\n");
158
+ const b = after.split("\n");
159
+ const out = [];
160
+ const max = Math.max(a.length, b.length);
161
+ for (let i = 0; i < max; i++) {
162
+ if (a[i] === b[i]) {
163
+ if (a[i] !== void 0) out.push(` ${a[i]}`);
164
+ } else {
165
+ if (a[i] !== void 0) out.push(`- ${a[i]}`);
166
+ if (b[i] !== void 0) out.push(`+ ${b[i]}`);
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+ async function safeRead(abs) {
172
+ try {
173
+ return await fs.readFile(abs, "utf8");
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+ async function ensureSampleFiles(workspace) {
179
+ const todo = path2.join(workspace, "notes/todo.md");
180
+ try {
181
+ await fs.access(todo);
182
+ } catch {
183
+ await fs.mkdir(path2.dirname(todo), { recursive: true });
184
+ await fs.writeFile(todo, "# Todo\n\n- [ ] Try the aarmos dry-run demo\n");
185
+ }
186
+ const hello = path2.join(workspace, "src/hello.js");
187
+ try {
188
+ await fs.access(hello);
189
+ } catch {
190
+ await fs.mkdir(path2.dirname(hello), { recursive: true });
191
+ await fs.writeFile(
192
+ hello,
193
+ "const name = process.argv[2] || 'world';\nconsole.log(`hello, ${name} \u2014 from the aarmos sample workspace`);\n"
194
+ );
195
+ }
196
+ }
197
+ function log(s) {
198
+ console.log(s);
199
+ }
200
+ function indented(s) {
201
+ for (const line of s.split("\n")) log(` \u2502 ${line}`);
202
+ }
203
+
204
+ // src/commands/run.ts
205
+ import path6 from "path";
206
+ import fs4 from "fs/promises";
207
+ import fssync from "fs";
208
+ import crypto4 from "crypto";
209
+
210
+ // src/lib/playbook.ts
211
+ import fs2 from "fs/promises";
212
+ import path3 from "path";
213
+ import crypto2 from "crypto";
214
+ async function loadPlaybook(p) {
215
+ const raw = await fs2.readFile(path3.resolve(p), "utf8");
216
+ let parsed;
217
+ try {
218
+ parsed = JSON.parse(raw);
219
+ } catch (err) {
220
+ throw new Error(`playbook is not valid JSON: ${err.message}`);
221
+ }
222
+ if (!parsed || typeof parsed !== "object") throw new Error("playbook must be an object");
223
+ const pb = parsed;
224
+ if (!pb.name || typeof pb.name !== "string") throw new Error("playbook.name is required");
225
+ if (!Array.isArray(pb.steps) || pb.steps.length === 0) {
226
+ throw new Error("playbook.steps must be a non-empty array");
227
+ }
228
+ const seen = /* @__PURE__ */ new Set();
229
+ for (const s of pb.steps) {
230
+ if (!s.id || typeof s.id !== "string") throw new Error("each step needs an id");
231
+ if (seen.has(s.id)) throw new Error(`duplicate step id: ${s.id}`);
232
+ seen.add(s.id);
233
+ if (s.kind !== "shell.exec" && s.kind !== "fs.write" && s.kind !== "http.call" && s.kind !== "mcp.tool") {
234
+ throw new Error(
235
+ `step ${s.id}: kind must be one of shell.exec, fs.write, http.call, mcp.tool`
236
+ );
237
+ }
238
+ if (!s.params || typeof s.params !== "object") {
239
+ throw new Error(`step ${s.id}: params object required`);
240
+ }
241
+ }
242
+ return pb;
243
+ }
244
+ function lintPlaybook(pb) {
245
+ const out = [];
246
+ if (pb.steps.length > 100) {
247
+ out.push({
248
+ level: "warn",
249
+ message: `playbook has ${pb.steps.length} steps \u2014 consider splitting`
250
+ });
251
+ }
252
+ for (const s of pb.steps) {
253
+ if (!s.label)
254
+ out.push({ level: "warn", stepId: s.id, message: "missing human-readable label" });
255
+ if (s.intervalSec !== void 0 && (typeof s.intervalSec !== "number" || s.intervalSec < 1)) {
256
+ out.push({ level: "error", stepId: s.id, message: "intervalSec must be a positive number" });
257
+ }
258
+ if (s.kind === "shell.exec") {
259
+ const p = s.params;
260
+ if (typeof p.bin !== "string" || !p.bin) {
261
+ out.push({
262
+ level: "error",
263
+ stepId: s.id,
264
+ message: "shell.exec: params.bin (string) required"
265
+ });
266
+ }
267
+ if (p.args !== void 0 && !Array.isArray(p.args)) {
268
+ out.push({
269
+ level: "error",
270
+ stepId: s.id,
271
+ message: "shell.exec: params.args must be an array"
272
+ });
273
+ }
274
+ if (s.autoApply) {
275
+ out.push({
276
+ level: "info",
277
+ stepId: s.id,
278
+ message: "shell.exec has autoApply=true \u2014 will execute unattended if bin is in allowlist"
279
+ });
280
+ }
281
+ }
282
+ if (s.kind === "fs.write") {
283
+ const p = s.params;
284
+ if (typeof p.path !== "string" || !p.path) {
285
+ out.push({
286
+ level: "error",
287
+ stepId: s.id,
288
+ message: "fs.write: params.path (string) required"
289
+ });
290
+ }
291
+ if (p.data !== void 0 && typeof p.data !== "string") {
292
+ out.push({
293
+ level: "error",
294
+ stepId: s.id,
295
+ message: "fs.write: params.data must be a string"
296
+ });
297
+ }
298
+ }
299
+ if (s.kind === "http.call") {
300
+ const p = s.params;
301
+ if (typeof p.url !== "string" || !/^https?:\/\//.test(p.url)) {
302
+ out.push({
303
+ level: "error",
304
+ stepId: s.id,
305
+ message: "http.call: params.url (http(s) URL) required"
306
+ });
307
+ }
308
+ if (p.method !== void 0 && typeof p.method !== "string") {
309
+ out.push({
310
+ level: "error",
311
+ stepId: s.id,
312
+ message: "http.call: params.method must be a string"
313
+ });
314
+ }
315
+ }
316
+ if (s.kind === "mcp.tool") {
317
+ const p = s.params;
318
+ if (typeof p.endpoint !== "string" || !/^https?:\/\//.test(p.endpoint)) {
319
+ out.push({
320
+ level: "error",
321
+ stepId: s.id,
322
+ message: "mcp.tool: params.endpoint (http(s) URL) required"
323
+ });
324
+ }
325
+ if (typeof p.tool !== "string" || !p.tool) {
326
+ out.push({
327
+ level: "error",
328
+ stepId: s.id,
329
+ message: "mcp.tool: params.tool (string) required"
330
+ });
331
+ }
332
+ }
333
+ }
334
+ return out;
335
+ }
336
+ function canonicalize(v) {
337
+ if (v === void 0) return "null";
338
+ if (v === null || typeof v !== "object") return JSON.stringify(v);
339
+ if (Array.isArray(v)) return "[" + v.map((x) => canonicalize(x)).join(",") + "]";
340
+ const obj = v;
341
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
342
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
343
+ }
344
+ function playbookBody(pb) {
345
+ const { signature: _s, ...rest } = pb;
346
+ void _s;
347
+ return canonicalize(rest);
348
+ }
349
+ function b64u(buf) {
350
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
351
+ }
352
+ function b64uDecode(s) {
353
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
354
+ return Buffer.from(b64, "base64");
355
+ }
356
+ async function generateSigningKeyPair(outDir, name) {
357
+ const { publicKey, privateKey } = crypto2.generateKeyPairSync("ed25519");
358
+ const pem = privateKey.export({ format: "pem", type: "pkcs8" }).toString();
359
+ const raw = publicKey.export({ format: "der", type: "spki" });
360
+ const rawKey = raw.subarray(raw.length - 32);
361
+ const pubB64u = b64u(rawKey);
362
+ await fs2.mkdir(outDir, { recursive: true });
363
+ const privPath = path3.join(outDir, `${name}.key.pem`);
364
+ const pubPath = path3.join(outDir, `${name}.pub`);
365
+ await fs2.writeFile(privPath, pem, { mode: 384 });
366
+ await fs2.writeFile(pubPath, pubB64u + "\n");
367
+ return { privatePem: privPath, publicKey: pubB64u };
368
+ }
369
+ async function signPlaybook(pb, privateKeyPem) {
370
+ const key = crypto2.createPrivateKey(privateKeyPem);
371
+ const bytes = Buffer.from(playbookBody(pb));
372
+ const sig = crypto2.sign(null, bytes, key);
373
+ const pubDer = crypto2.createPublicKey(key).export({ format: "der", type: "spki" });
374
+ const rawPub = pubDer.subarray(pubDer.length - 32);
375
+ return {
376
+ ...pb,
377
+ signature: { alg: "ed25519", pubKey: b64u(rawPub), sig: b64u(sig) }
378
+ };
379
+ }
380
+ function verifyPlaybookSignature(pb) {
381
+ if (!pb.signature) return { ok: false, reason: "playbook is not signed" };
382
+ if (pb.signature.alg !== "ed25519") {
383
+ return { ok: false, reason: `unsupported alg: ${pb.signature.alg}` };
384
+ }
385
+ try {
386
+ const spkiPrefix = Buffer.from([
387
+ 48,
388
+ 42,
389
+ 48,
390
+ 5,
391
+ 6,
392
+ 3,
393
+ 43,
394
+ 101,
395
+ 112,
396
+ 3,
397
+ 33,
398
+ 0
399
+ ]);
400
+ const rawPub = b64uDecode(pb.signature.pubKey);
401
+ if (rawPub.length !== 32) return { ok: false, reason: "pubKey must be 32 bytes" };
402
+ const der = Buffer.concat([spkiPrefix, rawPub]);
403
+ const key = crypto2.createPublicKey({ key: der, format: "der", type: "spki" });
404
+ const bytes = Buffer.from(playbookBody(pb));
405
+ const sig = b64uDecode(pb.signature.sig);
406
+ const ok = crypto2.verify(null, bytes, key, sig);
407
+ return ok ? { ok: true } : { ok: false, reason: "signature does not match playbook body" };
408
+ } catch (err) {
409
+ return { ok: false, reason: err.message };
410
+ }
411
+ }
412
+ function hashPlaybook(pb) {
413
+ return crypto2.createHash("sha256").update(playbookBody(pb)).digest("hex");
414
+ }
415
+ async function writePlaybook(p, pb) {
416
+ await fs2.writeFile(path3.resolve(p), JSON.stringify(pb, null, 2) + "\n", "utf8");
417
+ }
418
+
419
+ // src/lib/scope-templates.ts
420
+ var KUBECTL_READ = /* @__PURE__ */ new Set([
421
+ "get",
422
+ "describe",
423
+ "logs",
424
+ "top",
425
+ "version",
426
+ "api-resources",
427
+ "api-versions",
428
+ "explain",
429
+ "config",
430
+ "cluster-info",
431
+ "auth"
432
+ ]);
433
+ var KUBECTL_WRITE = /* @__PURE__ */ new Set([
434
+ "apply",
435
+ "create",
436
+ "scale",
437
+ "patch",
438
+ "rollout",
439
+ "edit",
440
+ "label",
441
+ "annotate",
442
+ "set",
443
+ "expose",
444
+ "run",
445
+ "autoscale",
446
+ "replace",
447
+ "taint"
448
+ ]);
449
+ var KUBECTL_DESTRUCTIVE = /* @__PURE__ */ new Set(["delete", "drain", "cordon", "uncordon", "evict", "exec"]);
450
+ var GIT_DESTRUCTIVE_FLAGS = ["--force", "-f", "--hard", "-D"];
451
+ var GIT_DESTRUCTIVE_SUBCMDS = /* @__PURE__ */ new Set(["push", "reset", "clean", "branch", "rebase"]);
452
+ var GH_READ = /* @__PURE__ */ new Set(["view", "list", "status", "search", "browse", "auth"]);
453
+ var GH_DESTRUCTIVE = /* @__PURE__ */ new Set(["delete", "close"]);
454
+ function httpRisk(method) {
455
+ const m = method.toUpperCase();
456
+ if (m === "GET" || m === "HEAD" || m === "OPTIONS") return "read";
457
+ if (m === "DELETE") return "destructive";
458
+ return "write";
459
+ }
460
+ function classifyStep(step) {
461
+ const p = step.params;
462
+ const explicit = step.risk;
463
+ if (explicit) return { rank: explicit, reason: "explicit step.risk override" };
464
+ if (step.kind === "fs.write") {
465
+ return { rank: "write", reason: "fs.write mutates workspace" };
466
+ }
467
+ if (step.kind === "shell.exec") {
468
+ const bin = String(p.bin ?? "");
469
+ const args = Array.isArray(p.args) ? p.args : [];
470
+ if (bin === "kubectl") {
471
+ const sub = args.find((a) => !a.startsWith("-")) ?? "";
472
+ if (KUBECTL_DESTRUCTIVE.has(sub)) {
473
+ return { rank: "destructive", reason: `kubectl ${sub}`, template: "kubectl" };
474
+ }
475
+ if (KUBECTL_WRITE.has(sub)) {
476
+ return { rank: "write", reason: `kubectl ${sub}`, template: "kubectl" };
477
+ }
478
+ if (KUBECTL_READ.has(sub)) {
479
+ return { rank: "read", reason: `kubectl ${sub}`, template: "kubectl" };
480
+ }
481
+ return {
482
+ rank: "write",
483
+ reason: `kubectl ${sub || "(unknown)"} \u2014 defaulted`,
484
+ template: "kubectl"
485
+ };
486
+ }
487
+ if (bin === "git") {
488
+ const sub = args.find((a) => !a.startsWith("-")) ?? "";
489
+ const hasForce = args.some((a) => GIT_DESTRUCTIVE_FLAGS.includes(a));
490
+ if (GIT_DESTRUCTIVE_SUBCMDS.has(sub) && hasForce) {
491
+ return { rank: "destructive", reason: `git ${sub} with force flag`, template: "git" };
492
+ }
493
+ if (sub === "push") return { rank: "write", reason: "git push", template: "git" };
494
+ if (sub === "commit" || sub === "add" || sub === "checkout") {
495
+ return { rank: "write", reason: `git ${sub}`, template: "git" };
496
+ }
497
+ return { rank: "read", reason: `git ${sub || "status"}`, template: "git" };
498
+ }
499
+ if (bin === "gh") {
500
+ const sub = args.find((a) => !a.startsWith("-")) ?? "";
501
+ if (GH_DESTRUCTIVE.has(sub)) {
502
+ return { rank: "destructive", reason: `gh ${sub}`, template: "gh" };
503
+ }
504
+ if (GH_READ.has(sub)) return { rank: "read", reason: `gh ${sub}`, template: "gh" };
505
+ return { rank: "write", reason: `gh ${sub || "(unknown)"}`, template: "gh" };
506
+ }
507
+ return { rank: "write", reason: `shell.exec ${bin} \u2014 defaulted` };
508
+ }
509
+ if (step.kind === "http.call") {
510
+ const method = String(p.method ?? "GET");
511
+ const url = String(p.url ?? "");
512
+ if (/\/rest\/api\/\d+\/issue\/[^/]+$/.test(url) && method.toUpperCase() === "DELETE") {
513
+ return { rank: "destructive", reason: "Jira DELETE /issue/{id}", template: "jira" };
514
+ }
515
+ return { rank: httpRisk(method), reason: `HTTP ${method.toUpperCase()}` };
516
+ }
517
+ if (step.kind === "mcp.tool") {
518
+ const hint = String(p.riskHint ?? "");
519
+ if (hint === "read" || hint === "write" || hint === "destructive") {
520
+ return { rank: hint, reason: `mcp.tool riskHint=${hint}` };
521
+ }
522
+ return { rank: "write", reason: "mcp.tool defaulted to write" };
523
+ }
524
+ return { rank: "write", reason: `unknown kind ${String(step.kind)} \u2014 defaulted` };
525
+ }
526
+ function resolveEffectiveApproval(step, cls, opts) {
527
+ const requested = Boolean(step.autoApply);
528
+ if (cls.rank !== "destructive") {
529
+ return { autoApply: requested, forcedApproval: false };
530
+ }
531
+ const ack = Boolean(step.acknowledgeDestructive);
532
+ if (ack && opts.playbookSigned) {
533
+ return { autoApply: requested, forcedApproval: false };
534
+ }
535
+ return {
536
+ autoApply: false,
537
+ forcedApproval: true,
538
+ reason: ack ? "destructive + acknowledgeDestructive requires a signed playbook \u2014 forcing approval" : "destructive step \u2014 forcing approval (set acknowledgeDestructive on a signed playbook to auto-apply)"
539
+ };
540
+ }
541
+
542
+ // src/lib/pack.ts
543
+ import fs3 from "fs/promises";
544
+ import path4 from "path";
545
+ import crypto3 from "crypto";
546
+ function packBody(pack2) {
547
+ const { signature: _s, ...rest } = pack2;
548
+ void _s;
549
+ return canonicalize(rest);
550
+ }
551
+ function hashPack(pack2) {
552
+ return crypto3.createHash("sha256").update(packBody(pack2)).digest("hex");
553
+ }
554
+ async function loadPack(p) {
555
+ const raw = await fs3.readFile(path4.resolve(p), "utf8");
556
+ const parsed = JSON.parse(raw);
557
+ if (!parsed?.name || typeof parsed.name !== "string") throw new Error("pack.name required");
558
+ if (!parsed.version || typeof parsed.version !== "string")
559
+ throw new Error("pack.version required");
560
+ if (!Array.isArray(parsed.playbooks) || parsed.playbooks.length === 0) {
561
+ throw new Error("pack.playbooks must be a non-empty array");
562
+ }
563
+ return parsed;
564
+ }
565
+ var SAFE_NAME_RE = /[^a-zA-Z0-9._-]/g;
566
+ function packInstallDir(pack2, rootDir) {
567
+ const safe2 = pack2.name.replace(SAFE_NAME_RE, "_");
568
+ return path4.join(rootDir, `${safe2}-${pack2.version}`);
569
+ }
570
+ async function installPack(source, rootDir) {
571
+ const pack2 = await loadPack(source);
572
+ const dir = packInstallDir(pack2, rootDir);
573
+ await fs3.mkdir(dir, { recursive: true });
574
+ const dest = path4.join(dir, "pack.json");
575
+ await fs3.writeFile(dest, JSON.stringify(pack2, null, 2) + "\n", "utf8");
576
+ return { pack: pack2, installed: dest };
577
+ }
578
+ async function listInstalledPacks(rootDir) {
579
+ const out = [];
580
+ try {
581
+ const entries = await fs3.readdir(rootDir, { withFileTypes: true });
582
+ for (const e of entries) {
583
+ if (!e.isDirectory()) continue;
584
+ const file = path4.join(rootDir, e.name, "pack.json");
585
+ try {
586
+ const raw = await fs3.readFile(file, "utf8");
587
+ out.push(JSON.parse(raw));
588
+ } catch {
589
+ }
590
+ }
591
+ } catch {
592
+ }
593
+ return out;
594
+ }
595
+
596
+ // src/commands/simulate.ts
597
+ import path5 from "path";
598
+ async function runSimulate(opts) {
599
+ const workspace = path5.resolve(opts.workspace ?? process.cwd());
600
+ const pb = await loadPlaybook(opts.playbook);
601
+ const packAllow = await collectPackAllow(workspace);
602
+ const session = createSession({
603
+ workspace,
604
+ extraShellAllow: [...opts.extraShellAllow, ...packAllow.shell],
605
+ httpAllow: [...opts.httpAllow ?? [], ...packAllow.http],
606
+ mcpAllow: [...opts.mcpAllow ?? [], ...packAllow.mcp]
607
+ });
608
+ const sigCheck = pb.signature ? verifyPlaybookSignature(pb) : { ok: false, reason: "unsigned" };
609
+ const lint = lintPlaybook(pb);
610
+ const steps = [];
611
+ for (const step of pb.steps) {
612
+ steps.push(await simulateStep(session, pb, step));
613
+ }
614
+ const summary = {
615
+ total: steps.length,
616
+ wouldApply: steps.filter((s) => s.verdict === "would-apply").length,
617
+ needsApproval: steps.filter((s) => s.verdict === "needs-approval").length,
618
+ wouldFail: steps.filter((s) => s.verdict === "would-fail" || s.verdict === "unsupported").length,
619
+ destructive: steps.filter((s) => s.risk === "destructive").length
620
+ };
621
+ const report = {
622
+ playbook: opts.playbook,
623
+ signature: {
624
+ present: Boolean(pb.signature),
625
+ ok: pb.signature ? sigCheck.ok : void 0,
626
+ reason: pb.signature ? sigCheck.reason : void 0,
627
+ pubKey: pb.signature?.pubKey
628
+ },
629
+ lint,
630
+ steps,
631
+ summary
632
+ };
633
+ if (opts.json) {
634
+ console.log(JSON.stringify(report, null, 2));
635
+ } else {
636
+ printHuman(report);
637
+ }
638
+ if (opts.strict && summary.wouldFail > 0) {
639
+ process.exitCode = 2;
640
+ }
641
+ return report;
642
+ }
643
+ async function simulateStep(session, pb, step) {
644
+ const autoApply = Boolean(step.autoApply);
645
+ const cls = classifyStep(step);
646
+ const approval = resolveEffectiveApproval(step, cls, {
647
+ playbookSigned: Boolean(pb.signature)
648
+ });
649
+ try {
650
+ const preview = await callRpc(session, step.kind, step.params);
651
+ const summary = summarizePreview(preview);
652
+ if (approval.forcedApproval || !approval.autoApply) {
653
+ return {
654
+ id: step.id,
655
+ label: step.label,
656
+ kind: step.kind,
657
+ autoApply,
658
+ risk: cls.rank,
659
+ riskReason: cls.reason,
660
+ verdict: "needs-approval",
661
+ reason: approval.reason ?? (autoApply ? "risk-gated" : "autoApply not set \u2014 human approval required in the PWA"),
662
+ previewSummary: summary
663
+ };
664
+ }
665
+ return {
666
+ id: step.id,
667
+ label: step.label,
668
+ kind: step.kind,
669
+ autoApply,
670
+ risk: cls.rank,
671
+ riskReason: cls.reason,
672
+ verdict: "would-apply",
673
+ previewSummary: summary
674
+ };
675
+ } catch (err) {
676
+ const msg = err.message;
677
+ return {
678
+ id: step.id,
679
+ label: step.label,
680
+ kind: step.kind,
681
+ autoApply,
682
+ risk: cls.rank,
683
+ riskReason: cls.reason,
684
+ verdict: "would-fail",
685
+ reason: msg
686
+ };
687
+ }
688
+ }
689
+ async function collectPackAllow(workspace) {
690
+ const packs = await listInstalledPacks(path5.join(workspace, ".aarmos/packs"));
691
+ const shell = /* @__PURE__ */ new Set();
692
+ const http = /* @__PURE__ */ new Set();
693
+ const mcp = /* @__PURE__ */ new Set();
694
+ for (const p of packs) {
695
+ for (const b of p.allow?.shell ?? []) shell.add(b);
696
+ for (const h of p.allow?.http ?? []) http.add(h);
697
+ for (const m of p.allow?.mcp ?? []) mcp.add(m);
698
+ }
699
+ return { shell: [...shell], http: [...http], mcp: [...mcp] };
700
+ }
701
+ function summarizePreview(preview) {
702
+ if (!preview || typeof preview !== "object") return String(preview);
703
+ const p = preview;
704
+ const body = p.action?.preview ?? p.preview ?? preview;
705
+ const s = JSON.stringify(body);
706
+ return s.length > 200 ? s.slice(0, 200) + "\u2026" : s;
707
+ }
708
+ async function callRpc(session, method, params) {
709
+ const res = await handleRpc(session, { id: 1, type: "rpc", method, params });
710
+ if ("type" in res && res.type === "error") throw new Error(res.error);
711
+ if ("type" in res && res.type === "result") return res.result;
712
+ throw new Error("unexpected rpc envelope");
713
+ }
714
+ function printHuman(r) {
715
+ const log6 = (s) => console.log(s);
716
+ log6("");
717
+ log6(` aarmos simulate \u2014 ${r.playbook}`);
718
+ log6("");
719
+ if (r.signature.present) {
720
+ log6(
721
+ ` signature : ${r.signature.ok ? "OK" : "INVALID"} (${r.signature.pubKey?.slice(0, 16)}\u2026)${r.signature.reason ? " " + r.signature.reason : ""}`
722
+ );
723
+ } else {
724
+ log6(" signature : (unsigned \u2014 allowed in dev; blocked under --enforce-signatures)");
725
+ }
726
+ if (r.lint.length > 0) {
727
+ log6("");
728
+ log6(" lint:");
729
+ for (const f of r.lint) {
730
+ log6(` [${f.level}] ${f.stepId ? `${f.stepId}: ` : ""}${f.message}`);
731
+ }
732
+ }
733
+ log6("");
734
+ log6(" steps:");
735
+ for (const s of r.steps) {
736
+ const tag = s.verdict === "would-apply" ? "would-apply " : s.verdict === "needs-approval" ? "needs-approval " : "would-fail ";
737
+ log6(` [${tag}] ${s.id} ${s.label} (risk: ${s.risk} \u2014 ${s.riskReason})`);
738
+ if (s.reason) log6(` reason: ${s.reason}`);
739
+ if (s.previewSummary) log6(` preview: ${s.previewSummary}`);
740
+ }
741
+ log6("");
742
+ log6(
743
+ ` summary : ${r.summary.wouldApply} would-apply \xB7 ${r.summary.needsApproval} needs-approval \xB7 ${r.summary.wouldFail} would-fail \xB7 ${r.summary.destructive} destructive`
744
+ );
745
+ log6("");
746
+ }
747
+
748
+ // src/commands/run.ts
749
+ async function startRun(opts) {
750
+ const workspace = path6.resolve(opts.workspace ?? process.cwd());
751
+ const playbook2 = await loadPlaybook(opts.playbook);
752
+ if (opts.enforceSignatures) {
753
+ if (!playbook2.signature) {
754
+ throw new Error(
755
+ "--enforce-signatures: playbook is unsigned. Run `aarmos playbook sign` first."
756
+ );
757
+ }
758
+ const v = verifyPlaybookSignature(playbook2);
759
+ if (!v.ok) throw new Error(`--enforce-signatures: ${v.reason}`);
760
+ if (opts.trustedPubKeys && opts.trustedPubKeys.length > 0) {
761
+ if (!opts.trustedPubKeys.includes(playbook2.signature.pubKey)) {
762
+ throw new Error(
763
+ `--enforce-signatures: signer ${playbook2.signature.pubKey} not in --trusted-pubkey set`
764
+ );
765
+ }
766
+ }
767
+ }
768
+ if (opts.dry) {
769
+ await runSimulate({
770
+ playbook: opts.playbook,
771
+ workspace: opts.workspace,
772
+ extraShellAllow: opts.extraShellAllow,
773
+ httpAllow: opts.httpAllow,
774
+ mcpAllow: opts.mcpAllow,
775
+ strict: true
776
+ });
777
+ return;
778
+ }
779
+ const packAllow = await collectPackAllow2(workspace);
780
+ const session = createSession({
781
+ workspace,
782
+ extraShellAllow: [...opts.extraShellAllow, ...packAllow.shell],
783
+ httpAllow: [...opts.httpAllow ?? [], ...packAllow.http],
784
+ mcpAllow: [...opts.mcpAllow ?? [], ...packAllow.mcp]
785
+ });
786
+ const receiptsPath = path6.resolve(opts.receipts);
787
+ await fs4.mkdir(path6.dirname(receiptsPath), { recursive: true });
788
+ log2("");
789
+ log2(" aarmos run (headless)");
790
+ log2(` workspace : ${workspace}`);
791
+ log2(` playbook : ${opts.playbook} (${playbook2.name}, ${playbook2.steps.length} step[s])`);
792
+ log2(` receipts : ${receiptsPath}`);
793
+ log2(` kill-switch : ${opts.killSwitch ?? "(none)"}`);
794
+ log2(` ceiling : ${opts.maxPerHour}/hour`);
795
+ log2(` heartbeat : ${opts.heartbeatUrl ?? "(none)"}`);
796
+ log2(` mode : ${opts.once ? "single pass" : "loop"}`);
797
+ log2("");
798
+ const nextRunAt = /* @__PURE__ */ new Map();
799
+ const now = Date.now();
800
+ for (const step of playbook2.steps) {
801
+ nextRunAt.set(step.id, now);
802
+ }
803
+ const applyTimestamps = [];
804
+ let stopped = false;
805
+ const stop = (why) => {
806
+ if (stopped) return;
807
+ stopped = true;
808
+ log2(`
809
+ [stop] ${why}`);
810
+ };
811
+ process.on("SIGINT", () => stop("SIGINT"));
812
+ process.on("SIGTERM", () => stop("SIGTERM"));
813
+ const heartbeatTimer = opts.heartbeatUrl ? setInterval(() => {
814
+ void postHeartbeat(opts, {
815
+ playbook: playbook2.name,
816
+ runsLastHour: applyTimestamps.length,
817
+ killed: killSwitchTripped(opts.killSwitch)
818
+ });
819
+ }, 3e4) : null;
820
+ try {
821
+ do {
822
+ if (killSwitchTripped(opts.killSwitch)) {
823
+ for (const step of playbook2.steps) {
824
+ const r = makeReceipt(playbook2, step, { mode: "skipped-killed" });
825
+ await appendReceipt(receiptsPath, r);
826
+ await postReceipt(opts, r);
827
+ const short = (r.hash ?? "").slice(0, 8) || "--------";
828
+ log2(`avar+ ${short} skipped-killed ${step.label}`);
34
829
  }
35
- const alias = raw.slice(0, eq).trim();
36
- const dsn = raw.slice(eq + 1).trim();
37
- if (!alias || !dsn) {
38
- throw new Error(`--allow-sql alias and dsn must be non-empty: ${raw}`);
830
+ stop(`kill-switch present at ${opts.killSwitch}`);
831
+ break;
832
+ }
833
+ const tickAt = Date.now();
834
+ pruneWindow(applyTimestamps, tickAt);
835
+ for (const step of playbook2.steps) {
836
+ if (stopped) break;
837
+ const due = (nextRunAt.get(step.id) ?? 0) <= tickAt;
838
+ if (!due) continue;
839
+ let receipt;
840
+ try {
841
+ const cls = classifyStep(step);
842
+ const approval = resolveEffectiveApproval(step, cls, {
843
+ playbookSigned: Boolean(playbook2.signature)
844
+ });
845
+ const preview = await callRpc2(session, step.kind, step.params);
846
+ const isPrev = isPreviewEnvelope(preview);
847
+ if (approval.forcedApproval || !approval.autoApply) {
848
+ receipt = makeReceipt(playbook2, step, {
849
+ mode: "needs-approval",
850
+ preview: isPrev ? preview.action.preview : preview,
851
+ risk: cls.rank,
852
+ riskReason: cls.reason,
853
+ approvalReason: approval.reason
854
+ });
855
+ } else if (applyTimestamps.length >= opts.maxPerHour) {
856
+ receipt = makeReceipt(playbook2, step, { mode: "skipped-ceiling", risk: cls.rank });
857
+ } else if (!isPrev) {
858
+ receipt = makeReceipt(playbook2, step, {
859
+ mode: "dry-run",
860
+ preview,
861
+ risk: cls.rank
862
+ });
863
+ } else {
864
+ const applyRes = await callRpc2(session, "apply.batch", {
865
+ actions: [{ kind: step.kind, params: step.params }]
866
+ });
867
+ const first = applyRes.results[0];
868
+ if (first?.ok) {
869
+ applyTimestamps.push(Date.now());
870
+ receipt = makeReceipt(playbook2, step, {
871
+ mode: "applied",
872
+ preview: preview.action.preview,
873
+ result: first.result,
874
+ risk: cls.rank
875
+ });
876
+ } else {
877
+ receipt = makeReceipt(playbook2, step, {
878
+ mode: "error",
879
+ preview: preview.action.preview,
880
+ error: first?.error ?? "apply.batch returned no result",
881
+ risk: cls.rank
882
+ });
883
+ }
884
+ }
885
+ } catch (err) {
886
+ receipt = makeReceipt(playbook2, step, {
887
+ mode: "error",
888
+ error: err.message
889
+ });
39
890
  }
40
- if (!/^[a-zA-Z0-9._-]+$/.test(alias)) {
41
- throw new Error(`--allow-sql alias must be [a-zA-Z0-9._-]+: ${alias}`);
891
+ await appendReceipt(receiptsPath, receipt);
892
+ await postReceipt(opts, receipt);
893
+ const short = (receipt.hash ?? "").slice(0, 8) || "--------";
894
+ log2(`avar+ ${short} ${receipt.mode} ${step.label}`);
895
+ if (!opts.once && step.intervalSec && step.intervalSec > 0) {
896
+ nextRunAt.set(step.id, Date.now() + step.intervalSec * 1e3);
42
897
  }
43
- return { alias, dsn };
44
- });
45
- await startDev({
46
- port: Number(opts.port),
47
- pwaUrl: opts.pwa,
48
- autoOpen: opts.open !== false,
49
- extraShellAllow: opts.allowShell
50
- ? String(opts.allowShell)
51
- .split(",")
52
- .map((s) => s.trim())
53
- .filter(Boolean)
54
- : [],
55
- sqlAllow,
56
- readAllow: opts.allowRead,
898
+ }
899
+ if (opts.once) break;
900
+ const soonest = Math.min(...playbook2.steps.map((s) => nextRunAt.get(s.id) ?? Infinity));
901
+ const waitMs = Math.max(1e3, Math.min(5e3, soonest - Date.now()));
902
+ await sleep(waitMs);
903
+ } while (!stopped);
904
+ } finally {
905
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
906
+ }
907
+ }
908
+ function makeReceipt(pb, step, extra) {
909
+ const base = {
910
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
911
+ playbook: pb.name,
912
+ stepId: step.id,
913
+ label: step.label,
914
+ kind: step.kind,
915
+ mode: extra.mode,
916
+ preview: extra.preview,
917
+ result: extra.result,
918
+ error: extra.error,
919
+ risk: extra.risk,
920
+ riskReason: extra.riskReason,
921
+ approvalReason: extra.approvalReason
922
+ };
923
+ const hash = crypto4.createHash("sha256").update(JSON.stringify(base)).digest("hex");
924
+ return { ...base, hash };
925
+ }
926
+ async function appendReceipt(file, r) {
927
+ await fs4.appendFile(file, JSON.stringify(r) + "\n", "utf8");
928
+ }
929
+ async function collectPackAllow2(workspace) {
930
+ const packs = await listInstalledPacks(path6.join(workspace, ".aarmos/packs"));
931
+ const shell = /* @__PURE__ */ new Set();
932
+ const http = /* @__PURE__ */ new Set();
933
+ const mcp = /* @__PURE__ */ new Set();
934
+ for (const p of packs) {
935
+ for (const b of p.allow?.shell ?? []) shell.add(b);
936
+ for (const h of p.allow?.http ?? []) http.add(h);
937
+ for (const m of p.allow?.mcp ?? []) mcp.add(m);
938
+ }
939
+ return { shell: [...shell], http: [...http], mcp: [...mcp] };
940
+ }
941
+ function killSwitchTripped(file) {
942
+ if (!file) return false;
943
+ try {
944
+ fssync.accessSync(file);
945
+ return true;
946
+ } catch {
947
+ return false;
948
+ }
949
+ }
950
+ function pruneWindow(ts, now) {
951
+ const cutoff = now - 60 * 60 * 1e3;
952
+ while (ts.length > 0 && ts[0] < cutoff) ts.shift();
953
+ }
954
+ async function postReceipt(opts, r) {
955
+ if (!opts.heartbeatUrl) return;
956
+ await safePost(opts.heartbeatUrl.replace(/\/$/, "") + "/receipts", opts.heartbeatToken, r);
957
+ }
958
+ async function postHeartbeat(opts, body) {
959
+ await safePost(opts.heartbeatUrl.replace(/\/$/, "") + "/heartbeat", opts.heartbeatToken, {
960
+ ...body,
961
+ ts: (/* @__PURE__ */ new Date()).toISOString()
962
+ });
963
+ }
964
+ async function safePost(url, token, body) {
965
+ try {
966
+ await fetch(url, {
967
+ method: "POST",
968
+ headers: {
969
+ "content-type": "application/json",
970
+ ...token ? { authorization: `Bearer ${token}` } : {}
971
+ },
972
+ body: JSON.stringify(body)
57
973
  });
974
+ } catch {
975
+ }
976
+ }
977
+ function isPreviewEnvelope(x) {
978
+ return !!x && typeof x === "object" && x.preview === true;
979
+ }
980
+ async function callRpc2(session, method, params) {
981
+ const res = await handleRpc(session, { id: 1, type: "rpc", method, params });
982
+ if ("type" in res && res.type === "error") throw new Error(res.error);
983
+ if ("type" in res && res.type === "result") return res.result;
984
+ throw new Error("unexpected rpc envelope");
985
+ }
986
+ function sleep(ms) {
987
+ return new Promise((r) => setTimeout(r, ms));
988
+ }
989
+ function log2(s) {
990
+ console.log(s);
991
+ }
992
+
993
+ // src/commands/playbook.ts
994
+ import fs5 from "fs/promises";
995
+ import path7 from "path";
996
+ var log3 = (s) => console.log(s);
997
+ async function playbookKeygen(opts) {
998
+ const kp = await generateSigningKeyPair(path7.resolve(opts.out), opts.name);
999
+ log3("");
1000
+ log3(" aarmos playbook keygen");
1001
+ log3(` private : ${kp.privatePem} (chmod 600 \u2014 do not commit)`);
1002
+ log3(` public key : ${kp.publicKey}`);
1003
+ log3("");
1004
+ log3(" Distribute the public key to reviewers; pin it via");
1005
+ log3(` aarmos run --enforce-signatures --trusted-pubkey ${kp.publicKey}`);
1006
+ log3("");
1007
+ }
1008
+ async function playbookSign(opts) {
1009
+ const pb = await loadPlaybook(opts.playbook);
1010
+ const pem = await fs5.readFile(path7.resolve(opts.key), "utf8");
1011
+ const signed = await signPlaybook(pb, pem);
1012
+ const outPath = path7.resolve(opts.out ?? opts.playbook);
1013
+ await writePlaybook(outPath, signed);
1014
+ log3("");
1015
+ log3(` signed ${opts.playbook}`);
1016
+ log3(` pubkey : ${signed.signature.pubKey}`);
1017
+ log3(` out : ${outPath}`);
1018
+ log3("");
1019
+ }
1020
+ async function playbookVerify(opts) {
1021
+ const pb = await loadPlaybook(opts.playbook);
1022
+ const res = verifyPlaybookSignature(pb);
1023
+ log3("");
1024
+ log3(` aarmos playbook verify \u2014 ${opts.playbook}`);
1025
+ if (!pb.signature) {
1026
+ log3(" unsigned");
1027
+ process.exitCode = 1;
1028
+ return;
1029
+ }
1030
+ log3(` pubkey : ${pb.signature.pubKey}`);
1031
+ log3(` result : ${res.ok ? "OK" : "INVALID"}${res.reason ? " " + res.reason : ""}`);
1032
+ if (opts.trustedPubkey && pb.signature.pubKey !== opts.trustedPubkey) {
1033
+ log3(` trust : REJECTED \u2014 signer not in trusted set`);
1034
+ process.exitCode = 1;
1035
+ return;
1036
+ }
1037
+ if (!res.ok) process.exitCode = 1;
1038
+ log3("");
1039
+ }
1040
+
1041
+ // src/commands/pack.ts
1042
+ import fs6 from "fs/promises";
1043
+ import path8 from "path";
1044
+ import crypto5 from "crypto";
1045
+ var log4 = (s) => console.log(s);
1046
+ var PACKS_DIR = ".aarmos/packs";
1047
+ async function packInstall(opts) {
1048
+ const root = path8.resolve(opts.workspace ?? process.cwd(), PACKS_DIR);
1049
+ const { pack: pack2, installed } = await installPack(opts.source, root);
1050
+ log4("");
1051
+ log4(` installed ${pack2.name}@${pack2.version}`);
1052
+ log4(` hash : ${hashPack(pack2)}`);
1053
+ log4(` signed : ${pack2.signature ? pack2.signature.pubKey : "(no)"}`);
1054
+ log4(` at : ${installed}`);
1055
+ log4("");
1056
+ log4(" Next: run one of the pack's playbooks");
1057
+ for (const pb of pack2.playbooks) {
1058
+ log4(
1059
+ ` aarmos run --playbook ${path8.join(packInstallDir(pack2, PACKS_DIR), "playbook." + safe(pb.name) + ".json")}`
1060
+ );
1061
+ }
1062
+ const dir = packInstallDir(pack2, root);
1063
+ for (const pb of pack2.playbooks) {
1064
+ const pbFile = path8.join(dir, `playbook.${safe(pb.name)}.json`);
1065
+ await fs6.writeFile(pbFile, JSON.stringify(pb, null, 2) + "\n", "utf8");
1066
+ }
1067
+ log4("");
1068
+ }
1069
+ async function packList(opts) {
1070
+ const root = path8.resolve(opts.workspace ?? process.cwd(), PACKS_DIR);
1071
+ const packs = await listInstalledPacks(root);
1072
+ log4("");
1073
+ log4(` installed packs (${root})`);
1074
+ if (packs.length === 0) log4(" (none)");
1075
+ for (const p of packs) {
1076
+ log4(
1077
+ ` ${p.name}@${p.version} hash=${hashPack(p).slice(0, 16)}\u2026 signed=${p.signature ? "yes" : "no"} playbooks=${p.playbooks.length}`
1078
+ );
1079
+ }
1080
+ log4("");
1081
+ }
1082
+ async function packVerify(opts) {
1083
+ const pack2 = await loadPack(opts.source);
1084
+ log4("");
1085
+ log4(` aarmos pack verify \u2014 ${pack2.name}@${pack2.version}`);
1086
+ log4(` hash : ${hashPack(pack2)}`);
1087
+ if (!pack2.signature) {
1088
+ log4(" signed : NO");
1089
+ process.exitCode = 1;
1090
+ return;
1091
+ }
1092
+ const ok = verifyPackSignatureDirect(pack2);
1093
+ log4(` signer : ${pack2.signature.pubKey}`);
1094
+ log4(` result : ${ok ? "OK" : "INVALID"}`);
1095
+ if (opts.trustedPubkey && pack2.signature.pubKey !== opts.trustedPubkey) {
1096
+ log4(" trust : REJECTED \u2014 signer not in trusted set");
1097
+ process.exitCode = 1;
1098
+ } else if (!ok) {
1099
+ process.exitCode = 1;
1100
+ }
1101
+ for (const pb of pack2.playbooks) {
1102
+ if (pb.signature) {
1103
+ const v = verifyPlaybookSignature(pb);
1104
+ log4(
1105
+ ` playbook signature (${pb.name}): ${v.ok ? "OK" : "INVALID"}${v.reason ? " " + v.reason : ""}`
1106
+ );
1107
+ }
1108
+ }
1109
+ log4("");
1110
+ }
1111
+ function verifyPackSignatureDirect(pack2) {
1112
+ if (!pack2.signature) return false;
1113
+ const body = Buffer.from(packBody(pack2));
1114
+ try {
1115
+ const spkiPrefix = Buffer.from([
1116
+ 48,
1117
+ 42,
1118
+ 48,
1119
+ 5,
1120
+ 6,
1121
+ 3,
1122
+ 43,
1123
+ 101,
1124
+ 112,
1125
+ 3,
1126
+ 33,
1127
+ 0
1128
+ ]);
1129
+ const b64uDecode2 = (s) => Buffer.from(
1130
+ s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4),
1131
+ "base64"
1132
+ );
1133
+ const rawPub = b64uDecode2(pack2.signature.pubKey);
1134
+ if (rawPub.length !== 32) return false;
1135
+ const der = Buffer.concat([spkiPrefix, rawPub]);
1136
+ const key = crypto5.createPublicKey({ key: der, format: "der", type: "spki" });
1137
+ return crypto5.verify(null, body, key, b64uDecode2(pack2.signature.sig));
1138
+ } catch {
1139
+ return false;
1140
+ }
1141
+ }
1142
+ function safe(s) {
1143
+ return s.replace(/[^a-zA-Z0-9._-]/g, "_");
1144
+ }
1145
+
1146
+ // src/commands/attest.ts
1147
+ import fs8 from "fs/promises";
1148
+ import path10 from "path";
1149
+
1150
+ // src/lib/attest.ts
1151
+ import fs7 from "fs/promises";
1152
+ import path9 from "path";
1153
+ import crypto6 from "crypto";
1154
+ async function buildAttestation(opts) {
1155
+ const until = opts.until ?? /* @__PURE__ */ new Date();
1156
+ const since = opts.since ?? /* @__PURE__ */ new Date(0);
1157
+ const receipts = [];
1158
+ try {
1159
+ const raw = await fs7.readFile(opts.receiptsFile, "utf8");
1160
+ for (const line of raw.split("\n")) {
1161
+ const t = line.trim();
1162
+ if (!t) continue;
1163
+ let obj;
1164
+ try {
1165
+ obj = JSON.parse(t);
1166
+ } catch {
1167
+ continue;
1168
+ }
1169
+ const at = Date.parse(obj.ts);
1170
+ if (Number.isFinite(at) && at >= since.getTime() && at <= until.getTime()) {
1171
+ receipts.push(obj);
1172
+ }
1173
+ }
1174
+ } catch {
1175
+ }
1176
+ const rootHash = merkleRoot(receipts.map((r) => r.hash ?? ""));
1177
+ let playbookEntry;
1178
+ if (opts.playbookFile) {
1179
+ const pb = await loadPlaybook(opts.playbookFile);
1180
+ playbookEntry = {
1181
+ name: pb.name,
1182
+ hash: hashPlaybook(pb),
1183
+ signed: Boolean(pb.signature),
1184
+ pubKey: pb.signature?.pubKey
1185
+ };
1186
+ }
1187
+ let policyHash;
1188
+ if (opts.policyFile) {
1189
+ try {
1190
+ const raw = await fs7.readFile(opts.policyFile, "utf8");
1191
+ policyHash = crypto6.createHash("sha256").update(raw).digest("hex");
1192
+ } catch {
1193
+ }
1194
+ }
1195
+ const packs = opts.packsDir ? (await listInstalledPacks(opts.packsDir)).map((p) => ({
1196
+ name: p.name,
1197
+ version: p.version,
1198
+ hash: hashPack(p)
1199
+ })) : [];
1200
+ return {
1201
+ version: 1,
1202
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1203
+ since: since.toISOString(),
1204
+ until: until.toISOString(),
1205
+ playbook: playbookEntry,
1206
+ policyHash,
1207
+ packs,
1208
+ receipts,
1209
+ rootHash
1210
+ };
1211
+ }
1212
+ function attestationBody(a) {
1213
+ const { signature: _s, ...rest } = a;
1214
+ void _s;
1215
+ return canonicalize(rest);
1216
+ }
1217
+ async function signAttestation(a, privateKeyPem) {
1218
+ const key = crypto6.createPrivateKey(privateKeyPem);
1219
+ const sig = crypto6.sign(null, Buffer.from(attestationBody(a)), key);
1220
+ const pubDer = crypto6.createPublicKey(key).export({ format: "der", type: "spki" });
1221
+ const rawPub = pubDer.subarray(pubDer.length - 32);
1222
+ const b64u2 = (b) => b.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1223
+ return { ...a, signature: { alg: "ed25519", pubKey: b64u2(rawPub), sig: b64u2(sig) } };
1224
+ }
1225
+ function verifyAttestation(a) {
1226
+ const recomputed = merkleRoot(a.receipts.map((r) => r.hash ?? ""));
1227
+ if (recomputed !== a.rootHash) {
1228
+ return { ok: false, reason: `rootHash mismatch (bundle=${a.rootHash} computed=${recomputed})` };
1229
+ }
1230
+ if (!a.signature) return { ok: true };
1231
+ if (a.signature.alg !== "ed25519") {
1232
+ return { ok: false, reason: `unsupported alg: ${a.signature.alg}` };
1233
+ }
1234
+ try {
1235
+ const spkiPrefix = Buffer.from([
1236
+ 48,
1237
+ 42,
1238
+ 48,
1239
+ 5,
1240
+ 6,
1241
+ 3,
1242
+ 43,
1243
+ 101,
1244
+ 112,
1245
+ 3,
1246
+ 33,
1247
+ 0
1248
+ ]);
1249
+ const b64uDecode2 = (s) => Buffer.from(
1250
+ s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4),
1251
+ "base64"
1252
+ );
1253
+ const rawPub = b64uDecode2(a.signature.pubKey);
1254
+ if (rawPub.length !== 32) return { ok: false, reason: "pubKey must be 32 bytes" };
1255
+ const der = Buffer.concat([spkiPrefix, rawPub]);
1256
+ const key = crypto6.createPublicKey({ key: der, format: "der", type: "spki" });
1257
+ const ok = crypto6.verify(
1258
+ null,
1259
+ Buffer.from(attestationBody(a)),
1260
+ key,
1261
+ b64uDecode2(a.signature.sig)
1262
+ );
1263
+ return ok ? { ok: true } : { ok: false, reason: "signature does not match bundle body" };
1264
+ } catch (err) {
1265
+ return { ok: false, reason: err.message };
1266
+ }
1267
+ }
1268
+ function merkleRoot(hashes) {
1269
+ let acc = crypto6.createHash("sha256").update("").digest("hex");
1270
+ for (const h of hashes) {
1271
+ acc = crypto6.createHash("sha256").update(acc + h).digest("hex");
1272
+ }
1273
+ return acc;
1274
+ }
1275
+ function parseSince(s) {
1276
+ const m = /^(\d+)\s*([hdm])$/.exec(s.trim());
1277
+ if (m) {
1278
+ const n = Number(m[1]);
1279
+ const unit = m[2];
1280
+ const ms = unit === "h" ? n * 36e5 : unit === "d" ? n * 864e5 : n * 6e4;
1281
+ return new Date(Date.now() - ms);
1282
+ }
1283
+ const t = Date.parse(s);
1284
+ if (Number.isFinite(t)) return new Date(t);
1285
+ throw new Error(`--since: expected e.g. 24h, 7d, 30m, or an ISO date (got: ${s})`);
1286
+ }
1287
+ async function writeAttestation(file, a) {
1288
+ await fs7.mkdir(path9.dirname(path9.resolve(file)), { recursive: true });
1289
+ await fs7.writeFile(path9.resolve(file), JSON.stringify(a, null, 2) + "\n", "utf8");
1290
+ }
1291
+ async function readAttestation(file) {
1292
+ const raw = await fs7.readFile(path9.resolve(file), "utf8");
1293
+ return JSON.parse(raw);
1294
+ }
1295
+
1296
+ // src/commands/attest.ts
1297
+ var log5 = (s) => console.log(s);
1298
+ async function runAttest(opts) {
1299
+ const workspace = path10.resolve(opts.workspace ?? process.cwd());
1300
+ const since = opts.since ? parseSince(opts.since) : /* @__PURE__ */ new Date(0);
1301
+ const bundle = await buildAttestation({
1302
+ workspace,
1303
+ receiptsFile: path10.resolve(opts.receipts),
1304
+ since,
1305
+ until: /* @__PURE__ */ new Date(),
1306
+ playbookFile: opts.playbook,
1307
+ policyFile: opts.policy ?? path10.join(workspace, "aarmos.json"),
1308
+ packsDir: path10.join(workspace, ".aarmos/packs")
1309
+ });
1310
+ const finalBundle = opts.signKey ? await signAttestation(bundle, await fs8.readFile(path10.resolve(opts.signKey), "utf8")) : bundle;
1311
+ await writeAttestation(opts.out, finalBundle);
1312
+ log5("");
1313
+ log5(" aarmos attest");
1314
+ log5(` window : ${finalBundle.since} \u2192 ${finalBundle.until}`);
1315
+ log5(` receipts : ${finalBundle.receipts.length}`);
1316
+ log5(
1317
+ ` playbook : ${finalBundle.playbook ? finalBundle.playbook.name + " (" + finalBundle.playbook.hash.slice(0, 16) + "\u2026)" : "(none)"}`
1318
+ );
1319
+ log5(
1320
+ ` policy : ${finalBundle.policyHash ? finalBundle.policyHash.slice(0, 16) + "\u2026" : "(none)"}`
1321
+ );
1322
+ log5(` packs : ${finalBundle.packs.length}`);
1323
+ log5(` rootHash : ${finalBundle.rootHash}`);
1324
+ log5(` signature : ${finalBundle.signature ? finalBundle.signature.pubKey : "(unsigned)"}`);
1325
+ log5(` written to : ${path10.resolve(opts.out)}`);
1326
+ log5("");
1327
+ }
1328
+ async function runAttestVerify(opts) {
1329
+ const bundle = await readAttestation(opts.file);
1330
+ const res = verifyAttestation(bundle);
1331
+ log5("");
1332
+ log5(` aarmos attest verify \u2014 ${opts.file}`);
1333
+ log5(` receipts : ${bundle.receipts.length}`);
1334
+ log5(` rootHash : ${bundle.rootHash}`);
1335
+ log5(` result : ${res.ok ? "OK" : "INVALID"}${res.reason ? " " + res.reason : ""}`);
1336
+ if (bundle.signature) {
1337
+ log5(` signer : ${bundle.signature.pubKey}`);
1338
+ if (opts.trustedPubkey && bundle.signature.pubKey !== opts.trustedPubkey) {
1339
+ log5(" trust : REJECTED \u2014 signer not in trusted set");
1340
+ process.exitCode = 1;
1341
+ }
1342
+ } else {
1343
+ log5(" signer : (unsigned)");
1344
+ }
1345
+ if (!res.ok) process.exitCode = 1;
1346
+ log5("");
1347
+ }
1348
+
1349
+ // src/index.ts
1350
+ var program = new Command();
1351
+ program.name("aarmos").description("Aarmos CLI bridge \u2014 native host for the Aarmos PWA").version("0.0.0-dev");
1352
+ function collect(value, prev) {
1353
+ return [...prev, value];
1354
+ }
1355
+ program.command("dev").description("Start the local bridge and pair with the PWA").option("-p, --port <port>", "WebSocket port to bind on 127.0.0.1", "7423").option(
1356
+ "--pwa <url>",
1357
+ "PWA base URL to open for pairing",
1358
+ process.env.AARMOS_PWA_URL ?? "https://www.aarmos.io"
1359
+ ).option("--no-open", "Do not auto-open the PWA in the browser").option(
1360
+ "--allow-shell <bins>",
1361
+ "Comma-separated additional shell binaries to allow (in addition to defaults)",
1362
+ ""
1363
+ ).option(
1364
+ "--allow-sql <alias=dsn>",
1365
+ "Register a read-only Postgres target. Repeatable. Example: --allow-sql prod=postgres://readonly@\u2026/db",
1366
+ collect,
1367
+ []
1368
+ ).option(
1369
+ "--allow-read <path>",
1370
+ "Allow fs.scopedRead / fs.scopedList inside this absolute path (outside the workspace). Repeatable.",
1371
+ collect,
1372
+ []
1373
+ ).action(async (opts) => {
1374
+ const sqlAllow = opts.allowSql.map((raw) => {
1375
+ const eq = raw.indexOf("=");
1376
+ if (eq < 0) {
1377
+ throw new Error(`--allow-sql expects <alias>=<dsn>, got: ${raw}`);
1378
+ }
1379
+ const alias = raw.slice(0, eq).trim();
1380
+ const dsn = raw.slice(eq + 1).trim();
1381
+ if (!alias || !dsn) {
1382
+ throw new Error(`--allow-sql alias and dsn must be non-empty: ${raw}`);
1383
+ }
1384
+ if (!/^[a-zA-Z0-9._-]+$/.test(alias)) {
1385
+ throw new Error(`--allow-sql alias must be [a-zA-Z0-9._-]+: ${alias}`);
1386
+ }
1387
+ return { alias, dsn };
1388
+ });
1389
+ await startDev({
1390
+ port: Number(opts.port),
1391
+ pwaUrl: opts.pwa,
1392
+ autoOpen: opts.open !== false,
1393
+ extraShellAllow: opts.allowShell ? String(opts.allowShell).split(",").map((s) => s.trim()).filter(Boolean) : [],
1394
+ sqlAllow,
1395
+ readAllow: opts.allowRead
1396
+ });
58
1397
  });
59
- program
60
- .command("demo")
61
- .description("Scripted end-to-end walkthrough of the dry-run pipeline (no PWA required)")
62
- .option("--apply", "Commit the proposed batch after previewing (default: preview only)", false)
63
- .option("--workspace <path>", "Workspace to run against (default: cwd)")
64
- .action(async (opts) => {
65
- await runDemo({ apply: Boolean(opts.apply), workspace: opts.workspace });
1398
+ program.command("demo").description("Scripted end-to-end walkthrough of the dry-run pipeline (no PWA required)").option("--apply", "Commit the proposed batch after previewing (default: preview only)", false).option("--workspace <path>", "Workspace to run against (default: cwd)").action(async (opts) => {
1399
+ await runDemo({ apply: Boolean(opts.apply), workspace: opts.workspace });
66
1400
  });
67
- program
68
- .command("run")
69
- .description("Headless unattended loop executes a playbook against the local dry-run pipeline")
70
- .requiredOption("--playbook <path>", "Path to a JSON playbook file")
71
- .option("--once", "Run one pass and exit (default: loop)", false)
72
- .option("--dry", "Simulate only no side effects. Prints per-step verdicts and exits.", false)
73
- .option("--enforce-signatures", "Refuse to run unless the playbook is signed (Ed25519)", false)
74
- .option("--trusted-pubkey <key>", "Only accept playbooks signed by this pubkey. Repeatable. Requires --enforce-signatures.", collect, [])
75
- .option("--kill-switch <file>", "Halt gracefully when this file exists")
76
- .option("--receipts <file>", "Append signed receipts here (JSONL)", ".aarmos/receipts.jsonl")
77
- .option("--max-per-hour <n>", "Ceiling on applied actions per rolling hour", "60")
78
- .option("--heartbeat-url <url>", "POST receipts + heartbeats to this base URL")
79
- .option("--heartbeat-token <token>", "Bearer token attached to heartbeat POSTs")
80
- .option("--workspace <path>", "Workspace to run against (default: cwd)")
81
- .option("--allow-shell <bins>", "Comma-separated additional shell binaries to allow (in addition to defaults)", "")
82
- .option("--allow-http <host>", "Allow governed http.call steps to reach this host. Repeatable.", collect, [])
83
- .option("--allow-mcp <url>", "Allow governed mcp.tool steps to invoke this MCP endpoint. Repeatable.", collect, [])
84
- .action(async (opts) => {
85
- await startRun({
86
- playbook: opts.playbook,
87
- once: Boolean(opts.once),
88
- dry: Boolean(opts.dry),
89
- enforceSignatures: Boolean(opts.enforceSignatures),
90
- trustedPubKeys: opts.trustedPubkey,
91
- killSwitch: opts.killSwitch,
92
- receipts: opts.receipts,
93
- maxPerHour: Number(opts.maxPerHour),
94
- heartbeatUrl: opts.heartbeatUrl,
95
- heartbeatToken: opts.heartbeatToken,
96
- workspace: opts.workspace,
97
- extraShellAllow: opts.allowShell
98
- ? String(opts.allowShell)
99
- .split(",")
100
- .map((s) => s.trim())
101
- .filter(Boolean)
102
- : [],
103
- httpAllow: opts.allowHttp,
104
- mcpAllow: opts.allowMcp,
105
- });
1401
+ program.command("run").description("Headless unattended loop \u2014 executes a playbook against the local dry-run pipeline").requiredOption("--playbook <path>", "Path to a JSON playbook file").option("--once", "Run one pass and exit (default: loop)", false).option("--dry", "Simulate only \u2014 no side effects. Prints per-step verdicts and exits.", false).option("--enforce-signatures", "Refuse to run unless the playbook is signed (Ed25519)", false).option(
1402
+ "--trusted-pubkey <key>",
1403
+ "Only accept playbooks signed by this pubkey. Repeatable. Requires --enforce-signatures.",
1404
+ collect,
1405
+ []
1406
+ ).option("--kill-switch <file>", "Halt gracefully when this file exists").option("--receipts <file>", "Append signed receipts here (JSONL)", ".aarmos/receipts.jsonl").option("--max-per-hour <n>", "Ceiling on applied actions per rolling hour", "60").option("--heartbeat-url <url>", "POST receipts + heartbeats to this base URL").option("--heartbeat-token <token>", "Bearer token attached to heartbeat POSTs").option("--workspace <path>", "Workspace to run against (default: cwd)").option(
1407
+ "--allow-shell <bins>",
1408
+ "Comma-separated additional shell binaries to allow (in addition to defaults)",
1409
+ ""
1410
+ ).option(
1411
+ "--allow-http <host>",
1412
+ "Allow governed http.call steps to reach this host. Repeatable.",
1413
+ collect,
1414
+ []
1415
+ ).option(
1416
+ "--allow-mcp <url>",
1417
+ "Allow governed mcp.tool steps to invoke this MCP endpoint. Repeatable.",
1418
+ collect,
1419
+ []
1420
+ ).action(async (opts) => {
1421
+ await startRun({
1422
+ playbook: opts.playbook,
1423
+ once: Boolean(opts.once),
1424
+ dry: Boolean(opts.dry),
1425
+ enforceSignatures: Boolean(opts.enforceSignatures),
1426
+ trustedPubKeys: opts.trustedPubkey,
1427
+ killSwitch: opts.killSwitch,
1428
+ receipts: opts.receipts,
1429
+ maxPerHour: Number(opts.maxPerHour),
1430
+ heartbeatUrl: opts.heartbeatUrl,
1431
+ heartbeatToken: opts.heartbeatToken,
1432
+ workspace: opts.workspace,
1433
+ extraShellAllow: opts.allowShell ? String(opts.allowShell).split(",").map((s) => s.trim()).filter(Boolean) : [],
1434
+ httpAllow: opts.allowHttp,
1435
+ mcpAllow: opts.allowMcp
1436
+ });
106
1437
  });
107
- program
108
- .command("simulate")
109
- .description("Compile-time governance: replay a playbook through the dry-run pipeline without applying anything")
110
- .requiredOption("--playbook <path>", "Path to a JSON playbook file")
111
- .option("--workspace <path>", "Workspace to simulate against (default: cwd)")
112
- .option("--json", "Emit machine-readable JSON (for CI)", false)
113
- .option("--strict", "Exit non-zero if any step would fail", false)
114
- .option("--allow-shell <bins>", "Comma-separated additional shell binaries to allow", "")
115
- .option("--allow-http <host>", "Allow http.call to this host. Repeatable.", collect, [])
116
- .option("--allow-mcp <url>", "Allow mcp.tool against this MCP endpoint. Repeatable.", collect, [])
117
- .action(async (opts) => {
118
- await runSimulate({
119
- playbook: opts.playbook,
120
- workspace: opts.workspace,
121
- json: Boolean(opts.json()),
122
- strict: Boolean(opts.strict),
123
- extraShellAllow: opts.allowShell
124
- ? String(opts.allowShell)
125
- .split(",")
126
- .map((s) => s.trim())
127
- .filter(Boolean)
128
- : [],
129
- httpAllow: opts.allowHttp,
130
- mcpAllow: opts.allowMcp,
131
- });
1438
+ program.command("simulate").description(
1439
+ "Compile-time governance: replay a playbook through the dry-run pipeline without applying anything"
1440
+ ).requiredOption("--playbook <path>", "Path to a JSON playbook file").option("--workspace <path>", "Workspace to simulate against (default: cwd)").option("--json", "Emit machine-readable JSON (for CI)", false).option("--strict", "Exit non-zero if any step would fail", false).option("--allow-shell <bins>", "Comma-separated additional shell binaries to allow", "").option(
1441
+ "--allow-http <host>",
1442
+ "Allow http.call to this host. Repeatable.",
1443
+ collect,
1444
+ []
1445
+ ).option(
1446
+ "--allow-mcp <url>",
1447
+ "Allow mcp.tool against this MCP endpoint. Repeatable.",
1448
+ collect,
1449
+ []
1450
+ ).action(async (opts) => {
1451
+ await runSimulate({
1452
+ playbook: opts.playbook,
1453
+ workspace: opts.workspace,
1454
+ json: Boolean(opts.json()),
1455
+ strict: Boolean(opts.strict),
1456
+ extraShellAllow: opts.allowShell ? String(opts.allowShell).split(",").map((s) => s.trim()).filter(Boolean) : [],
1457
+ httpAllow: opts.allowHttp,
1458
+ mcpAllow: opts.allowMcp
1459
+ });
132
1460
  });
133
- const playbook = program
134
- .command("playbook")
135
- .description("Sign, verify, and manage Ed25519 keys for playbooks (optional; only enforced under --enforce-signatures)");
136
- playbook
137
- .command("keygen")
138
- .description("Generate an Ed25519 signing keypair")
139
- .option("--out <dir>", "Directory to write keys into", ".aarmos/keys")
140
- .option("--name <name>", "Basename for the key files", "playbook")
141
- .action(async (opts) => {
142
- await playbookKeygen({ out: opts.out, name: opts.name });
1461
+ var playbook = program.command("playbook").description(
1462
+ "Sign, verify, and manage Ed25519 keys for playbooks (optional; only enforced under --enforce-signatures)"
1463
+ );
1464
+ playbook.command("keygen").description("Generate an Ed25519 signing keypair").option("--out <dir>", "Directory to write keys into", ".aarmos/keys").option("--name <name>", "Basename for the key files", "playbook").action(async (opts) => {
1465
+ await playbookKeygen({ out: opts.out, name: opts.name });
143
1466
  });
144
- playbook
145
- .command("sign")
146
- .description("Sign a playbook in place (or to --out)")
147
- .requiredOption("--playbook <path>", "Playbook to sign")
148
- .requiredOption("--key <pem>", "Ed25519 private key PEM file")
149
- .option("--out <path>", "Write signed playbook here (default: overwrite in place)")
150
- .action(async (opts) => {
151
- await playbookSign({ playbook: opts.playbook, key: opts.key, out: opts.out });
1467
+ playbook.command("sign").description("Sign a playbook in place (or to --out)").requiredOption("--playbook <path>", "Playbook to sign").requiredOption("--key <pem>", "Ed25519 private key PEM file").option("--out <path>", "Write signed playbook here (default: overwrite in place)").action(async (opts) => {
1468
+ await playbookSign({ playbook: opts.playbook, key: opts.key, out: opts.out });
152
1469
  });
153
- playbook
154
- .command("verify")
155
- .description("Verify a signed playbook")
156
- .requiredOption("--playbook <path>", "Playbook to verify")
157
- .option("--trusted-pubkey <key>", "Reject if signer is not this pubkey")
158
- .action(async (opts) => {
159
- await playbookVerify({ playbook: opts.playbook, trustedPubkey: opts.trustedPubkey });
1470
+ playbook.command("verify").description("Verify a signed playbook").requiredOption("--playbook <path>", "Playbook to verify").option("--trusted-pubkey <key>", "Reject if signer is not this pubkey").action(async (opts) => {
1471
+ await playbookVerify({ playbook: opts.playbook, trustedPubkey: opts.trustedPubkey });
160
1472
  });
161
- const pack = program
162
- .command("pack")
163
- .description("Install / list / verify certified playbook packs (signed bundles of playbooks + scope allowlists)");
164
- pack
165
- .command("install")
166
- .description("Install a pack from a local path (or URL — future).")
167
- .requiredOption("--source <path>", "Path to a pack JSON file")
168
- .option("--workspace <path>", "Workspace to install into (default: cwd)")
169
- .action(async (opts) => {
170
- await packInstall({ source: opts.source, workspace: opts.workspace });
1473
+ var pack = program.command("pack").description(
1474
+ "Install / list / verify certified playbook packs (signed bundles of playbooks + scope allowlists)"
1475
+ );
1476
+ pack.command("install").description("Install a pack from a local path (or URL \u2014 future).").requiredOption("--source <path>", "Path to a pack JSON file").option("--workspace <path>", "Workspace to install into (default: cwd)").action(async (opts) => {
1477
+ await packInstall({ source: opts.source, workspace: opts.workspace });
171
1478
  });
172
- pack
173
- .command("list")
174
- .description("List installed packs")
175
- .option("--workspace <path>", "Workspace to inspect (default: cwd)")
176
- .action(async (opts) => {
177
- await packList({ workspace: opts.workspace });
1479
+ pack.command("list").description("List installed packs").option("--workspace <path>", "Workspace to inspect (default: cwd)").action(async (opts) => {
1480
+ await packList({ workspace: opts.workspace });
178
1481
  });
179
- pack
180
- .command("verify")
181
- .description("Verify a pack's Ed25519 signature")
182
- .requiredOption("--source <path>", "Path to a pack JSON file")
183
- .option("--trusted-pubkey <key>", "Reject if signer is not this pubkey")
184
- .action(async (opts) => {
185
- await packVerify({ source: opts.source, trustedPubkey: opts.trustedPubkey });
1482
+ pack.command("verify").description("Verify a pack's Ed25519 signature").requiredOption("--source <path>", "Path to a pack JSON file").option("--trusted-pubkey <key>", "Reject if signer is not this pubkey").action(async (opts) => {
1483
+ await packVerify({ source: opts.source, trustedPubkey: opts.trustedPubkey });
186
1484
  });
187
- const attest = program
188
- .command("attest")
189
- .description("Export a signed audit bundle (receipts + policy hash + pack hashes + rootHash) — verifiable offline")
190
- .option("--since <window>", "Only include receipts from this window (e.g. 24h, 7d, or ISO date)", "24h")
191
- .option("--out <file>", "Where to write the attestation JSON", "audit.attest.json")
192
- .option("--workspace <path>", "Workspace to attest against (default: cwd)")
193
- .option("--receipts <file>", "Receipts JSONL to fold in", ".aarmos/receipts.jsonl")
194
- .option("--playbook <path>", "Include this playbook's hash + signature")
195
- .option("--policy <path>", "Path to aarmos.json (default: <workspace>/aarmos.json())")
196
- .option("--sign-key <pem>", "Ed25519 private key PEM — signs the bundle so auditors can verify offline")
197
- .action(async (opts) => {
198
- await runAttest({
199
- out: opts.out,
200
- since: opts.since,
201
- workspace: opts.workspace,
202
- receipts: opts.receipts,
203
- playbook: opts.playbook,
204
- policy: opts.policy,
205
- signKey: opts.signKey,
206
- });
1485
+ var attest = program.command("attest").description(
1486
+ "Export a signed audit bundle (receipts + policy hash + pack hashes + rootHash) \u2014 verifiable offline"
1487
+ ).option(
1488
+ "--since <window>",
1489
+ "Only include receipts from this window (e.g. 24h, 7d, or ISO date)",
1490
+ "24h"
1491
+ ).option("--out <file>", "Where to write the attestation JSON", "audit.attest.json").option("--workspace <path>", "Workspace to attest against (default: cwd)").option("--receipts <file>", "Receipts JSONL to fold in", ".aarmos/receipts.jsonl").option("--playbook <path>", "Include this playbook's hash + signature").option("--policy <path>", "Path to aarmos.json (default: <workspace>/aarmos.json())").option(
1492
+ "--sign-key <pem>",
1493
+ "Ed25519 private key PEM \u2014 signs the bundle so auditors can verify offline"
1494
+ ).action(async (opts) => {
1495
+ await runAttest({
1496
+ out: opts.out,
1497
+ since: opts.since,
1498
+ workspace: opts.workspace,
1499
+ receipts: opts.receipts,
1500
+ playbook: opts.playbook,
1501
+ policy: opts.policy,
1502
+ signKey: opts.signKey
1503
+ });
207
1504
  });
208
- attest
209
- .command("verify")
210
- .description("Verify an attestation bundle offline — recomputes rootHash and checks signature")
211
- .requiredOption("--file <path>", "Attestation JSON to verify")
212
- .option("--trusted-pubkey <key>", "Reject if signer is not this pubkey")
213
- .action(async (opts) => {
214
- await runAttestVerify({ file: opts.file, trustedPubkey: opts.trustedPubkey });
1505
+ attest.command("verify").description("Verify an attestation bundle offline \u2014 recomputes rootHash and checks signature").requiredOption("--file <path>", "Attestation JSON to verify").option("--trusted-pubkey <key>", "Reject if signer is not this pubkey").action(async (opts) => {
1506
+ await runAttestVerify({ file: opts.file, trustedPubkey: opts.trustedPubkey });
215
1507
  });
216
- // aarmos verify offline AVAR bundle verifier (AVAR spec §6).
217
- // Uses the same @aarmos/avar-core module the browser drop-zone at
218
- // /trust/verify uses, so results are bit-identical across runtimes.
219
- program
220
- .command("verify")
221
- .description("Verify an AVAR bundle offline (.avar.zip)")
222
- .argument("<bundle>", "Path to a .avar.zip bundle")
223
- .option("--json", "Emit the VerificationReport as JSON on stdout")
224
- .option("--quiet", "Suppress all human-readable output")
225
- .option("--strict", "Exit non-zero on 'valid-with-warnings'")
226
- .action(async (bundle, opts) => {
227
- const { runVerify } = await import("./commands/verify.js");
228
- const code = await runVerify({
229
- file: bundle,
230
- json: !!opts.json(),
231
- quiet: !!opts.quiet,
232
- strict: !!opts.strict,
233
- });
234
- process.exit(code);
1508
+ program.command("verify").description("Verify an AVAR bundle offline (.avar.zip)").argument("<bundle>", "Path to a .avar.zip bundle").option("--json", "Emit the VerificationReport as JSON on stdout").option("--quiet", "Suppress all human-readable output").option("--strict", "Exit non-zero on 'valid-with-warnings'").action(async (bundle, opts) => {
1509
+ const { runVerify } = await import("./verify-O6X7MFEE.js");
1510
+ const code = await runVerify({
1511
+ file: bundle,
1512
+ json: !!opts.json(),
1513
+ quiet: !!opts.quiet,
1514
+ strict: !!opts.strict
1515
+ });
1516
+ process.exit(code);
235
1517
  });
236
1518
  program.parseAsync(process.argv).catch((err) => {
237
- console.error("[aarmos] fatal:", err);
238
- process.exit(1);
1519
+ console.error("[aarmos] fatal:", err);
1520
+ process.exit(1);
239
1521
  });
240
- //# sourceMappingURL=index.js.map