@aarmos/cli 0.1.1 → 0.1.3

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 (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +393 -18
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Run your agent locally, across any protocol, with a signed receipt — in 60 seconds.
4
4
 
5
+ > **Not what you're looking for?** If you use the browser PWA at [aarmos.io](https://aarmos.io) and want it to reach your local files/shell/git, you want [`@aarmos/bridge`](https://www.npmjs.com/package/@aarmos/bridge) instead. This package is the terminal-native agent runtime.
6
+
5
7
  ```bash
6
8
  npm i -g @aarmos/cli
7
9
  aarmos init
@@ -16,7 +18,7 @@ aarmos run ./my-agent.js
16
18
  | `aarmos run <agent>` | Run agent under policy — writes AVAR receipt on every execution |
17
19
  | `aarmos proxy` | Transparent local HTTP proxy on 127.0.0.1 (any framework enrolls via `HTTPS_PROXY`) |
18
20
  | `aarmos daemon start|stop|status` | Manage background proxy daemon |
19
- | `aarmos verify <receipt>` | Discovery wrapper install standalone `avar` for full verification |
21
+ | `aarmos verify <receipt>` | Verify an AVAR receipt locally (signatures + hash chain, via `@aarmos/avar-core`). Works with the daemon stopped. Zero network. |
20
22
 
21
23
  ## Guarantees
22
24
 
package/dist/index.js CHANGED
@@ -299,29 +299,404 @@ function isRunning() {
299
299
  // src/commands/verify.ts
300
300
  import { Command as Command5 } from "commander";
301
301
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
302
- function verifyCommand() {
303
- return new Command5("verify").description(
304
- "Thin wrapper on the standalone `avar` binary for discovery. Install `avar` for the full verifier \u2014 it works with the Aarmos daemon stopped."
305
- ).argument("<receipt>", "Path to AVAR receipt").action(async (receipt) => {
306
- if (!existsSync5(receipt)) {
307
- console.error(`\u2717 receipt not found: ${receipt}`);
308
- process.exit(2);
302
+ import { unzipSync, strFromU8 } from "fflate";
303
+
304
+ // ../avar-core/src/canonicalize.ts
305
+ var NFC = (s) => s.normalize("NFC");
306
+ function encodeString(s) {
307
+ return JSON.stringify(NFC(s));
308
+ }
309
+ function encodeNumber(n) {
310
+ if (!Number.isFinite(n)) {
311
+ throw new Error(`avar-core/canonicalize: non-finite number (${n}) is forbidden.`);
312
+ }
313
+ return JSON.stringify(n);
314
+ }
315
+ function canonicalize(value) {
316
+ if (value === null) return "null";
317
+ if (typeof value === "boolean") return value ? "true" : "false";
318
+ if (typeof value === "number") return encodeNumber(value);
319
+ if (typeof value === "string") return encodeString(value);
320
+ if (typeof value === "undefined") {
321
+ throw new Error("avar-core/canonicalize: undefined is forbidden at value position.");
322
+ }
323
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
324
+ throw new Error(`avar-core/canonicalize: ${typeof value} is forbidden.`);
325
+ }
326
+ if (Array.isArray(value)) {
327
+ return "[" + value.map((v) => canonicalize(v)).join(",") + "]";
328
+ }
329
+ if (typeof value === "object") {
330
+ const obj = value;
331
+ const keys = [];
332
+ for (const k of Object.keys(obj)) {
333
+ if (obj[k] === void 0) continue;
334
+ keys.push(NFC(k));
335
+ }
336
+ keys.sort();
337
+ const parts = new Array(keys.length);
338
+ for (let i = 0; i < keys.length; i++) {
339
+ const k = keys[i];
340
+ const rawVal = obj[k] ?? findKey(obj, k);
341
+ parts[i] = encodeString(k) + ":" + canonicalize(rawVal);
342
+ }
343
+ return "{" + parts.join(",") + "}";
344
+ }
345
+ throw new Error(`avar-core/canonicalize: unsupported type ${typeof value}.`);
346
+ }
347
+ function findKey(obj, nfcKey) {
348
+ for (const k of Object.keys(obj)) {
349
+ if (NFC(k) === nfcKey) return obj[k];
350
+ }
351
+ return void 0;
352
+ }
353
+ function utf8(s) {
354
+ return new TextEncoder().encode(s);
355
+ }
356
+
357
+ // ../avar-core/src/hash.ts
358
+ var GENESIS_PREV_HASH = "0000000000000000000000000000000000000000000000000000000000000000";
359
+ function getSubtle() {
360
+ const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
361
+ if (!c || !c.subtle) {
362
+ throw new Error(
363
+ "avar-core: WebCrypto SubtleCrypto is unavailable. Requires Node 20+ or a modern browser."
364
+ );
365
+ }
366
+ return c.subtle;
367
+ }
368
+ async function sha256Hex(input) {
369
+ const bytes = typeof input === "string" ? utf8(input) : input;
370
+ const digest = await getSubtle().digest("SHA-256", bytes);
371
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
372
+ }
373
+ async function computeEntryHash(entry, prevHash) {
374
+ const withPrev = { ...entry, prevHash };
375
+ const {
376
+ entryHash: _e,
377
+ signature: _sig,
378
+ devicePubKey: _pk,
379
+ ...rest
380
+ } = withPrev;
381
+ void _e;
382
+ void _sig;
383
+ void _pk;
384
+ return sha256Hex(prevHash + "\n" + canonicalize(rest));
385
+ }
386
+ async function computeStepHash(step, prevStepHash) {
387
+ const asRec = step;
388
+ const { stepHash: _s, prevStepHash: _p, ...rest } = asRec;
389
+ void _s;
390
+ void _p;
391
+ const withPrev = { ...rest, prevStepHash };
392
+ return sha256Hex(prevStepHash + "\n" + canonicalize(withPrev));
393
+ }
394
+
395
+ // ../avar-core/src/signature.ts
396
+ function getSubtle2() {
397
+ const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
398
+ if (!c || !c.subtle) {
399
+ throw new Error("avar-core: WebCrypto SubtleCrypto is unavailable.");
400
+ }
401
+ return c.subtle;
402
+ }
403
+ function b64uDecode(s) {
404
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
405
+ const bin = typeof atob === "function" ? atob(b64) : globalThis.Buffer.from(b64, "base64").toString("binary");
406
+ const out = new Uint8Array(bin.length);
407
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
408
+ return out;
409
+ }
410
+ async function verifySignature(signedBody, signatureB64u, publicKeyB64u) {
411
+ try {
412
+ const raw = b64uDecode(publicKeyB64u);
413
+ const key = await getSubtle2().importKey("raw", raw, "Ed25519", false, [
414
+ "verify"
415
+ ]);
416
+ const sig = b64uDecode(signatureB64u);
417
+ const bytes = utf8(canonicalize(signedBody));
418
+ return await getSubtle2().verify(
419
+ "Ed25519",
420
+ key,
421
+ sig,
422
+ bytes
423
+ );
424
+ } catch {
425
+ return false;
426
+ }
427
+ }
428
+ async function computeDeviceFingerprint(publicKeyB64u) {
429
+ const hex = await sha256Hex(utf8(publicKeyB64u));
430
+ return hex.slice(0, 12);
431
+ }
432
+ function signedBodyOf(entry) {
433
+ const { signature: _sig, devicePubKey: _pk, ...rest } = entry;
434
+ void _sig;
435
+ void _pk;
436
+ return rest;
437
+ }
438
+
439
+ // ../avar-core/src/verify.ts
440
+ var SPEC_VERSION = "avar/1";
441
+ async function verifyBundle(bundle) {
442
+ const issues = [];
443
+ let formatOk = true;
444
+ const specV = bundle.specVersion.trim();
445
+ if (specV !== SPEC_VERSION) {
446
+ formatOk = false;
447
+ issues.push({
448
+ index: -1,
449
+ kind: "spec-version-mismatch",
450
+ detail: `Expected "${SPEC_VERSION}", got "${specV}".`
451
+ });
452
+ }
453
+ if (bundle.manifest.format !== SPEC_VERSION) {
454
+ formatOk = false;
455
+ issues.push({
456
+ index: -1,
457
+ kind: "manifest-invalid",
458
+ detail: `manifest.format expected "${SPEC_VERSION}", got "${bundle.manifest.format}".`
459
+ });
460
+ }
461
+ const actualSha = await sha256Hex(bundle.entriesNdjsonBytes);
462
+ const entriesSha256Ok = actualSha === bundle.manifest.entriesSha256;
463
+ if (!entriesSha256Ok) {
464
+ issues.push({
465
+ index: -1,
466
+ kind: "entries-sha256-mismatch",
467
+ detail: `Expected ${bundle.manifest.entriesSha256}, got ${actualSha}.`
468
+ });
469
+ }
470
+ const entries = bundle.entries;
471
+ const entryCount = entries.length;
472
+ let signaturesOk = true;
473
+ let fingerprintsOk = true;
474
+ let signedCount = 0;
475
+ let unsignedCount = 0;
476
+ for (let i = 0; i < entries.length; i++) {
477
+ const e = entries[i];
478
+ const hasSig = typeof e.signature === "string" && typeof e.devicePubKey === "string";
479
+ if (!hasSig) {
480
+ unsignedCount++;
481
+ continue;
309
482
  }
310
- const raw = readFileSync4(receipt, "utf8");
483
+ signedCount++;
484
+ if (typeof e.deviceFingerprint === "string") {
485
+ const expected = await computeDeviceFingerprint(e.devicePubKey);
486
+ if (expected !== e.deviceFingerprint) {
487
+ fingerprintsOk = false;
488
+ issues.push({
489
+ index: i,
490
+ kind: "fingerprint-mismatch",
491
+ detail: `Expected ${expected}, got ${e.deviceFingerprint}.`
492
+ });
493
+ }
494
+ }
495
+ const ok = await verifySignature(signedBodyOf(e), e.signature, e.devicePubKey);
496
+ if (!ok) {
497
+ signaturesOk = false;
498
+ issues.push({ index: i, kind: "signature-invalid" });
499
+ }
500
+ }
501
+ let chainOk = true;
502
+ let unchainedCount = 0;
503
+ let expectedPrev = GENESIS_PREV_HASH;
504
+ for (let i = 0; i < entries.length; i++) {
505
+ const e = entries[i];
506
+ const isLegacyUnchained = !e.entryHash || !e.prevHash;
507
+ if (isLegacyUnchained) {
508
+ unchainedCount++;
509
+ expectedPrev = GENESIS_PREV_HASH;
510
+ continue;
511
+ }
512
+ if (e.prevHash !== expectedPrev) {
513
+ chainOk = false;
514
+ issues.push({
515
+ index: i,
516
+ kind: "chain-broken",
517
+ detail: `prevHash mismatch at entry ${i}.`
518
+ });
519
+ }
520
+ const recomputed = await computeEntryHash(e, e.prevHash);
521
+ if (recomputed !== e.entryHash) {
522
+ chainOk = false;
523
+ issues.push({
524
+ index: i,
525
+ kind: "chain-broken",
526
+ detail: `entryHash mismatch at entry ${i} (body modified after signing).`
527
+ });
528
+ }
529
+ expectedPrev = e.entryHash;
530
+ }
531
+ const perStepChainOk = await verifyAllStepChains(entries, issues);
532
+ const chainHead = computeChainHead(entries);
533
+ const anyHardFail = !formatOk || !entriesSha256Ok || !chainOk || !perStepChainOk || !signaturesOk || !fingerprintsOk;
534
+ const verdict = anyHardFail ? "invalid" : unsignedCount > 0 || unchainedCount > 0 ? "valid-with-warnings" : "valid";
535
+ return {
536
+ formatOk,
537
+ entriesSha256Ok,
538
+ chainOk,
539
+ perStepChainOk,
540
+ signaturesOk,
541
+ fingerprintsOk,
542
+ entryCount,
543
+ signedCount,
544
+ unsignedCount,
545
+ unchainedCount,
546
+ chainHead,
547
+ issues,
548
+ verdict
549
+ };
550
+ }
551
+ async function verifyAllStepChains(entries, issues) {
552
+ let ok = true;
553
+ for (let i = 0; i < entries.length; i++) {
554
+ const e = entries[i];
555
+ if (!Array.isArray(e.steps) || e.steps.length === 0) continue;
556
+ const anyChained = e.steps.some(hasChainFields);
557
+ const allChained = e.steps.every(hasChainFields);
558
+ if (anyChained && !allChained) {
559
+ ok = false;
560
+ issues.push({ index: i, kind: "partial-step-chain" });
561
+ continue;
562
+ }
563
+ if (!anyChained) continue;
564
+ let prev = "step-genesis:0000000000000000000000000000000000000000000000000000000000000000";
565
+ for (let j = 0; j < e.steps.length; j++) {
566
+ const s = e.steps[j];
567
+ if (s.prevStepHash !== prev) {
568
+ ok = false;
569
+ issues.push({
570
+ index: i,
571
+ kind: "step-chain-broken",
572
+ detail: `entry ${i} step ${j}: prevStepHash mismatch.`
573
+ });
574
+ }
575
+ const recomputed = await computeStepHash(s, s.prevStepHash);
576
+ if (recomputed !== s.stepHash) {
577
+ ok = false;
578
+ issues.push({
579
+ index: i,
580
+ kind: "step-chain-broken",
581
+ detail: `entry ${i} step ${j}: stepHash mismatch (step body modified).`
582
+ });
583
+ }
584
+ prev = s.stepHash;
585
+ }
586
+ }
587
+ return ok;
588
+ }
589
+ function hasChainFields(s) {
590
+ return typeof s.prevStepHash === "string" && typeof s.stepHash === "string";
591
+ }
592
+ function computeChainHead(entries) {
593
+ for (let i = entries.length - 1; i >= 0; i--) {
594
+ const e = entries[i];
595
+ if (e.entryHash) return { entryHash: e.entryHash, index: i };
596
+ }
597
+ return { entryHash: "", index: -1 };
598
+ }
599
+
600
+ // src/commands/verify.ts
601
+ async function runVerify(opts) {
602
+ const { file, json = false, quiet = false } = opts;
603
+ if (!existsSync5(file)) {
604
+ if (!quiet) process.stderr.write(`\u2717 receipt not found: ${file}
605
+ `);
606
+ return 2;
607
+ }
608
+ const bytes = readFileSync4(file);
609
+ const isZip = looksLikeZip(bytes);
610
+ if (!isZip) {
311
611
  try {
312
- const parsed = JSON.parse(raw);
612
+ const parsed = JSON.parse(bytes.toString("utf8"));
313
613
  const entries = Array.isArray(parsed.entries) ? parsed.entries.length : 0;
314
- console.log(`\u2713 receipt parsed (${entries} entr${entries === 1 ? "y" : "ies"})`);
315
- console.log(` spec_version: ${parsed.spec_version ?? "unknown"}`);
316
- console.log(` signature_alg: ${parsed.signature_alg ?? "unknown"}`);
317
- console.log(
318
- "\nFor full signature + hash-chain verification, install the standalone `avar` binary:"
319
- );
320
- console.log(" https://www.aarmos.io/docs/avar-spec");
614
+ if (json) {
615
+ process.stdout.write(
616
+ JSON.stringify({
617
+ verdict: "parsed-only",
618
+ format: "json",
619
+ entryCount: entries,
620
+ note: "JSON receipt parsed; full verification requires an .avar.zip bundle."
621
+ }) + "\n"
622
+ );
623
+ } else if (!quiet) {
624
+ process.stdout.write(`\u2713 receipt parsed (${entries} entr${entries === 1 ? "y" : "ies"})
625
+ `);
626
+ process.stdout.write(` spec_version: ${parsed.spec_version ?? "unknown"}
627
+ `);
628
+ process.stdout.write(` signature_alg: ${parsed.signature_alg ?? "unknown"}
629
+ `);
630
+ process.stdout.write(
631
+ "\nNote: JSON receipts are parse-only. For full signature + hash-chain\nverification, export an .avar.zip bundle.\n"
632
+ );
633
+ }
634
+ return 0;
321
635
  } catch (err) {
322
- console.error(`\u2717 invalid JSON: ${String(err)}`);
323
- process.exit(1);
636
+ if (!quiet) process.stderr.write(`\u2717 invalid JSON: ${String(err)}
637
+ `);
638
+ return 1;
639
+ }
640
+ }
641
+ let bundle;
642
+ try {
643
+ bundle = parseBundleBytes(bytes);
644
+ } catch (err) {
645
+ if (!quiet) process.stderr.write(`\u2717 malformed .avar.zip: ${String(err)}
646
+ `);
647
+ return 1;
648
+ }
649
+ const report = await verifyBundle(bundle);
650
+ const failed = report.verdict === "invalid";
651
+ if (json) {
652
+ process.stdout.write(JSON.stringify(report) + "\n");
653
+ } else if (!quiet) {
654
+ const mark = failed ? "\u2717" : "\u2713";
655
+ process.stdout.write(`${mark} verdict: ${report.verdict}
656
+ `);
657
+ process.stdout.write(` entries: ${report.entryCount}
658
+ `);
659
+ process.stdout.write(` signed: ${report.signedCount}
660
+ `);
661
+ process.stdout.write(` unsigned: ${report.unsignedCount}
662
+ `);
663
+ process.stdout.write(` unchained: ${report.unchainedCount}
664
+ `);
665
+ if (report.issues.length) {
666
+ process.stdout.write(` issues:
667
+ `);
668
+ for (const issue of report.issues) {
669
+ process.stdout.write(` - [${issue.kind}] ${issue.detail}
670
+ `);
671
+ }
324
672
  }
673
+ process.stdout.write("\nverified locally \u2014 no Aarmos service was contacted.\n");
674
+ }
675
+ return failed ? 1 : 0;
676
+ }
677
+ function looksLikeZip(bytes) {
678
+ return bytes.length >= 4 && bytes[0] === 80 && bytes[1] === 75 && bytes[2] === 3 && bytes[3] === 4;
679
+ }
680
+ function parseBundleBytes(bytes) {
681
+ const files = unzipSync(bytes);
682
+ const requireFile = (name) => {
683
+ const f = files[name];
684
+ if (!f) throw new Error(`missing ${name} in bundle`);
685
+ return f;
686
+ };
687
+ const specVersion = strFromU8(requireFile("SPEC-VERSION")).trim();
688
+ const manifest = JSON.parse(strFromU8(requireFile("manifest.json")));
689
+ const pubkeys = JSON.parse(strFromU8(requireFile("pubkeys.json")));
690
+ const entriesNdjsonBytes = requireFile("entries.ndjson");
691
+ const entries = strFromU8(entriesNdjsonBytes).split("\n").filter((l) => l.length > 0).map((l) => JSON.parse(l));
692
+ return { specVersion, manifest, entriesNdjsonBytes, entries, pubkeys };
693
+ }
694
+ function verifyCommand() {
695
+ return new Command5("verify").description(
696
+ "Verify an AVAR receipt (.avar.zip bundle or legacy .json) locally \u2014 signatures + hash chain via @aarmos/avar-core. Works with the Aarmos daemon stopped. Zero network."
697
+ ).argument("<receipt>", "Path to AVAR receipt (.avar.zip or .json)").option("--json", "Emit machine-readable JSON report on stdout").option("--quiet", "Suppress non-error output; exit code carries the verdict").action(async (receipt, opts) => {
698
+ const rc = await runVerify({ file: receipt, json: opts.json, quiet: opts.quiet });
699
+ process.exit(rc);
325
700
  });
326
701
  }
327
702
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aarmos/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Aarmos CLI — run any agent locally, across any protocol (MCP, OpenAPI, deep-link), with a signed AVAR receipt on every execution. Sovereign runtime, universal tool gateway, verifiable governance.",
5
5
  "keywords": [
6
6
  "aarmos",
@@ -36,14 +36,16 @@
36
36
  "author": "Aarmatix LLC",
37
37
  "license": "Apache-2.0",
38
38
  "dependencies": {
39
+ "@aarmos/avar-core": "^1.0.0-rc.3",
39
40
  "commander": "^12.1.0",
41
+ "fflate": "^0.8.3",
40
42
  "zod": "^3.23.8"
41
43
  },
42
44
  "devDependencies": {
43
45
  "@types/node": "^22.5.0",
44
- "tsup": "^8.3.0",
46
+ "tsup": "^8.5.1",
45
47
  "tsx": "^4.19.0",
46
- "typescript": "^5.6.2"
48
+ "typescript": "^6.0.3"
47
49
  },
48
50
  "engines": {
49
51
  "node": ">=18.17"