@aarmos/cli 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +923 -18
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ aarmos run ./my-agent.js
|
|
|
18
18
|
| `aarmos run <agent>` | Run agent under policy — writes AVAR receipt on every execution |
|
|
19
19
|
| `aarmos proxy` | Transparent local HTTP proxy on 127.0.0.1 (any framework enrolls via `HTTPS_PROXY`) |
|
|
20
20
|
| `aarmos daemon start|stop|status` | Manage background proxy daemon |
|
|
21
|
-
| `aarmos verify <receipt>` |
|
|
21
|
+
| `aarmos verify <receipt>` | Verify an AVAR receipt locally (signatures + hash chain, via `@aarmos/avar-core`). Works with the daemon stopped. Zero network. |
|
|
22
22
|
|
|
23
23
|
## Guarantees
|
|
24
24
|
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command9 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -299,34 +299,936 @@ 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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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 b642 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
|
|
405
|
+
const bin = typeof atob === "function" ? atob(b642) : globalThis.Buffer.from(b642, "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;
|
|
482
|
+
}
|
|
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
|
+
// ../avar-core/src/errors.ts
|
|
601
|
+
var SPEC_URL = "https://aarmos.io/docs/avar-spec";
|
|
602
|
+
var VERIFY_URL = "https://aarmos.io/trust/verify";
|
|
603
|
+
var QUICKSTART_URL = "https://aarmos.io/docs/quickstart";
|
|
604
|
+
var AARMOS_ERROR_TEMPLATES = {
|
|
605
|
+
BUNDLE_OVERSIZED: {
|
|
606
|
+
message: "That bundle is larger than the 25 MB browser limit.",
|
|
607
|
+
hint: "Run `aarmos verify <bundle>` locally instead \u2014 the CLI has no size cap.",
|
|
608
|
+
docsUrl: QUICKSTART_URL
|
|
609
|
+
},
|
|
610
|
+
BUNDLE_NOT_ZIP: {
|
|
611
|
+
message: "That file is not a valid .avar.zip.",
|
|
612
|
+
hint: "Bundles are zip archives. Rename or re-export the file, then try again.",
|
|
613
|
+
docsUrl: SPEC_URL
|
|
614
|
+
},
|
|
615
|
+
BUNDLE_MISSING_FILES: {
|
|
616
|
+
message: "Bundle is missing one or more required files.",
|
|
617
|
+
hint: "A valid AVAR bundle contains SPEC-VERSION, manifest.json, entries.ndjson, and pubkeys.json.",
|
|
618
|
+
docsUrl: SPEC_URL
|
|
619
|
+
},
|
|
620
|
+
BUNDLE_INVALID_JSON: {
|
|
621
|
+
message: "manifest.json or pubkeys.json is not valid JSON.",
|
|
622
|
+
hint: "The file exists but cannot be parsed. Re-export from the CLI to regenerate a clean bundle.",
|
|
623
|
+
docsUrl: SPEC_URL
|
|
624
|
+
},
|
|
625
|
+
BUNDLE_INVALID_NDJSON: {
|
|
626
|
+
message: "entries.ndjson contains a malformed line.",
|
|
627
|
+
hint: "Each line must be a self-contained JSON entry. One corrupt line usually means the file was edited by hand.",
|
|
628
|
+
docsUrl: SPEC_URL
|
|
629
|
+
},
|
|
630
|
+
BUNDLE_SPEC_UNSUPPORTED: {
|
|
631
|
+
message: "This bundle was produced by a spec version this verifier does not support.",
|
|
632
|
+
hint: "Update to the latest verifier (`npm i -g @aarmos/avar-core`) or re-export with a matching spec version.",
|
|
633
|
+
docsUrl: SPEC_URL
|
|
634
|
+
},
|
|
635
|
+
BUNDLE_EMPTY: {
|
|
636
|
+
message: "Bundle contains no entries.",
|
|
637
|
+
hint: "An empty ledger is unusual \u2014 check that the run actually produced receipts before exporting.",
|
|
638
|
+
docsUrl: SPEC_URL
|
|
639
|
+
},
|
|
640
|
+
SPEC_VERSION_MISMATCH: {
|
|
641
|
+
message: "This bundle uses a spec version this verifier does not recognize.",
|
|
642
|
+
hint: "Update the verifier (`npm i -g @aarmos/avar-core`) or re-export the bundle with matching spec version.",
|
|
643
|
+
docsUrl: SPEC_URL
|
|
644
|
+
},
|
|
645
|
+
ENTRIES_SHA256_MISMATCH: {
|
|
646
|
+
message: "entries.ndjson has been modified since the bundle was signed.",
|
|
647
|
+
hint: "The envelope hash in manifest.json no longer matches the file. Do not trust this bundle \u2014 re-export from the CLI.",
|
|
648
|
+
docsUrl: VERIFY_URL
|
|
649
|
+
},
|
|
650
|
+
SIGNATURE_MISMATCH: {
|
|
651
|
+
message: "One or more entry signatures do not match the embedded public keys.",
|
|
652
|
+
hint: "The bundle was modified after signing, or the wrong pubkeys.json is bundled.",
|
|
653
|
+
docsUrl: VERIFY_URL
|
|
654
|
+
},
|
|
655
|
+
FINGERPRINT_MISMATCH: {
|
|
656
|
+
message: "Device fingerprint on one or more entries does not match its public key.",
|
|
657
|
+
hint: "The entry claims a device whose public key does not hash to the declared fingerprint. Treat this bundle as untrusted.",
|
|
658
|
+
docsUrl: VERIFY_URL
|
|
659
|
+
},
|
|
660
|
+
CHAIN_BROKEN: {
|
|
661
|
+
message: "The per-entry hash chain is broken.",
|
|
662
|
+
hint: "An entry was inserted, removed, or edited after signing. The break index is shown in the issues list.",
|
|
663
|
+
docsUrl: VERIFY_URL
|
|
664
|
+
},
|
|
665
|
+
STEP_CHAIN_BROKEN: {
|
|
666
|
+
message: "The per-step hash chain inside one or more entries is broken.",
|
|
667
|
+
hint: "A decision or tool step was tampered with. The affected entry index is in the issues list.",
|
|
668
|
+
docsUrl: VERIFY_URL
|
|
669
|
+
},
|
|
670
|
+
MANIFEST_INVALID: {
|
|
671
|
+
message: "manifest.json is missing required fields or uses an unsupported format.",
|
|
672
|
+
hint: "Re-export the bundle from the CLI \u2014 do not edit manifest.json by hand.",
|
|
673
|
+
docsUrl: SPEC_URL
|
|
674
|
+
},
|
|
675
|
+
FILE_NOT_FOUND: {
|
|
676
|
+
message: "Receipt not found at the given path.",
|
|
677
|
+
hint: "Check the path \u2014 a common cause is running from a different working directory than expected.",
|
|
678
|
+
docsUrl: QUICKSTART_URL
|
|
679
|
+
},
|
|
680
|
+
FILE_UNREADABLE: {
|
|
681
|
+
message: "Receipt exists but could not be read.",
|
|
682
|
+
hint: "Check filesystem permissions on the file.",
|
|
683
|
+
docsUrl: QUICKSTART_URL
|
|
684
|
+
},
|
|
685
|
+
UNKNOWN: {
|
|
686
|
+
message: "Something went wrong while verifying that bundle.",
|
|
687
|
+
hint: "Try again with a fresh export. If the error persists, run `aarmos verify` locally to see the raw error.",
|
|
688
|
+
docsUrl: QUICKSTART_URL
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
function aarmosError(code, overrides = {}) {
|
|
692
|
+
const base = AARMOS_ERROR_TEMPLATES[code];
|
|
693
|
+
return {
|
|
694
|
+
code,
|
|
695
|
+
message: overrides.message ?? base.message,
|
|
696
|
+
hint: overrides.hint ?? base.hint,
|
|
697
|
+
docsUrl: overrides.docsUrl ?? base.docsUrl
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function formatAarmosErrorLine(err) {
|
|
701
|
+
return `\u2717 ${err.code}: ${err.message}
|
|
702
|
+
Next: ${err.hint}
|
|
703
|
+
Docs: ${err.docsUrl}`;
|
|
704
|
+
}
|
|
705
|
+
function classifyReport(report) {
|
|
706
|
+
if (report.verdict !== "invalid") return null;
|
|
707
|
+
if (!report.entriesSha256Ok) return aarmosError("ENTRIES_SHA256_MISMATCH");
|
|
708
|
+
if (!report.signaturesOk) return aarmosError("SIGNATURE_MISMATCH");
|
|
709
|
+
if (!report.fingerprintsOk) return aarmosError("FINGERPRINT_MISMATCH");
|
|
710
|
+
if (!report.chainOk) return aarmosError("CHAIN_BROKEN");
|
|
711
|
+
if (!report.perStepChainOk) return aarmosError("STEP_CHAIN_BROKEN");
|
|
712
|
+
if (!report.formatOk) {
|
|
713
|
+
const specIssue = report.issues.find((i) => i.kind === "spec-version-mismatch");
|
|
714
|
+
if (specIssue) {
|
|
715
|
+
return aarmosError("SPEC_VERSION_MISMATCH", {
|
|
716
|
+
message: specIssue.detail ? `Spec mismatch \u2014 ${specIssue.detail}` : void 0
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
return aarmosError("MANIFEST_INVALID");
|
|
720
|
+
}
|
|
721
|
+
return aarmosError("UNKNOWN");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/commands/verify.ts
|
|
725
|
+
async function runVerify(opts) {
|
|
726
|
+
const { file, json = false, quiet = false } = opts;
|
|
727
|
+
const emitError = (err, rc) => {
|
|
728
|
+
if (json) {
|
|
729
|
+
process.stdout.write(JSON.stringify({ verdict: "invalid", error: err }) + "\n");
|
|
730
|
+
} else if (!quiet) {
|
|
731
|
+
process.stderr.write(formatAarmosErrorLine(err) + "\n");
|
|
309
732
|
}
|
|
310
|
-
|
|
733
|
+
return rc;
|
|
734
|
+
};
|
|
735
|
+
if (!existsSync5(file)) {
|
|
736
|
+
return emitError(aarmosError("FILE_NOT_FOUND", { message: `Receipt not found: ${file}` }), 2);
|
|
737
|
+
}
|
|
738
|
+
let bytes;
|
|
739
|
+
try {
|
|
740
|
+
bytes = readFileSync4(file);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
return emitError(
|
|
743
|
+
aarmosError("FILE_UNREADABLE", {
|
|
744
|
+
message: err instanceof Error ? err.message : String(err)
|
|
745
|
+
}),
|
|
746
|
+
1
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
const isZip = looksLikeZip(bytes);
|
|
750
|
+
if (!isZip) {
|
|
311
751
|
try {
|
|
312
|
-
const parsed = JSON.parse(
|
|
752
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
313
753
|
const entries = Array.isArray(parsed.entries) ? parsed.entries.length : 0;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
754
|
+
if (json) {
|
|
755
|
+
process.stdout.write(
|
|
756
|
+
JSON.stringify({
|
|
757
|
+
verdict: "parsed-only",
|
|
758
|
+
format: "json",
|
|
759
|
+
entryCount: entries,
|
|
760
|
+
note: "JSON receipt parsed; full verification requires an .avar.zip bundle."
|
|
761
|
+
}) + "\n"
|
|
762
|
+
);
|
|
763
|
+
} else if (!quiet) {
|
|
764
|
+
process.stdout.write(`\u2713 receipt parsed (${entries} entr${entries === 1 ? "y" : "ies"})
|
|
765
|
+
`);
|
|
766
|
+
process.stdout.write(` spec_version: ${parsed.spec_version ?? "unknown"}
|
|
767
|
+
`);
|
|
768
|
+
process.stdout.write(` signature_alg: ${parsed.signature_alg ?? "unknown"}
|
|
769
|
+
`);
|
|
770
|
+
process.stdout.write(
|
|
771
|
+
"\nNote: JSON receipts are parse-only. For full signature + hash-chain\nverification, export an .avar.zip bundle.\n"
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
return 0;
|
|
775
|
+
} catch (err) {
|
|
776
|
+
return emitError(
|
|
777
|
+
aarmosError("BUNDLE_INVALID_JSON", {
|
|
778
|
+
message: `invalid JSON receipt: ${err instanceof Error ? err.message : String(err)}`
|
|
779
|
+
}),
|
|
780
|
+
1
|
|
319
781
|
);
|
|
320
|
-
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
let bundle;
|
|
785
|
+
try {
|
|
786
|
+
bundle = parseBundleBytes(bytes);
|
|
787
|
+
} catch (err) {
|
|
788
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
789
|
+
let classified2;
|
|
790
|
+
if (/missing /i.test(raw)) {
|
|
791
|
+
classified2 = aarmosError("BUNDLE_MISSING_FILES", { message: raw });
|
|
792
|
+
} else if (/invalid zip|central directory|not a zip/i.test(raw)) {
|
|
793
|
+
classified2 = aarmosError("BUNDLE_NOT_ZIP", { message: raw });
|
|
794
|
+
} else if (/ndjson|entries\.ndjson/i.test(raw)) {
|
|
795
|
+
classified2 = aarmosError("BUNDLE_INVALID_NDJSON", { message: raw });
|
|
796
|
+
} else if (/JSON|Unexpected token/i.test(raw)) {
|
|
797
|
+
classified2 = aarmosError("BUNDLE_INVALID_JSON", { message: raw });
|
|
798
|
+
} else {
|
|
799
|
+
classified2 = aarmosError("UNKNOWN", { message: `malformed .avar.zip: ${raw}` });
|
|
800
|
+
}
|
|
801
|
+
return emitError(classified2, 1);
|
|
802
|
+
}
|
|
803
|
+
const report = await verifyBundle(bundle);
|
|
804
|
+
const failed = report.verdict === "invalid";
|
|
805
|
+
const classified = classifyReport(report);
|
|
806
|
+
if (json) {
|
|
807
|
+
process.stdout.write(
|
|
808
|
+
JSON.stringify(classified ? { ...report, error: classified } : report) + "\n"
|
|
809
|
+
);
|
|
810
|
+
} else if (!quiet) {
|
|
811
|
+
const mark = failed ? "\u2717" : "\u2713";
|
|
812
|
+
process.stdout.write(`${mark} verdict: ${report.verdict}
|
|
813
|
+
`);
|
|
814
|
+
process.stdout.write(` entries: ${report.entryCount}
|
|
815
|
+
`);
|
|
816
|
+
process.stdout.write(` signed: ${report.signedCount}
|
|
817
|
+
`);
|
|
818
|
+
process.stdout.write(` unsigned: ${report.unsignedCount}
|
|
819
|
+
`);
|
|
820
|
+
process.stdout.write(` unchained: ${report.unchainedCount}
|
|
821
|
+
`);
|
|
822
|
+
if (report.issues.length) {
|
|
823
|
+
process.stdout.write(` issues:
|
|
824
|
+
`);
|
|
825
|
+
for (const issue of report.issues) {
|
|
826
|
+
process.stdout.write(` - [${issue.kind}] ${issue.detail ?? ""}
|
|
827
|
+
`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (classified) {
|
|
831
|
+
process.stderr.write("\n" + formatAarmosErrorLine(classified) + "\n");
|
|
832
|
+
} else {
|
|
833
|
+
process.stdout.write("\nverified locally \u2014 no Aarmos service was contacted.\n");
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return failed ? 1 : 0;
|
|
837
|
+
}
|
|
838
|
+
function looksLikeZip(bytes) {
|
|
839
|
+
return bytes.length >= 4 && bytes[0] === 80 && bytes[1] === 75 && bytes[2] === 3 && bytes[3] === 4;
|
|
840
|
+
}
|
|
841
|
+
function parseBundleBytes(bytes) {
|
|
842
|
+
const files = unzipSync(bytes);
|
|
843
|
+
const requireFile = (name) => {
|
|
844
|
+
const f = files[name];
|
|
845
|
+
if (!f) throw new Error(`missing ${name} in bundle`);
|
|
846
|
+
return f;
|
|
847
|
+
};
|
|
848
|
+
const specVersion = strFromU8(requireFile("SPEC-VERSION")).trim();
|
|
849
|
+
const manifest = JSON.parse(strFromU8(requireFile("manifest.json")));
|
|
850
|
+
const pubkeys = JSON.parse(strFromU8(requireFile("pubkeys.json")));
|
|
851
|
+
const entriesNdjsonBytes = requireFile("entries.ndjson");
|
|
852
|
+
const entries = strFromU8(entriesNdjsonBytes).split("\n").filter((l) => l.length > 0).map((l) => JSON.parse(l));
|
|
853
|
+
return { specVersion, manifest, entriesNdjsonBytes, entries, pubkeys };
|
|
854
|
+
}
|
|
855
|
+
function verifyCommand() {
|
|
856
|
+
return new Command5("verify").description(
|
|
857
|
+
"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."
|
|
858
|
+
).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) => {
|
|
859
|
+
const rc = await runVerify({ file: receipt, json: opts.json, quiet: opts.quiet });
|
|
860
|
+
process.exit(rc);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/commands/lint.ts
|
|
865
|
+
import { Command as Command6 } from "commander";
|
|
866
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, statSync, readdirSync } from "fs";
|
|
867
|
+
import { join as join4, extname } from "path";
|
|
868
|
+
|
|
869
|
+
// src/lib/asp-compile.ts
|
|
870
|
+
import { parse as parseYaml } from "yaml";
|
|
871
|
+
import { createHash as createHash3, createPrivateKey, sign as edSign, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
|
|
872
|
+
function canonicalize2(value) {
|
|
873
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
874
|
+
if (Array.isArray(value)) return "[" + value.map(canonicalize2).join(",") + "]";
|
|
875
|
+
const obj = value;
|
|
876
|
+
const keys = Object.keys(obj).sort();
|
|
877
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize2(obj[k])).join(",") + "}";
|
|
878
|
+
}
|
|
879
|
+
function parseAspYaml(source) {
|
|
880
|
+
const doc = parseYaml(source);
|
|
881
|
+
if (!doc || typeof doc !== "object") throw new Error("ASP YAML must be a mapping");
|
|
882
|
+
return doc;
|
|
883
|
+
}
|
|
884
|
+
function compileAsp(doc) {
|
|
885
|
+
const errors = [];
|
|
886
|
+
const v = doc.version ?? 1;
|
|
887
|
+
if (v !== 1) errors.push(`version must be 1 (got ${v})`);
|
|
888
|
+
const name = doc.metadata?.issuer?.name;
|
|
889
|
+
const pubKey = doc.metadata?.issuer?.pubKey;
|
|
890
|
+
if (!name) errors.push("metadata.issuer.name is required");
|
|
891
|
+
if (!pubKey) errors.push("metadata.issuer.pubKey is required (base64 raw 32B ed25519)");
|
|
892
|
+
const issuedAt = doc.metadata?.issuedAt;
|
|
893
|
+
const expiresAt = doc.metadata?.expiresAt;
|
|
894
|
+
if (!issuedAt) errors.push("metadata.issuedAt is required (ISO 8601)");
|
|
895
|
+
if (!expiresAt) errors.push("metadata.expiresAt is required (ISO 8601)");
|
|
896
|
+
if (issuedAt && Number.isNaN(Date.parse(issuedAt)))
|
|
897
|
+
errors.push(`metadata.issuedAt is not a valid ISO date: ${issuedAt}`);
|
|
898
|
+
if (expiresAt && Number.isNaN(Date.parse(expiresAt)))
|
|
899
|
+
errors.push(`metadata.expiresAt is not a valid ISO date: ${expiresAt}`);
|
|
900
|
+
if (issuedAt && expiresAt && Date.parse(expiresAt) <= Date.parse(issuedAt))
|
|
901
|
+
errors.push("metadata.expiresAt must be after metadata.issuedAt");
|
|
902
|
+
if (errors.length) return { ok: false, errors };
|
|
903
|
+
const policy = {};
|
|
904
|
+
if (doc.allow?.servers?.length) policy.allowedServers = [...doc.allow.servers];
|
|
905
|
+
if (doc.allow?.tools?.length) policy.allowedTools = [...doc.allow.tools];
|
|
906
|
+
if (doc.deny?.tools?.length) policy.deniedTools = [...doc.deny.tools];
|
|
907
|
+
if (doc.scopes && Object.keys(doc.scopes).length) policy.allowedScopes = { ...doc.scopes };
|
|
908
|
+
if (typeof doc.kill_switch?.default === "boolean")
|
|
909
|
+
policy.killSwitchDefault = doc.kill_switch.default;
|
|
910
|
+
if (doc.redaction?.length) policy.redactionRules = doc.redaction.map((r) => ({ ...r }));
|
|
911
|
+
if (doc.autonomous_scopes?.length)
|
|
912
|
+
policy.autonomousScopes = doc.autonomous_scopes.map((s) => ({
|
|
913
|
+
agentId: s.agent_id,
|
|
914
|
+
tools: [...s.tools],
|
|
915
|
+
maxUsdPerDay: s.max_usd_per_day,
|
|
916
|
+
maxStepsPerRun: s.max_steps_per_run,
|
|
917
|
+
expiresAt: s.expires_at
|
|
918
|
+
}));
|
|
919
|
+
const body = {
|
|
920
|
+
version: 1,
|
|
921
|
+
issuer: { name, pubKey },
|
|
922
|
+
issuedAt,
|
|
923
|
+
expiresAt,
|
|
924
|
+
policy
|
|
925
|
+
};
|
|
926
|
+
return { ok: true, body, canonical: canonicalize2(body) };
|
|
927
|
+
}
|
|
928
|
+
function compileAspYamlToCanonicalJson(source) {
|
|
929
|
+
let doc;
|
|
930
|
+
try {
|
|
931
|
+
doc = parseAspYaml(source);
|
|
932
|
+
} catch (err) {
|
|
933
|
+
return { ok: false, errors: [`YAML parse error: ${err.message}`] };
|
|
934
|
+
}
|
|
935
|
+
return compileAsp(doc);
|
|
936
|
+
}
|
|
937
|
+
var fromB64u = (s) => {
|
|
938
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
939
|
+
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
|
|
940
|
+
};
|
|
941
|
+
var b64 = (buf) => Buffer.from(buf).toString("base64");
|
|
942
|
+
function privateKeyFromSeedB64u(seedB64u) {
|
|
943
|
+
const seed = fromB64u(seedB64u);
|
|
944
|
+
if (seed.length !== 32) throw new Error("Seed must be 32 bytes (base64url)");
|
|
945
|
+
const pkcs8 = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]);
|
|
946
|
+
return createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" });
|
|
947
|
+
}
|
|
948
|
+
function signBody(body, seedB64u) {
|
|
949
|
+
const priv = privateKeyFromSeedB64u(seedB64u);
|
|
950
|
+
const canon = canonicalize2(body);
|
|
951
|
+
const sig = edSign(null, Buffer.from(canon), priv);
|
|
952
|
+
return { ...body, sig: b64(sig) };
|
|
953
|
+
}
|
|
954
|
+
function keygen() {
|
|
955
|
+
const { publicKey, privateKey } = generateKeyPairSync2("ed25519");
|
|
956
|
+
const pubJwk = publicKey.export({ format: "jwk" });
|
|
957
|
+
const privJwk = privateKey.export({ format: "jwk" });
|
|
958
|
+
return { pubKeyB64: b64(fromB64u(pubJwk.x)), seedB64u: privJwk.d };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/commands/lint.ts
|
|
962
|
+
function collectFiles(target) {
|
|
963
|
+
if (!existsSync6(target)) return [];
|
|
964
|
+
const st = statSync(target);
|
|
965
|
+
if (st.isFile()) return [target];
|
|
966
|
+
const out = [];
|
|
967
|
+
for (const name of readdirSync(target)) {
|
|
968
|
+
const full = join4(target, name);
|
|
969
|
+
const s = statSync(full);
|
|
970
|
+
if (s.isDirectory()) {
|
|
971
|
+
if (name === "tests" || name === "node_modules" || name.startsWith(".")) continue;
|
|
972
|
+
out.push(...collectFiles(full));
|
|
973
|
+
} else if (extname(name) === ".yaml" || extname(name) === ".yml") {
|
|
974
|
+
if (name.endsWith(".asp.yaml") || name === "asp.yaml" || name === "asp.yml") {
|
|
975
|
+
out.push(full);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return out;
|
|
980
|
+
}
|
|
981
|
+
function lintPath(target) {
|
|
982
|
+
const files = collectFiles(target);
|
|
983
|
+
const issues = [];
|
|
984
|
+
if (files.length === 0) {
|
|
985
|
+
issues.push({ severity: "error", file: target, message: "no ASP files found (asp.yaml or *.asp.yaml)" });
|
|
986
|
+
return issues;
|
|
987
|
+
}
|
|
988
|
+
for (const file of files) {
|
|
989
|
+
const src = readFileSync5(file, "utf8");
|
|
990
|
+
const result = compileAspYamlToCanonicalJson(src);
|
|
991
|
+
if (!result.ok) {
|
|
992
|
+
for (const m of result.errors) issues.push({ severity: "error", file, message: m });
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
const body = result.body;
|
|
996
|
+
const dangerous = /(delete|drop|admin|payment|transfer)/i;
|
|
997
|
+
const allow = body.policy.allowedTools ?? [];
|
|
998
|
+
const hasDangerous = allow.some((t) => dangerous.test(t));
|
|
999
|
+
if (hasDangerous) {
|
|
1000
|
+
issues.push({
|
|
1001
|
+
severity: "warning",
|
|
1002
|
+
file,
|
|
1003
|
+
message: "allow.tools includes admin/delete-capable patterns \u2014 declare human_oversight.require_approval_for or move to deny."
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
const deny = body.policy.deniedTools ?? [];
|
|
1007
|
+
for (const d of deny) {
|
|
1008
|
+
if (allow.includes(d)) {
|
|
1009
|
+
issues.push({ severity: "warning", file, message: `rule "${d}" is in both allow.tools and deny.tools (deny wins; remove from allow).` });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const soon = Date.parse(body.expiresAt) - Date.now() < 7 * 24 * 3600 * 1e3;
|
|
1013
|
+
if (soon) {
|
|
1014
|
+
issues.push({ severity: "warning", file, message: `metadata.expiresAt is within 7 days (${body.expiresAt}).` });
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return issues;
|
|
1018
|
+
}
|
|
1019
|
+
function lintCommand() {
|
|
1020
|
+
return new Command6("lint").description("Validate ASP YAML policy files. Zero network.").argument("[path]", "File or directory to lint", ".").option("--strict", "Treat warnings as errors").option("--json", "Emit JSON report").action((path, opts) => {
|
|
1021
|
+
const issues = lintPath(path);
|
|
1022
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
1023
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
1024
|
+
if (opts.json) {
|
|
1025
|
+
process.stdout.write(JSON.stringify({ errors, warnings }, null, 2) + "\n");
|
|
1026
|
+
} else {
|
|
1027
|
+
for (const i of issues) {
|
|
1028
|
+
const mark = i.severity === "error" ? "\u2717" : "\u26A0";
|
|
1029
|
+
process.stdout.write(`${mark} [${i.severity}] ${i.file}: ${i.message}
|
|
1030
|
+
`);
|
|
1031
|
+
}
|
|
1032
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
1033
|
+
process.stdout.write("\u2713 ASP clean.\n");
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (errors.length > 0) process.exit(1);
|
|
1037
|
+
if (opts.strict && warnings.length > 0) process.exit(2);
|
|
1038
|
+
process.exit(0);
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/commands/test.ts
|
|
1043
|
+
import { Command as Command7 } from "commander";
|
|
1044
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1045
|
+
import { join as join5, extname as extname2, dirname } from "path";
|
|
1046
|
+
import { parse as parseYaml2 } from "yaml";
|
|
1047
|
+
|
|
1048
|
+
// src/lib/asp-eval.ts
|
|
1049
|
+
function globMatch(pattern, value) {
|
|
1050
|
+
const re = new RegExp(
|
|
1051
|
+
"^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
|
|
1052
|
+
);
|
|
1053
|
+
return re.test(value);
|
|
1054
|
+
}
|
|
1055
|
+
function evaluate(body, call) {
|
|
1056
|
+
const p = body.policy;
|
|
1057
|
+
if (p.deniedTools?.some((g) => globMatch(g, call.tool))) {
|
|
1058
|
+
return { decision: "deny", reason: `matched deny.tools glob for "${call.tool}"` };
|
|
1059
|
+
}
|
|
1060
|
+
if (call.server && p.allowedServers && p.allowedServers.length > 0) {
|
|
1061
|
+
const serverOk = p.allowedServers.some((s) => s === call.server || globMatch(s, call.server));
|
|
1062
|
+
if (!serverOk) return { decision: "deny", reason: `server "${call.server}" not in allow.servers` };
|
|
1063
|
+
}
|
|
1064
|
+
if (p.allowedTools && p.allowedTools.length > 0) {
|
|
1065
|
+
const toolOk = p.allowedTools.some((g) => globMatch(g, call.tool));
|
|
1066
|
+
if (!toolOk) return { decision: "deny", reason: `tool "${call.tool}" not in allow.tools` };
|
|
1067
|
+
}
|
|
1068
|
+
if (call.scopes && p.allowedScopes) {
|
|
1069
|
+
for (const [svc, requested] of Object.entries(call.scopes)) {
|
|
1070
|
+
const allowed = p.allowedScopes[svc] ?? [];
|
|
1071
|
+
for (const scope of requested) {
|
|
1072
|
+
if (!allowed.includes(scope)) {
|
|
1073
|
+
return { decision: "deny", reason: `scope "${scope}" not allowed for service "${svc}"` };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return { decision: "allow", reason: "matched allow rules" };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/commands/test.ts
|
|
1082
|
+
function findAspYaml(startDir) {
|
|
1083
|
+
let d = startDir;
|
|
1084
|
+
for (let i = 0; i < 6; i++) {
|
|
1085
|
+
for (const cand of ["asp.yaml", "asp.yml"]) {
|
|
1086
|
+
const p = join5(d, cand);
|
|
1087
|
+
if (existsSync7(p)) return p;
|
|
1088
|
+
}
|
|
1089
|
+
const parent = dirname(d);
|
|
1090
|
+
if (parent === d) break;
|
|
1091
|
+
d = parent;
|
|
1092
|
+
}
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
function collectFixtures(target) {
|
|
1096
|
+
if (!existsSync7(target)) return [];
|
|
1097
|
+
const st = statSync2(target);
|
|
1098
|
+
if (st.isFile()) return [target];
|
|
1099
|
+
const out = [];
|
|
1100
|
+
for (const name of readdirSync2(target)) {
|
|
1101
|
+
const full = join5(target, name);
|
|
1102
|
+
const s = statSync2(full);
|
|
1103
|
+
if (s.isDirectory()) out.push(...collectFixtures(full));
|
|
1104
|
+
else if ((extname2(name) === ".yaml" || extname2(name) === ".yml") && !name.endsWith(".asp.yaml"))
|
|
1105
|
+
out.push(full);
|
|
1106
|
+
}
|
|
1107
|
+
return out;
|
|
1108
|
+
}
|
|
1109
|
+
function runFixtures(target) {
|
|
1110
|
+
const dir = existsSync7(target) && statSync2(target).isDirectory() ? target : "policies/tests";
|
|
1111
|
+
const fixtures = collectFixtures(existsSync7(dir) ? dir : target);
|
|
1112
|
+
const results = [];
|
|
1113
|
+
for (const file of fixtures) {
|
|
1114
|
+
let fx;
|
|
1115
|
+
try {
|
|
1116
|
+
fx = parseYaml2(readFileSync6(file, "utf8"));
|
|
321
1117
|
} catch (err) {
|
|
322
|
-
|
|
1118
|
+
results.push({ file, name: "<parse>", ok: false, reason: err.message });
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
const policyPath = fx.policy ? join5(dirname(file), fx.policy) : findAspYaml(dirname(file));
|
|
1122
|
+
if (!policyPath || !existsSync7(policyPath)) {
|
|
1123
|
+
results.push({ file, name: "<policy>", ok: false, reason: "no asp.yaml found" });
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const compiled = compileAspYamlToCanonicalJson(readFileSync6(policyPath, "utf8"));
|
|
1127
|
+
if (!compiled.ok) {
|
|
1128
|
+
results.push({ file, name: "<compile>", ok: false, reason: compiled.errors.join("; ") });
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
for (const c of fx.cases ?? []) {
|
|
1132
|
+
const r = evaluate(compiled.body, c.call);
|
|
1133
|
+
const ok = r.decision === c.expect;
|
|
1134
|
+
results.push({
|
|
1135
|
+
file,
|
|
1136
|
+
name: c.name,
|
|
1137
|
+
ok,
|
|
1138
|
+
reason: ok ? r.reason : `expected ${c.expect}, got ${r.decision} (${r.reason})`
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return results;
|
|
1143
|
+
}
|
|
1144
|
+
function testCommand() {
|
|
1145
|
+
return new Command7("test").description("Run ASP policy golden tests against fixtures. Deterministic, zero network.").argument("[path]", "Fixtures dir", "policies/tests").option("--json", "Emit JSON report").action((path, opts) => {
|
|
1146
|
+
const results = runFixtures(path);
|
|
1147
|
+
const failed = results.filter((r) => !r.ok);
|
|
1148
|
+
if (opts.json) {
|
|
1149
|
+
process.stdout.write(JSON.stringify({ results }, null, 2) + "\n");
|
|
1150
|
+
} else {
|
|
1151
|
+
for (const r of results) {
|
|
1152
|
+
process.stdout.write(`${r.ok ? "\u2713" : "\u2717"} ${r.file} :: ${r.name} \u2014 ${r.reason}
|
|
1153
|
+
`);
|
|
1154
|
+
}
|
|
1155
|
+
process.stdout.write(`
|
|
1156
|
+
${results.length - failed.length}/${results.length} passed.
|
|
1157
|
+
`);
|
|
1158
|
+
}
|
|
1159
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/commands/policy.ts
|
|
1164
|
+
import { Command as Command8 } from "commander";
|
|
1165
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
1166
|
+
function readAspOrDie(path) {
|
|
1167
|
+
if (!existsSync8(path)) {
|
|
1168
|
+
process.stderr.write(`\u2717 file not found: ${path}
|
|
1169
|
+
`);
|
|
1170
|
+
process.exit(1);
|
|
1171
|
+
}
|
|
1172
|
+
return readFileSync7(path, "utf8");
|
|
1173
|
+
}
|
|
1174
|
+
function policyCommand() {
|
|
1175
|
+
const cmd = new Command8("policy").description("Compile / sign ASP YAML policies.");
|
|
1176
|
+
cmd.command("compile <file>").description("Compile ASP YAML \u2192 canonical JSON bundle body (unsigned).").option("--out <file>", "Write to file instead of stdout").action((file, opts) => {
|
|
1177
|
+
const result = compileAspYamlToCanonicalJson(readAspOrDie(file));
|
|
1178
|
+
if (!result.ok) {
|
|
1179
|
+
for (const m of result.errors) process.stderr.write(`\u2717 ${m}
|
|
1180
|
+
`);
|
|
323
1181
|
process.exit(1);
|
|
324
1182
|
}
|
|
1183
|
+
const out = JSON.stringify(result.body, null, 2) + "\n";
|
|
1184
|
+
if (opts.out) {
|
|
1185
|
+
writeFileSync4(opts.out, out);
|
|
1186
|
+
process.stderr.write(`\u2713 wrote ${opts.out}
|
|
1187
|
+
`);
|
|
1188
|
+
} else {
|
|
1189
|
+
process.stdout.write(out);
|
|
1190
|
+
}
|
|
325
1191
|
});
|
|
1192
|
+
cmd.command("sign <file>").description(
|
|
1193
|
+
"Compile + Ed25519-sign ASP YAML into a policy bundle the Aarmos runtime consumes. Reads AARMOS_POLICY_PRIVATE_KEY (base64url 32B seed)."
|
|
1194
|
+
).option("--out <file>", "Write signed bundle here (default: stdout)").action((file, opts) => {
|
|
1195
|
+
const seed = process.env.AARMOS_POLICY_PRIVATE_KEY;
|
|
1196
|
+
if (!seed) {
|
|
1197
|
+
process.stderr.write("\u2717 AARMOS_POLICY_PRIVATE_KEY not set (base64url 32B ed25519 seed)\n");
|
|
1198
|
+
process.exit(1);
|
|
1199
|
+
}
|
|
1200
|
+
const result = compileAspYamlToCanonicalJson(readAspOrDie(file));
|
|
1201
|
+
if (!result.ok) {
|
|
1202
|
+
for (const m of result.errors) process.stderr.write(`\u2717 ${m}
|
|
1203
|
+
`);
|
|
1204
|
+
process.exit(1);
|
|
1205
|
+
}
|
|
1206
|
+
const signed = signBody(result.body, seed);
|
|
1207
|
+
const out = JSON.stringify(signed, null, 2) + "\n";
|
|
1208
|
+
if (opts.out) {
|
|
1209
|
+
writeFileSync4(opts.out, out);
|
|
1210
|
+
process.stderr.write(`\u2713 signed \u2192 ${opts.out}
|
|
1211
|
+
`);
|
|
1212
|
+
} else {
|
|
1213
|
+
process.stdout.write(out);
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
cmd.command("keygen").description("Generate a new Ed25519 keypair for signing ASP policies.").action(() => {
|
|
1217
|
+
const k = keygen();
|
|
1218
|
+
process.stdout.write(
|
|
1219
|
+
`issuer.pubKey (paste into metadata.issuer.pubKey):
|
|
1220
|
+
${k.pubKeyB64}
|
|
1221
|
+
|
|
1222
|
+
AARMOS_POLICY_PRIVATE_KEY (keep secret):
|
|
1223
|
+
${k.seedB64u}
|
|
1224
|
+
`
|
|
1225
|
+
);
|
|
1226
|
+
});
|
|
1227
|
+
return cmd;
|
|
326
1228
|
}
|
|
327
1229
|
|
|
328
1230
|
// src/index.ts
|
|
329
|
-
var program = new
|
|
1231
|
+
var program = new Command9();
|
|
330
1232
|
program.name("aarmos").description(
|
|
331
1233
|
"Run your agent locally, across any protocol, with a signed receipt \u2014 in 60 seconds."
|
|
332
1234
|
).version("0.1.0");
|
|
@@ -335,6 +1237,9 @@ program.addCommand(runCommand());
|
|
|
335
1237
|
program.addCommand(proxyCommand());
|
|
336
1238
|
program.addCommand(daemonCommand());
|
|
337
1239
|
program.addCommand(verifyCommand());
|
|
1240
|
+
program.addCommand(lintCommand());
|
|
1241
|
+
program.addCommand(testCommand());
|
|
1242
|
+
program.addCommand(policyCommand());
|
|
338
1243
|
program.parseAsync(process.argv).catch((err) => {
|
|
339
1244
|
console.error(err instanceof Error ? err.message : String(err));
|
|
340
1245
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aarmos/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Aarmos CLI
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
7
7
|
"agent",
|
|
@@ -36,14 +36,17 @@
|
|
|
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",
|
|
42
|
+
"yaml": "^2.9.0",
|
|
40
43
|
"zod": "^3.23.8"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"@types/node": "^22.5.0",
|
|
44
|
-
"tsup": "^8.
|
|
47
|
+
"tsup": "^8.5.1",
|
|
45
48
|
"tsx": "^4.19.0",
|
|
46
|
-
"typescript": "^
|
|
49
|
+
"typescript": "^6.0.3"
|
|
47
50
|
},
|
|
48
51
|
"engines": {
|
|
49
52
|
"node": ">=18.17"
|