@aarmos/cli 0.2.0 → 0.2.2
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 +285 -29
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,8 +5,10 @@ import { Command as Command9 } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import { mkdirSync, writeFileSync, existsSync } from "fs";
|
|
9
|
-
import { join } from "path";
|
|
8
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, statSync, cpSync, mkdtempSync, rmSync } from "fs";
|
|
9
|
+
import { join, dirname, relative } from "path";
|
|
10
|
+
import { tmpdir } from "os";
|
|
11
|
+
import { spawnSync } from "child_process";
|
|
10
12
|
|
|
11
13
|
// src/lib/keys.ts
|
|
12
14
|
import { generateKeyPairSync } from "crypto";
|
|
@@ -53,13 +55,102 @@ var GITIGNORE_TEMPLATE = `# Aarmos local state
|
|
|
53
55
|
.aarmos/keys/
|
|
54
56
|
.aarmos/avar/
|
|
55
57
|
`;
|
|
58
|
+
var TEMPLATES = {
|
|
59
|
+
hello: {
|
|
60
|
+
description: "Minimal \u2014 policy + a single echo playbook. Great first run.",
|
|
61
|
+
files: {
|
|
62
|
+
"playbooks/hello.md": '# hello\n\nOne-step playbook. Ask the agent to echo a greeting.\n\n## steps\n\n1. Say: "hello from aarmos"\n',
|
|
63
|
+
"policy.aarmos.toml": `# hello template \u2014 deny-by-default, no external tools.
|
|
64
|
+
[defaults]
|
|
65
|
+
gates.destructive = "deny"
|
|
66
|
+
rate.per_minute = 30
|
|
67
|
+
`
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
mcp: {
|
|
71
|
+
description: "MCP starter \u2014 one MCP adapter wired with a read-only scope.",
|
|
72
|
+
files: {
|
|
73
|
+
"policy.aarmos.toml": `# mcp template \u2014 one MCP adapter, read-only scope.
|
|
74
|
+
[defaults]
|
|
75
|
+
gates.destructive = "confirm"
|
|
76
|
+
rate.per_minute = 60
|
|
77
|
+
|
|
78
|
+
[[adapters.mcp]]
|
|
79
|
+
name = "example"
|
|
80
|
+
url = "https://mcp.example.com/server"
|
|
81
|
+
scopes = ["read:*"]
|
|
82
|
+
`,
|
|
83
|
+
"playbooks/mcp-probe.md": "# mcp-probe\n\nList tools exposed by the configured MCP server and print their schemas. Read-only.\n"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
policy: {
|
|
87
|
+
description: "ASP starter \u2014 YAML policy-as-code with a golden test.",
|
|
88
|
+
files: {
|
|
89
|
+
"asp.yaml": `# ASP \u2014 Agent Security Policy (draft)
|
|
90
|
+
version: asp/1
|
|
91
|
+
defaults:
|
|
92
|
+
gates:
|
|
93
|
+
destructive: confirm
|
|
94
|
+
rate:
|
|
95
|
+
per_minute: 60
|
|
96
|
+
rules:
|
|
97
|
+
- id: deny-shell
|
|
98
|
+
when:
|
|
99
|
+
tool: "shell.*"
|
|
100
|
+
decision: deny
|
|
101
|
+
reason: "Shell tools are not permitted in this workspace."
|
|
102
|
+
`,
|
|
103
|
+
"policies/tests/deny-shell.yaml": `# golden test \u2014 shell.* must be denied
|
|
104
|
+
input:
|
|
105
|
+
tool: shell.exec
|
|
106
|
+
args: { cmd: "ls" }
|
|
107
|
+
expect:
|
|
108
|
+
decision: deny
|
|
109
|
+
source: workspace-policy
|
|
110
|
+
`
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var IMPORT_ALLOWLIST = [
|
|
115
|
+
"policy.aarmos.toml",
|
|
116
|
+
"avar.config.json",
|
|
117
|
+
"asp.yaml",
|
|
118
|
+
"asp.yml",
|
|
119
|
+
"policies",
|
|
120
|
+
"playbooks",
|
|
121
|
+
".aarmos/packs"
|
|
122
|
+
];
|
|
56
123
|
function initCommand() {
|
|
57
|
-
return new Command("init").description("Scaffold policy.aarmos.toml, avar.config.json, and local signing key").option("--force", "Overwrite existing files", false).
|
|
124
|
+
return new Command("init").description("Scaffold policy.aarmos.toml, avar.config.json, and local signing key").option("--force", "Overwrite existing files", false).option(
|
|
125
|
+
"--template <name>",
|
|
126
|
+
`Embedded starter template: ${Object.keys(TEMPLATES).join(" | ")}. Writes template files before defaults; existing files are preserved unless --force.`
|
|
127
|
+
).option(
|
|
128
|
+
"--from <repo>",
|
|
129
|
+
"Seed policies/playbooks from a GitHub repo (owner/repo[#ref][/subdir]) or tarball URL. Only Aarmos config files are copied; source code and keys are never imported."
|
|
130
|
+
).action(async (opts) => {
|
|
58
131
|
const cwd = process.cwd();
|
|
59
132
|
const keyDir = join(cwd, ".aarmos", "keys");
|
|
60
133
|
const chainDir = join(cwd, ".aarmos", "avar");
|
|
61
134
|
mkdirSync(keyDir, { recursive: true });
|
|
62
135
|
mkdirSync(chainDir, { recursive: true });
|
|
136
|
+
if (opts.template) {
|
|
137
|
+
const name = opts.template;
|
|
138
|
+
const tpl = TEMPLATES[name];
|
|
139
|
+
if (!tpl) {
|
|
140
|
+
console.error(
|
|
141
|
+
`\u2717 unknown template "${opts.template}". Known: ${Object.keys(TEMPLATES).join(", ")}`
|
|
142
|
+
);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
console.log(`\u25B8 template: ${name} \u2014 ${tpl.description}`);
|
|
147
|
+
for (const [rel, contents] of Object.entries(tpl.files)) {
|
|
148
|
+
writeIfMissing(join(cwd, rel), contents, opts.force);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (opts.from) {
|
|
152
|
+
await seedFromRepo(opts.from, cwd, opts.force);
|
|
153
|
+
}
|
|
63
154
|
writeIfMissing(join(cwd, "policy.aarmos.toml"), POLICY_TEMPLATE, opts.force);
|
|
64
155
|
writeIfMissing(join(cwd, "avar.config.json"), AVAR_CONFIG_TEMPLATE, opts.force);
|
|
65
156
|
const gitignorePath = join(cwd, ".aarmos", ".gitignore");
|
|
@@ -78,16 +169,111 @@ function writeIfMissing(path, content, force) {
|
|
|
78
169
|
console.log(`\xB7 skipped (exists): ${path}`);
|
|
79
170
|
return;
|
|
80
171
|
}
|
|
172
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
81
173
|
writeFileSync(path, content);
|
|
82
174
|
console.log(`\u2713 wrote: ${path}`);
|
|
83
175
|
}
|
|
176
|
+
function parseFrom(from) {
|
|
177
|
+
if (from.startsWith("http://") || from.startsWith("https://")) {
|
|
178
|
+
return { tarballUrl: from, subdir: "", label: from };
|
|
179
|
+
}
|
|
180
|
+
const hashIdx = from.indexOf("#");
|
|
181
|
+
const [pathPart, refPart] = hashIdx >= 0 ? [from.slice(0, hashIdx), from.slice(hashIdx + 1)] : [from, "main"];
|
|
182
|
+
const segs = pathPart.split("/").filter(Boolean);
|
|
183
|
+
if (segs.length < 2) throw new Error(`--from: expected owner/repo, got "${from}"`);
|
|
184
|
+
const [owner, repo, ...rest] = segs;
|
|
185
|
+
const subdir = rest.join("/");
|
|
186
|
+
return {
|
|
187
|
+
tarballUrl: `https://codeload.github.com/${owner}/${repo}/tar.gz/refs/heads/${refPart}`,
|
|
188
|
+
subdir,
|
|
189
|
+
label: `${owner}/${repo}${refPart !== "main" ? `#${refPart}` : ""}${subdir ? `/${subdir}` : ""}`
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async function seedFromRepo(from, cwd, force) {
|
|
193
|
+
const parsed = parseFrom(from);
|
|
194
|
+
console.log(`\u25B8 seeding from ${parsed.label}`);
|
|
195
|
+
const staging = mkdtempSync(join(tmpdir(), "aarmos-init-"));
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch(parsed.tarballUrl);
|
|
198
|
+
if (!res.ok) throw new Error(`fetch ${parsed.tarballUrl}: ${res.status}`);
|
|
199
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
200
|
+
const tarPath = join(staging, "src.tar.gz");
|
|
201
|
+
writeFileSync(tarPath, buf);
|
|
202
|
+
const extractDir = join(staging, "extract");
|
|
203
|
+
mkdirSync(extractDir);
|
|
204
|
+
const tar = spawnSync("tar", ["-xzf", tarPath, "-C", extractDir, "--strip-components=1"], {
|
|
205
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
206
|
+
});
|
|
207
|
+
if (tar.status !== 0) throw new Error("tar extract failed (is `tar` on PATH?)");
|
|
208
|
+
const root = parsed.subdir ? join(extractDir, parsed.subdir) : extractDir;
|
|
209
|
+
if (!existsSync(root)) throw new Error(`subdir not found in repo: ${parsed.subdir}`);
|
|
210
|
+
let copied = 0;
|
|
211
|
+
for (const rel of IMPORT_ALLOWLIST) {
|
|
212
|
+
const src = join(root, rel);
|
|
213
|
+
if (!existsSync(src)) continue;
|
|
214
|
+
const dest = join(cwd, rel);
|
|
215
|
+
if (existsSync(dest) && !force) {
|
|
216
|
+
console.log(`\xB7 skipped (exists): ${rel} \u2014 pass --force to overwrite`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (statSync(src).isDirectory()) {
|
|
220
|
+
const violations = findKeyLikeFiles(src);
|
|
221
|
+
if (violations.length > 0) {
|
|
222
|
+
console.log(`\u2717 refusing to copy ${rel}: contains key-like files: ${violations.slice(0, 3).join(", ")}`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
226
|
+
cpSync(src, dest, { recursive: true, force });
|
|
227
|
+
} else {
|
|
228
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
229
|
+
cpSync(src, dest, { force });
|
|
230
|
+
}
|
|
231
|
+
console.log(`\u2713 imported: ${rel}`);
|
|
232
|
+
copied++;
|
|
233
|
+
}
|
|
234
|
+
if (copied === 0) {
|
|
235
|
+
console.log("\xB7 no Aarmos config artifacts found in that repo; falling back to defaults");
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
try {
|
|
239
|
+
rmSync(staging, { recursive: true, force: true });
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function findKeyLikeFiles(dir) {
|
|
245
|
+
const out = [];
|
|
246
|
+
const walk = (d) => {
|
|
247
|
+
for (const name of readdirSync(d)) {
|
|
248
|
+
const full = join(d, name);
|
|
249
|
+
const st = statSync(full);
|
|
250
|
+
if (st.isDirectory()) {
|
|
251
|
+
walk(full);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (/\.(key|pem|p12|pfx)$/i.test(name) || /signing\.key$/i.test(name)) {
|
|
255
|
+
out.push(relative(dir, full));
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (st.size < 32e3) {
|
|
259
|
+
try {
|
|
260
|
+
const head = readFileSync(full, "utf8").slice(0, 200);
|
|
261
|
+
if (/-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(head)) out.push(relative(dir, full));
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
walk(dir);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
84
270
|
|
|
85
271
|
// src/commands/run.ts
|
|
86
272
|
import { Command as Command2 } from "commander";
|
|
87
273
|
import { spawn } from "child_process";
|
|
88
274
|
|
|
89
275
|
// src/lib/avar.ts
|
|
90
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, readFileSync } from "fs";
|
|
276
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
91
277
|
import { join as join2 } from "path";
|
|
92
278
|
import { createHash } from "crypto";
|
|
93
279
|
var CHAIN_DIR = ".aarmos/avar";
|
|
@@ -131,10 +317,10 @@ function serialize(receipt) {
|
|
|
131
317
|
}
|
|
132
318
|
|
|
133
319
|
// src/lib/policy.ts
|
|
134
|
-
import { existsSync as existsSync3, readFileSync as
|
|
320
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
135
321
|
import { createHash as createHash2 } from "crypto";
|
|
136
322
|
function loadPolicy(path) {
|
|
137
|
-
const raw = existsSync3(path) ?
|
|
323
|
+
const raw = existsSync3(path) ? readFileSync3(path, "utf8") : "";
|
|
138
324
|
const digest = createHash2("sha256").update(raw).digest("hex");
|
|
139
325
|
return { path, raw, digest };
|
|
140
326
|
}
|
|
@@ -242,7 +428,7 @@ function stripHopByHop(headers) {
|
|
|
242
428
|
|
|
243
429
|
// src/commands/daemon.ts
|
|
244
430
|
import { Command as Command4 } from "commander";
|
|
245
|
-
import { existsSync as existsSync4, readFileSync as
|
|
431
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
|
|
246
432
|
import { join as join3 } from "path";
|
|
247
433
|
import { spawn as spawn2 } from "child_process";
|
|
248
434
|
var PID_FILE = join3(process.cwd(), ".aarmos", "daemon.pid");
|
|
@@ -267,7 +453,7 @@ function daemonCommand() {
|
|
|
267
453
|
console.log("\xB7 no daemon running");
|
|
268
454
|
return;
|
|
269
455
|
}
|
|
270
|
-
const pid = Number(
|
|
456
|
+
const pid = Number(readFileSync4(PID_FILE, "utf8"));
|
|
271
457
|
try {
|
|
272
458
|
process.kill(pid);
|
|
273
459
|
} catch {
|
|
@@ -280,14 +466,14 @@ function daemonCommand() {
|
|
|
280
466
|
console.log("stopped");
|
|
281
467
|
return;
|
|
282
468
|
}
|
|
283
|
-
const pid = Number(
|
|
469
|
+
const pid = Number(readFileSync4(PID_FILE, "utf8"));
|
|
284
470
|
console.log(`running (pid ${pid})`);
|
|
285
471
|
});
|
|
286
472
|
return cmd;
|
|
287
473
|
}
|
|
288
474
|
function isRunning() {
|
|
289
475
|
if (!existsSync4(PID_FILE)) return false;
|
|
290
|
-
const pid = Number(
|
|
476
|
+
const pid = Number(readFileSync4(PID_FILE, "utf8"));
|
|
291
477
|
try {
|
|
292
478
|
process.kill(pid, 0);
|
|
293
479
|
return true;
|
|
@@ -298,7 +484,7 @@ function isRunning() {
|
|
|
298
484
|
|
|
299
485
|
// src/commands/verify.ts
|
|
300
486
|
import { Command as Command5 } from "commander";
|
|
301
|
-
import { existsSync as existsSync5, readFileSync as
|
|
487
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
302
488
|
import { unzipSync, strFromU8 } from "fflate";
|
|
303
489
|
|
|
304
490
|
// ../avar-core/src/canonicalize.ts
|
|
@@ -737,7 +923,7 @@ async function runVerify(opts) {
|
|
|
737
923
|
}
|
|
738
924
|
let bytes;
|
|
739
925
|
try {
|
|
740
|
-
bytes =
|
|
926
|
+
bytes = readFileSync5(file);
|
|
741
927
|
} catch (err) {
|
|
742
928
|
return emitError(
|
|
743
929
|
aarmosError("FILE_UNREADABLE", {
|
|
@@ -863,7 +1049,7 @@ function verifyCommand() {
|
|
|
863
1049
|
|
|
864
1050
|
// src/commands/lint.ts
|
|
865
1051
|
import { Command as Command6 } from "commander";
|
|
866
|
-
import { existsSync as existsSync6, readFileSync as
|
|
1052
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync2, readdirSync as readdirSync2 } from "fs";
|
|
867
1053
|
import { join as join4, extname } from "path";
|
|
868
1054
|
|
|
869
1055
|
// src/lib/asp-compile.ts
|
|
@@ -961,12 +1147,12 @@ function keygen() {
|
|
|
961
1147
|
// src/commands/lint.ts
|
|
962
1148
|
function collectFiles(target) {
|
|
963
1149
|
if (!existsSync6(target)) return [];
|
|
964
|
-
const st =
|
|
1150
|
+
const st = statSync2(target);
|
|
965
1151
|
if (st.isFile()) return [target];
|
|
966
1152
|
const out = [];
|
|
967
|
-
for (const name of
|
|
1153
|
+
for (const name of readdirSync2(target)) {
|
|
968
1154
|
const full = join4(target, name);
|
|
969
|
-
const s =
|
|
1155
|
+
const s = statSync2(full);
|
|
970
1156
|
if (s.isDirectory()) {
|
|
971
1157
|
if (name === "tests" || name === "node_modules" || name.startsWith(".")) continue;
|
|
972
1158
|
out.push(...collectFiles(full));
|
|
@@ -986,7 +1172,7 @@ function lintPath(target) {
|
|
|
986
1172
|
return issues;
|
|
987
1173
|
}
|
|
988
1174
|
for (const file of files) {
|
|
989
|
-
const src =
|
|
1175
|
+
const src = readFileSync6(file, "utf8");
|
|
990
1176
|
const result = compileAspYamlToCanonicalJson(src);
|
|
991
1177
|
if (!result.ok) {
|
|
992
1178
|
for (const m of result.errors) issues.push({ severity: "error", file, message: m });
|
|
@@ -1041,8 +1227,8 @@ function lintCommand() {
|
|
|
1041
1227
|
|
|
1042
1228
|
// src/commands/test.ts
|
|
1043
1229
|
import { Command as Command7 } from "commander";
|
|
1044
|
-
import { existsSync as existsSync7, readFileSync as
|
|
1045
|
-
import { join as join5, extname as extname2, dirname } from "path";
|
|
1230
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
1231
|
+
import { join as join5, extname as extname2, dirname as dirname2 } from "path";
|
|
1046
1232
|
import { parse as parseYaml2 } from "yaml";
|
|
1047
1233
|
|
|
1048
1234
|
// src/lib/asp-eval.ts
|
|
@@ -1086,7 +1272,7 @@ function findAspYaml(startDir) {
|
|
|
1086
1272
|
const p = join5(d, cand);
|
|
1087
1273
|
if (existsSync7(p)) return p;
|
|
1088
1274
|
}
|
|
1089
|
-
const parent =
|
|
1275
|
+
const parent = dirname2(d);
|
|
1090
1276
|
if (parent === d) break;
|
|
1091
1277
|
d = parent;
|
|
1092
1278
|
}
|
|
@@ -1094,12 +1280,12 @@ function findAspYaml(startDir) {
|
|
|
1094
1280
|
}
|
|
1095
1281
|
function collectFixtures(target) {
|
|
1096
1282
|
if (!existsSync7(target)) return [];
|
|
1097
|
-
const st =
|
|
1283
|
+
const st = statSync3(target);
|
|
1098
1284
|
if (st.isFile()) return [target];
|
|
1099
1285
|
const out = [];
|
|
1100
|
-
for (const name of
|
|
1286
|
+
for (const name of readdirSync3(target)) {
|
|
1101
1287
|
const full = join5(target, name);
|
|
1102
|
-
const s =
|
|
1288
|
+
const s = statSync3(full);
|
|
1103
1289
|
if (s.isDirectory()) out.push(...collectFixtures(full));
|
|
1104
1290
|
else if ((extname2(name) === ".yaml" || extname2(name) === ".yml") && !name.endsWith(".asp.yaml"))
|
|
1105
1291
|
out.push(full);
|
|
@@ -1107,23 +1293,23 @@ function collectFixtures(target) {
|
|
|
1107
1293
|
return out;
|
|
1108
1294
|
}
|
|
1109
1295
|
function runFixtures(target) {
|
|
1110
|
-
const dir = existsSync7(target) &&
|
|
1296
|
+
const dir = existsSync7(target) && statSync3(target).isDirectory() ? target : "policies/tests";
|
|
1111
1297
|
const fixtures = collectFixtures(existsSync7(dir) ? dir : target);
|
|
1112
1298
|
const results = [];
|
|
1113
1299
|
for (const file of fixtures) {
|
|
1114
1300
|
let fx;
|
|
1115
1301
|
try {
|
|
1116
|
-
fx = parseYaml2(
|
|
1302
|
+
fx = parseYaml2(readFileSync7(file, "utf8"));
|
|
1117
1303
|
} catch (err) {
|
|
1118
1304
|
results.push({ file, name: "<parse>", ok: false, reason: err.message });
|
|
1119
1305
|
continue;
|
|
1120
1306
|
}
|
|
1121
|
-
const policyPath = fx.policy ? join5(
|
|
1307
|
+
const policyPath = fx.policy ? join5(dirname2(file), fx.policy) : findAspYaml(dirname2(file));
|
|
1122
1308
|
if (!policyPath || !existsSync7(policyPath)) {
|
|
1123
1309
|
results.push({ file, name: "<policy>", ok: false, reason: "no asp.yaml found" });
|
|
1124
1310
|
continue;
|
|
1125
1311
|
}
|
|
1126
|
-
const compiled = compileAspYamlToCanonicalJson(
|
|
1312
|
+
const compiled = compileAspYamlToCanonicalJson(readFileSync7(policyPath, "utf8"));
|
|
1127
1313
|
if (!compiled.ok) {
|
|
1128
1314
|
results.push({ file, name: "<compile>", ok: false, reason: compiled.errors.join("; ") });
|
|
1129
1315
|
continue;
|
|
@@ -1162,14 +1348,44 @@ ${results.length - failed.length}/${results.length} passed.
|
|
|
1162
1348
|
|
|
1163
1349
|
// src/commands/policy.ts
|
|
1164
1350
|
import { Command as Command8 } from "commander";
|
|
1165
|
-
import { existsSync as existsSync8, readFileSync as
|
|
1351
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync4, watch as fsWatch, statSync as statSync4 } from "fs";
|
|
1352
|
+
import { dirname as dirname3, resolve } from "path";
|
|
1166
1353
|
function readAspOrDie(path) {
|
|
1167
1354
|
if (!existsSync8(path)) {
|
|
1168
1355
|
process.stderr.write(`\u2717 file not found: ${path}
|
|
1169
1356
|
`);
|
|
1170
1357
|
process.exit(1);
|
|
1171
1358
|
}
|
|
1172
|
-
return
|
|
1359
|
+
return readFileSync8(path, "utf8");
|
|
1360
|
+
}
|
|
1361
|
+
function runPolicyOnce(policyPath, testsDir, opts = {}) {
|
|
1362
|
+
const started = Date.now();
|
|
1363
|
+
let output = `
|
|
1364
|
+
\u25B8 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} ${opts.label ?? policyPath}
|
|
1365
|
+
`;
|
|
1366
|
+
const src = readFileSync8(policyPath, "utf8");
|
|
1367
|
+
const compiled = compileAspYamlToCanonicalJson(src);
|
|
1368
|
+
if (!compiled.ok) {
|
|
1369
|
+
for (const m of compiled.errors) output += ` \u2717 compile: ${m}
|
|
1370
|
+
`;
|
|
1371
|
+
return { ok: false, output, passed: 0, total: 0 };
|
|
1372
|
+
}
|
|
1373
|
+
const toolCount = (compiled.body.policy.allowedTools?.length ?? 0) + (compiled.body.policy.deniedTools?.length ?? 0);
|
|
1374
|
+
output += ` \u2713 compiled (${toolCount} tool rule[s], issuer=${compiled.body.issuer?.pubKey ?? "(unsigned)"})
|
|
1375
|
+
`;
|
|
1376
|
+
if (!existsSync8(testsDir)) {
|
|
1377
|
+
output += ` \xB7 no fixtures at ${opts.testsLabel ?? testsDir} \u2014 skipping tests
|
|
1378
|
+
`;
|
|
1379
|
+
return { ok: true, output, passed: 0, total: 0 };
|
|
1380
|
+
}
|
|
1381
|
+
const results = runFixtures(testsDir);
|
|
1382
|
+
const failed = results.filter((r) => !r.ok);
|
|
1383
|
+
for (const r of failed) output += ` \u2717 ${r.file} :: ${r.name} \u2014 ${r.reason}
|
|
1384
|
+
`;
|
|
1385
|
+
const passed = results.length - failed.length;
|
|
1386
|
+
output += ` ${failed.length === 0 ? "\u2713" : "\u2717"} ${passed}/${results.length} passed (${Date.now() - started}ms)
|
|
1387
|
+
`;
|
|
1388
|
+
return { ok: failed.length === 0, output, passed, total: results.length };
|
|
1173
1389
|
}
|
|
1174
1390
|
function policyCommand() {
|
|
1175
1391
|
const cmd = new Command8("policy").description("Compile / sign ASP YAML policies.");
|
|
@@ -1224,6 +1440,46 @@ AARMOS_POLICY_PRIVATE_KEY (keep secret):
|
|
|
1224
1440
|
`
|
|
1225
1441
|
);
|
|
1226
1442
|
});
|
|
1443
|
+
cmd.command("watch <file>").description(
|
|
1444
|
+
"Recompile ASP YAML on save and re-run fixtures. Zero network, zero side effects \u2014 the tightest feedback loop for authoring policy."
|
|
1445
|
+
).option("--tests <dir>", "Fixtures directory to re-run on each change", "policies/tests").option("--debounce <ms>", "Debounce interval in ms", "150").action((file, opts) => {
|
|
1446
|
+
const policyPath = resolve(file);
|
|
1447
|
+
if (!existsSync8(policyPath)) {
|
|
1448
|
+
process.stderr.write(`\u2717 file not found: ${policyPath}
|
|
1449
|
+
`);
|
|
1450
|
+
process.exit(1);
|
|
1451
|
+
}
|
|
1452
|
+
const testsDir = resolve(opts.tests);
|
|
1453
|
+
const debounceMs = Math.max(0, Number(opts.debounce) || 150);
|
|
1454
|
+
const runOnce = () => {
|
|
1455
|
+
const { output } = runPolicyOnce(policyPath, testsDir, { label: file, testsLabel: opts.tests });
|
|
1456
|
+
process.stdout.write(output);
|
|
1457
|
+
};
|
|
1458
|
+
process.stdout.write(`aarmos policy watch
|
|
1459
|
+
policy : ${policyPath}
|
|
1460
|
+
tests : ${testsDir}
|
|
1461
|
+
`);
|
|
1462
|
+
runOnce();
|
|
1463
|
+
let timer = null;
|
|
1464
|
+
const schedule = () => {
|
|
1465
|
+
if (timer) clearTimeout(timer);
|
|
1466
|
+
timer = setTimeout(runOnce, debounceMs);
|
|
1467
|
+
};
|
|
1468
|
+
const watchTarget = (p) => {
|
|
1469
|
+
try {
|
|
1470
|
+
const st = statSync4(p);
|
|
1471
|
+
const opts2 = st.isDirectory() ? { recursive: true } : void 0;
|
|
1472
|
+
fsWatch(p, opts2, () => schedule());
|
|
1473
|
+
} catch {
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
watchTarget(policyPath);
|
|
1477
|
+
watchTarget(dirname3(policyPath));
|
|
1478
|
+
if (existsSync8(testsDir)) watchTarget(testsDir);
|
|
1479
|
+
process.stdout.write("\nwatching for changes \u2014 Ctrl+C to exit\n");
|
|
1480
|
+
setInterval(() => {
|
|
1481
|
+
}, 1 << 30);
|
|
1482
|
+
});
|
|
1227
1483
|
return cmd;
|
|
1228
1484
|
}
|
|
1229
1485
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aarmos/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|