@aarmos/cli 0.1.3 → 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/dist/index.js +547 -17
- package/package.json +2 -1
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";
|
|
@@ -401,8 +401,8 @@ function getSubtle2() {
|
|
|
401
401
|
return c.subtle;
|
|
402
402
|
}
|
|
403
403
|
function b64uDecode(s) {
|
|
404
|
-
const
|
|
405
|
-
const bin = typeof atob === "function" ? atob(
|
|
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
406
|
const out = new Uint8Array(bin.length);
|
|
407
407
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
408
408
|
return out;
|
|
@@ -597,15 +597,155 @@ function computeChainHead(entries) {
|
|
|
597
597
|
return { entryHash: "", index: -1 };
|
|
598
598
|
}
|
|
599
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
|
+
|
|
600
724
|
// src/commands/verify.ts
|
|
601
725
|
async function runVerify(opts) {
|
|
602
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");
|
|
732
|
+
}
|
|
733
|
+
return rc;
|
|
734
|
+
};
|
|
603
735
|
if (!existsSync5(file)) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
+
);
|
|
607
748
|
}
|
|
608
|
-
const bytes = readFileSync4(file);
|
|
609
749
|
const isZip = looksLikeZip(bytes);
|
|
610
750
|
if (!isZip) {
|
|
611
751
|
try {
|
|
@@ -633,23 +773,40 @@ async function runVerify(opts) {
|
|
|
633
773
|
}
|
|
634
774
|
return 0;
|
|
635
775
|
} catch (err) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
776
|
+
return emitError(
|
|
777
|
+
aarmosError("BUNDLE_INVALID_JSON", {
|
|
778
|
+
message: `invalid JSON receipt: ${err instanceof Error ? err.message : String(err)}`
|
|
779
|
+
}),
|
|
780
|
+
1
|
|
781
|
+
);
|
|
639
782
|
}
|
|
640
783
|
}
|
|
641
784
|
let bundle;
|
|
642
785
|
try {
|
|
643
786
|
bundle = parseBundleBytes(bytes);
|
|
644
787
|
} catch (err) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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);
|
|
648
802
|
}
|
|
649
803
|
const report = await verifyBundle(bundle);
|
|
650
804
|
const failed = report.verdict === "invalid";
|
|
805
|
+
const classified = classifyReport(report);
|
|
651
806
|
if (json) {
|
|
652
|
-
process.stdout.write(
|
|
807
|
+
process.stdout.write(
|
|
808
|
+
JSON.stringify(classified ? { ...report, error: classified } : report) + "\n"
|
|
809
|
+
);
|
|
653
810
|
} else if (!quiet) {
|
|
654
811
|
const mark = failed ? "\u2717" : "\u2713";
|
|
655
812
|
process.stdout.write(`${mark} verdict: ${report.verdict}
|
|
@@ -666,11 +823,15 @@ async function runVerify(opts) {
|
|
|
666
823
|
process.stdout.write(` issues:
|
|
667
824
|
`);
|
|
668
825
|
for (const issue of report.issues) {
|
|
669
|
-
process.stdout.write(` - [${issue.kind}] ${issue.detail}
|
|
826
|
+
process.stdout.write(` - [${issue.kind}] ${issue.detail ?? ""}
|
|
670
827
|
`);
|
|
671
828
|
}
|
|
672
829
|
}
|
|
673
|
-
|
|
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
|
+
}
|
|
674
835
|
}
|
|
675
836
|
return failed ? 1 : 0;
|
|
676
837
|
}
|
|
@@ -700,8 +861,374 @@ function verifyCommand() {
|
|
|
700
861
|
});
|
|
701
862
|
}
|
|
702
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"));
|
|
1117
|
+
} catch (err) {
|
|
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
|
+
`);
|
|
1181
|
+
process.exit(1);
|
|
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
|
+
}
|
|
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;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
703
1230
|
// src/index.ts
|
|
704
|
-
var program = new
|
|
1231
|
+
var program = new Command9();
|
|
705
1232
|
program.name("aarmos").description(
|
|
706
1233
|
"Run your agent locally, across any protocol, with a signed receipt \u2014 in 60 seconds."
|
|
707
1234
|
).version("0.1.0");
|
|
@@ -710,6 +1237,9 @@ program.addCommand(runCommand());
|
|
|
710
1237
|
program.addCommand(proxyCommand());
|
|
711
1238
|
program.addCommand(daemonCommand());
|
|
712
1239
|
program.addCommand(verifyCommand());
|
|
1240
|
+
program.addCommand(lintCommand());
|
|
1241
|
+
program.addCommand(testCommand());
|
|
1242
|
+
program.addCommand(policyCommand());
|
|
713
1243
|
program.parseAsync(process.argv).catch((err) => {
|
|
714
1244
|
console.error(err instanceof Error ? err.message : String(err));
|
|
715
1245
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aarmos/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@aarmos/avar-core": "^1.0.0-rc.3",
|
|
40
40
|
"commander": "^12.1.0",
|
|
41
41
|
"fflate": "^0.8.3",
|
|
42
|
+
"yaml": "^2.9.0",
|
|
42
43
|
"zod": "^3.23.8"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|