@mnemom/mnemom 0.7.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/LICENSE +191 -0
- package/README.md +123 -0
- package/dist/commands/agents.d.ts +15 -0
- package/dist/commands/agents.js +303 -0
- package/dist/commands/auth.d.ts +5 -0
- package/dist/commands/auth.js +60 -0
- package/dist/commands/card.d.ts +23 -0
- package/dist/commands/card.js +460 -0
- package/dist/commands/claim.d.ts +1 -0
- package/dist/commands/claim.js +72 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +763 -0
- package/dist/commands/integrity.d.ts +1 -0
- package/dist/commands/integrity.js +49 -0
- package/dist/commands/license.d.ts +3 -0
- package/dist/commands/license.js +163 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +73 -0
- package/dist/commands/migrate-config.d.ts +2 -0
- package/dist/commands/migrate-config.js +72 -0
- package/dist/commands/policy.d.ts +31 -0
- package/dist/commands/policy.js +543 -0
- package/dist/commands/register.d.ts +6 -0
- package/dist/commands/register.js +362 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +383 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +381 -0
- package/dist/lib/api.d.ts +133 -0
- package/dist/lib/api.js +207 -0
- package/dist/lib/auth.d.ts +60 -0
- package/dist/lib/auth.js +281 -0
- package/dist/lib/config.d.ts +105 -0
- package/dist/lib/config.js +253 -0
- package/dist/lib/format.d.ts +35 -0
- package/dist/lib/format.js +60 -0
- package/dist/lib/model-cache.d.ts +16 -0
- package/dist/lib/model-cache.js +138 -0
- package/dist/lib/models.d.ts +41 -0
- package/dist/lib/models.js +357 -0
- package/dist/lib/openclaw.d.ts +221 -0
- package/dist/lib/openclaw.js +474 -0
- package/dist/lib/prompt.d.ts +26 -0
- package/dist/lib/prompt.js +150 -0
- package/dist/smoltbot-shim.d.ts +2 -0
- package/dist/smoltbot-shim.js +7 -0
- package/package.json +61 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { requireAgent } from "../lib/config.js";
|
|
4
|
+
import { fmt } from "../lib/format.js";
|
|
5
|
+
import { askYesNo, isInteractive } from "../lib/prompt.js";
|
|
6
|
+
import { getPolicy, publishPolicy } from "../lib/api.js";
|
|
7
|
+
import { requireAuth } from "../lib/auth.js";
|
|
8
|
+
import { validatePolicySchema, evaluatePolicy, } from "@mnemom/policy-engine";
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// YAML parsing (minimal — parse structured YAML without external dep)
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Parse a policy file (JSON or YAML).
|
|
14
|
+
* For YAML, we use a minimal parser that handles the policy schema.
|
|
15
|
+
*/
|
|
16
|
+
function parsePolicyFile(raw, filePath) {
|
|
17
|
+
// Try JSON first
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Not JSON — try YAML via JSON conversion (user must install js-yaml or use JSON)
|
|
23
|
+
try {
|
|
24
|
+
// Dynamic import would be async; for simplicity, require JSON for now
|
|
25
|
+
// and provide clear error message
|
|
26
|
+
throw new Error("not json");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error(`Failed to parse ${path.basename(filePath)}. ` +
|
|
30
|
+
`Policy files must be valid JSON. ` +
|
|
31
|
+
`YAML support coming soon — convert with: npx js-yaml ${filePath}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Policy display
|
|
37
|
+
// ============================================================================
|
|
38
|
+
function displayPolicy(policy) {
|
|
39
|
+
const p = policy.policy_json;
|
|
40
|
+
console.log(fmt.header("Policy"));
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(fmt.label(" Policy ID:", ` ${policy.id}`));
|
|
43
|
+
console.log(fmt.label(" Name: ", ` ${policy.name}`));
|
|
44
|
+
if (policy.description) {
|
|
45
|
+
console.log(fmt.label(" Desc: ", ` ${policy.description}`));
|
|
46
|
+
}
|
|
47
|
+
console.log(fmt.label(" Version: ", ` ${policy.version}`));
|
|
48
|
+
console.log(fmt.label(" Created: ", ` ${policy.created_at}`));
|
|
49
|
+
// Capability mappings
|
|
50
|
+
const mappings = p.capability_mappings;
|
|
51
|
+
if (mappings && Object.keys(mappings).length > 0) {
|
|
52
|
+
console.log(fmt.section("Capability Mappings"));
|
|
53
|
+
console.log();
|
|
54
|
+
for (const [name, mapping] of Object.entries(mappings)) {
|
|
55
|
+
console.log(` ${fmt.success(name)}`);
|
|
56
|
+
if (mapping.description) {
|
|
57
|
+
console.log(` ${mapping.description}`);
|
|
58
|
+
}
|
|
59
|
+
console.log(` Tools: ${mapping.tools.join(", ")}`);
|
|
60
|
+
console.log(` Card actions: ${mapping.card_actions.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Forbidden rules
|
|
64
|
+
const forbidden = p.forbidden;
|
|
65
|
+
if (Array.isArray(forbidden) && forbidden.length > 0) {
|
|
66
|
+
console.log(fmt.section("Forbidden Rules"));
|
|
67
|
+
console.log();
|
|
68
|
+
for (const rule of forbidden) {
|
|
69
|
+
console.log(` ${fmt.error(`${rule.pattern} [${rule.severity}]`)}`);
|
|
70
|
+
console.log(` ${rule.reason}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Escalation triggers
|
|
74
|
+
const triggers = p.escalation_triggers;
|
|
75
|
+
if (Array.isArray(triggers) && triggers.length > 0) {
|
|
76
|
+
console.log(fmt.section("Escalation Triggers"));
|
|
77
|
+
console.log();
|
|
78
|
+
for (const trigger of triggers) {
|
|
79
|
+
console.log(` ${fmt.warn(`${trigger.condition} -> ${trigger.action}`)}`);
|
|
80
|
+
console.log(` ${trigger.reason}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Defaults
|
|
84
|
+
const defaults = p.defaults;
|
|
85
|
+
if (defaults) {
|
|
86
|
+
console.log(fmt.section("Defaults"));
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(fmt.label(" Unmapped action: ", defaults.unmapped_tool_action));
|
|
89
|
+
console.log(fmt.label(" Unmapped severity:", ` ${defaults.unmapped_severity}`));
|
|
90
|
+
console.log(fmt.label(" Fail open: ", ` ${defaults.fail_open}`));
|
|
91
|
+
}
|
|
92
|
+
console.log();
|
|
93
|
+
}
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Subcommands
|
|
96
|
+
// ============================================================================
|
|
97
|
+
/**
|
|
98
|
+
* smoltbot policy init — scaffold a policy.json with commented examples
|
|
99
|
+
*/
|
|
100
|
+
export async function policyInitCommand() {
|
|
101
|
+
const outputPath = path.resolve("policy.json");
|
|
102
|
+
if (fs.existsSync(outputPath)) {
|
|
103
|
+
console.log("\n" + fmt.warn("policy.json already exists in current directory") + "\n");
|
|
104
|
+
if (isInteractive()) {
|
|
105
|
+
const overwrite = await askYesNo("Overwrite?", false);
|
|
106
|
+
if (!overwrite) {
|
|
107
|
+
console.log("\nCancelled.\n");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const scaffold = {
|
|
116
|
+
meta: {
|
|
117
|
+
schema_version: "1.0",
|
|
118
|
+
name: "my-policy",
|
|
119
|
+
description: "Policy for my agent",
|
|
120
|
+
scope: "agent",
|
|
121
|
+
},
|
|
122
|
+
capability_mappings: {
|
|
123
|
+
web_fetch: {
|
|
124
|
+
description: "Web browsing and fetching",
|
|
125
|
+
tools: [
|
|
126
|
+
"WebFetch",
|
|
127
|
+
"WebSearch",
|
|
128
|
+
"mcp__browser__*",
|
|
129
|
+
"mcp__chrome-devtools__navigate_page",
|
|
130
|
+
],
|
|
131
|
+
card_actions: ["web_fetch", "web_browse"],
|
|
132
|
+
},
|
|
133
|
+
file_system: {
|
|
134
|
+
description: "File system operations",
|
|
135
|
+
tools: ["Read", "Write", "Edit", "Glob"],
|
|
136
|
+
card_actions: ["file_read", "file_write"],
|
|
137
|
+
},
|
|
138
|
+
code_execution: {
|
|
139
|
+
description: "Code and shell execution",
|
|
140
|
+
tools: ["Bash", "mcp__*__evaluate_script", "mcp__*__execute*"],
|
|
141
|
+
card_actions: ["code_execution"],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
forbidden: [
|
|
145
|
+
{
|
|
146
|
+
pattern: "mcp__*__delete*",
|
|
147
|
+
reason: "Destructive deletion forbidden",
|
|
148
|
+
severity: "critical",
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
escalation_triggers: [
|
|
152
|
+
{
|
|
153
|
+
condition: "tool_matches('*payment*')",
|
|
154
|
+
action: "escalate",
|
|
155
|
+
reason: "Payment tools require human approval",
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
defaults: {
|
|
159
|
+
unmapped_tool_action: "warn",
|
|
160
|
+
unmapped_severity: "medium",
|
|
161
|
+
fail_open: true,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
const tmpPath = `${outputPath}.${process.pid}.tmp`;
|
|
165
|
+
fs.writeFileSync(tmpPath, JSON.stringify(scaffold, null, 2) + "\n");
|
|
166
|
+
fs.renameSync(tmpPath, outputPath);
|
|
167
|
+
console.log(fmt.success(`Created policy.json`));
|
|
168
|
+
console.log(fmt.label(" Path:", ` ${outputPath}`));
|
|
169
|
+
console.log("\nEdit the file, then validate with:\n");
|
|
170
|
+
console.log(" smoltbot policy validate policy.json\n");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* smoltbot policy validate <file> — local-only validation
|
|
174
|
+
*/
|
|
175
|
+
export async function policyValidateCommand(file) {
|
|
176
|
+
const filePath = path.resolve(file);
|
|
177
|
+
if (!fs.existsSync(filePath)) {
|
|
178
|
+
console.log("\n" + fmt.error(`File not found: ${filePath}`) + "\n");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
let raw;
|
|
182
|
+
try {
|
|
183
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
187
|
+
console.log("\n" + fmt.error(`Could not read file: ${msg}`) + "\n");
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
let parsed;
|
|
191
|
+
try {
|
|
192
|
+
parsed = parsePolicyFile(raw, filePath);
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
196
|
+
console.log("\n" + fmt.error(msg) + "\n");
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
const result = validatePolicySchema(parsed);
|
|
200
|
+
console.log(fmt.header("Policy Validation Report"));
|
|
201
|
+
console.log();
|
|
202
|
+
console.log(fmt.label(" File:", ` ${filePath}`));
|
|
203
|
+
console.log();
|
|
204
|
+
if (result.valid) {
|
|
205
|
+
console.log(fmt.success("Policy schema is valid"));
|
|
206
|
+
const meta = parsed.meta;
|
|
207
|
+
if (meta) {
|
|
208
|
+
console.log(fmt.label(" Name: ", ` ${meta.name}`));
|
|
209
|
+
console.log(fmt.label(" Scope:", ` ${meta.scope}`));
|
|
210
|
+
}
|
|
211
|
+
const mappings = parsed.capability_mappings;
|
|
212
|
+
if (mappings) {
|
|
213
|
+
console.log(fmt.label(" Capabilities:", ` ${Object.keys(mappings).length}`));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
for (const error of result.errors) {
|
|
218
|
+
console.log(fmt.error(`${error.path}: ${error.message}`));
|
|
219
|
+
}
|
|
220
|
+
console.log();
|
|
221
|
+
console.log(fmt.error(`${result.errors.length} validation error(s)`) + "\n");
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
console.log();
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* smoltbot policy publish <file> — validate + upload to API
|
|
228
|
+
*/
|
|
229
|
+
export async function policyPublishCommand(file, agentName) {
|
|
230
|
+
const agent = await requireAgent(agentName);
|
|
231
|
+
const filePath = path.resolve(file);
|
|
232
|
+
if (!fs.existsSync(filePath)) {
|
|
233
|
+
console.log("\n" + fmt.error(`File not found: ${filePath}`) + "\n");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
let raw;
|
|
237
|
+
try {
|
|
238
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
242
|
+
console.log("\n" + fmt.error(`Could not read file: ${msg}`) + "\n");
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
let parsed;
|
|
246
|
+
try {
|
|
247
|
+
parsed = parsePolicyFile(raw, filePath);
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
251
|
+
console.log("\n" + fmt.error(msg) + "\n");
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
// Validate locally first
|
|
255
|
+
const validation = validatePolicySchema(parsed);
|
|
256
|
+
console.log(fmt.header("Policy Validation"));
|
|
257
|
+
console.log();
|
|
258
|
+
if (!validation.valid) {
|
|
259
|
+
for (const error of validation.errors) {
|
|
260
|
+
console.log(fmt.error(`${error.path}: ${error.message}`));
|
|
261
|
+
}
|
|
262
|
+
console.log();
|
|
263
|
+
console.log(fmt.error("Validation failed. Fix the errors above before publishing.") + "\n");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
console.log(fmt.success("Policy schema is valid"));
|
|
267
|
+
console.log();
|
|
268
|
+
// Require authentication
|
|
269
|
+
await requireAuth();
|
|
270
|
+
// Confirm
|
|
271
|
+
if (isInteractive()) {
|
|
272
|
+
const meta = parsed.meta;
|
|
273
|
+
const confirm = await askYesNo(`Publish policy "${meta?.name || "unnamed"}" for agent ${agent.agentId}?`, false);
|
|
274
|
+
if (!confirm) {
|
|
275
|
+
console.log("\nPublish cancelled.\n");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Publish
|
|
280
|
+
try {
|
|
281
|
+
console.log("\nPublishing policy...");
|
|
282
|
+
const result = await publishPolicy(agent.agentId, parsed);
|
|
283
|
+
console.log(fmt.success("Policy published successfully!"));
|
|
284
|
+
console.log(fmt.label(" Policy ID:", ` ${result.id}`));
|
|
285
|
+
console.log(fmt.label(" Version: ", ` ${result.version}`) + "\n");
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
289
|
+
console.log("\n" + fmt.error(`Failed to publish policy: ${message}`) + "\n");
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* smoltbot policy list — list active policies for current agent
|
|
295
|
+
*/
|
|
296
|
+
export async function policyListCommand(agentName) {
|
|
297
|
+
const agent = await requireAgent(agentName);
|
|
298
|
+
console.log("\nFetching policy...\n");
|
|
299
|
+
try {
|
|
300
|
+
const response = await getPolicy(agent.agentId);
|
|
301
|
+
if (!response || !response.policy) {
|
|
302
|
+
console.log(fmt.warn("No active policy"));
|
|
303
|
+
console.log("\nCreate a policy with:\n");
|
|
304
|
+
console.log(" smoltbot policy init");
|
|
305
|
+
console.log(" smoltbot policy publish policy.json\n");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
displayPolicy(response.policy);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
312
|
+
console.log("\n" + fmt.error(`Failed to fetch policy: ${message}`) + "\n");
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* smoltbot policy test <file> --against-traces — dry-run against historical traces
|
|
318
|
+
*/
|
|
319
|
+
export async function policyTestCommand(file, agentName) {
|
|
320
|
+
const agent = await requireAgent(agentName);
|
|
321
|
+
const filePath = path.resolve(file);
|
|
322
|
+
if (!fs.existsSync(filePath)) {
|
|
323
|
+
console.log("\n" + fmt.error(`File not found: ${filePath}`) + "\n");
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
let raw;
|
|
327
|
+
try {
|
|
328
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
332
|
+
console.log("\n" + fmt.error(`Could not read file: ${msg}`) + "\n");
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
let parsed;
|
|
336
|
+
try {
|
|
337
|
+
parsed = parsePolicyFile(raw, filePath);
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
341
|
+
console.log("\n" + fmt.error(msg) + "\n");
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
// Validate first
|
|
345
|
+
const validation = validatePolicySchema(parsed);
|
|
346
|
+
if (!validation.valid) {
|
|
347
|
+
console.log(fmt.error("Policy validation failed:"));
|
|
348
|
+
for (const error of validation.errors) {
|
|
349
|
+
console.log(fmt.error(` ${error.path}: ${error.message}`));
|
|
350
|
+
}
|
|
351
|
+
console.log();
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
// Require authentication
|
|
355
|
+
await requireAuth();
|
|
356
|
+
console.log(`\nTesting policy against historical traces for agent ${agent.agentId}...\n`);
|
|
357
|
+
try {
|
|
358
|
+
const { testPolicyHistorical } = await import("../lib/api.js");
|
|
359
|
+
const result = await testPolicyHistorical(agent.agentId, parsed);
|
|
360
|
+
console.log(fmt.header("Policy Test Results"));
|
|
361
|
+
console.log();
|
|
362
|
+
console.log(fmt.label(" Policy: ", ` ${result.policy_name}`));
|
|
363
|
+
console.log(fmt.label(" Total traces:", ` ${result.total_traces}`));
|
|
364
|
+
console.log();
|
|
365
|
+
const s = result.summary;
|
|
366
|
+
if (s.pass > 0)
|
|
367
|
+
console.log(fmt.success(`${s.pass} passed`));
|
|
368
|
+
if (s.warn > 0)
|
|
369
|
+
console.log(fmt.warn(`${s.warn} warnings`));
|
|
370
|
+
if (s.fail > 0)
|
|
371
|
+
console.log(fmt.error(`${s.fail} failed`));
|
|
372
|
+
if (s.skipped > 0)
|
|
373
|
+
console.log(fmt.label(" Skipped:", ` ${s.skipped} (no tools)`));
|
|
374
|
+
// Show first few failures
|
|
375
|
+
const failures = result.results.filter((r) => r.verdict === "fail");
|
|
376
|
+
if (failures.length > 0) {
|
|
377
|
+
console.log(fmt.section("Failed Traces (first 5)"));
|
|
378
|
+
console.log();
|
|
379
|
+
for (const f of failures.slice(0, 5)) {
|
|
380
|
+
console.log(fmt.error(` ${f.trace_id}`));
|
|
381
|
+
for (const v of f.violations ?? []) {
|
|
382
|
+
console.log(` ${v.type}: ${v.tool} — ${v.reason}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
console.log();
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
390
|
+
console.log("\n" + fmt.error(`Failed to test policy: ${message}`) + "\n");
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* smoltbot policy evaluate <policy-file> --card <card-file> --tools <tools> — local CI/CD evaluation
|
|
396
|
+
*
|
|
397
|
+
* Runs entirely locally using the embedded policy engine. No API key needed.
|
|
398
|
+
*/
|
|
399
|
+
export async function policyEvaluateCommand(file, options) {
|
|
400
|
+
// 1. Read + validate policy file
|
|
401
|
+
const policyPath = path.resolve(file);
|
|
402
|
+
if (!fs.existsSync(policyPath)) {
|
|
403
|
+
console.log("\n" + fmt.error(`Policy file not found: ${policyPath}`) + "\n");
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
let policyRaw;
|
|
407
|
+
try {
|
|
408
|
+
policyRaw = fs.readFileSync(policyPath, "utf-8");
|
|
409
|
+
}
|
|
410
|
+
catch (e) {
|
|
411
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
412
|
+
console.log("\n" + fmt.error(`Could not read policy file: ${msg}`) + "\n");
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
let policyParsed;
|
|
416
|
+
try {
|
|
417
|
+
policyParsed = parsePolicyFile(policyRaw, policyPath);
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
421
|
+
console.log("\n" + fmt.error(msg) + "\n");
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
const validation = validatePolicySchema(policyParsed);
|
|
425
|
+
if (!validation.valid) {
|
|
426
|
+
console.log(fmt.error("Policy validation failed:"));
|
|
427
|
+
for (const error of validation.errors) {
|
|
428
|
+
console.log(fmt.error(` ${error.path}: ${error.message}`));
|
|
429
|
+
}
|
|
430
|
+
console.log();
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
// 2. Read + parse card file
|
|
434
|
+
let cardContent = {};
|
|
435
|
+
if (options.card) {
|
|
436
|
+
const cardPath = path.resolve(options.card);
|
|
437
|
+
if (!fs.existsSync(cardPath)) {
|
|
438
|
+
console.log("\n" + fmt.error(`Card file not found: ${cardPath}`) + "\n");
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const cardRaw = fs.readFileSync(cardPath, "utf-8");
|
|
443
|
+
cardContent = JSON.parse(cardRaw);
|
|
444
|
+
}
|
|
445
|
+
catch (e) {
|
|
446
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
447
|
+
console.log("\n" + fmt.error(`Could not read card file: ${msg}`) + "\n");
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// 3. Parse tool list from --tools or --tool-manifest
|
|
452
|
+
let tools = [];
|
|
453
|
+
if (options.tools) {
|
|
454
|
+
tools = options.tools.split(",").map((t) => ({ name: t.trim() })).filter((t) => t.name);
|
|
455
|
+
}
|
|
456
|
+
else if (options.toolManifest) {
|
|
457
|
+
const manifestPath = path.resolve(options.toolManifest);
|
|
458
|
+
if (!fs.existsSync(manifestPath)) {
|
|
459
|
+
console.log("\n" + fmt.error(`Tool manifest file not found: ${manifestPath}`) + "\n");
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const manifestRaw = fs.readFileSync(manifestPath, "utf-8");
|
|
464
|
+
const manifest = JSON.parse(manifestRaw);
|
|
465
|
+
if (Array.isArray(manifest)) {
|
|
466
|
+
tools = manifest.map((t) => typeof t === "string" ? { name: t } : { name: t.name });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
471
|
+
console.log("\n" + fmt.error(`Could not read tool manifest: ${msg}`) + "\n");
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (tools.length === 0) {
|
|
476
|
+
console.log("\n" + fmt.error("No tools specified. Use --tools or --tool-manifest") + "\n");
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
// 4. Run evaluation
|
|
480
|
+
const result = evaluatePolicy({
|
|
481
|
+
context: "cicd",
|
|
482
|
+
policy: policyParsed,
|
|
483
|
+
card: cardContent,
|
|
484
|
+
tools,
|
|
485
|
+
});
|
|
486
|
+
// 5. Display results
|
|
487
|
+
console.log(fmt.header("Policy Evaluation Report"));
|
|
488
|
+
console.log();
|
|
489
|
+
console.log(fmt.label(" Policy:", ` ${policyParsed.meta?.name ?? "unknown"}`));
|
|
490
|
+
console.log(fmt.label(" Context:", " cicd"));
|
|
491
|
+
console.log(fmt.label(" Tools:", ` ${tools.length} (${tools.map((t) => t.name).join(", ")})`));
|
|
492
|
+
console.log();
|
|
493
|
+
// Verdict
|
|
494
|
+
if (result.verdict === "pass") {
|
|
495
|
+
console.log(fmt.success("PASS — all tools comply with policy"));
|
|
496
|
+
}
|
|
497
|
+
else if (result.verdict === "warn") {
|
|
498
|
+
console.log(fmt.warn("WARN — policy warnings detected"));
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
console.log(fmt.error("FAIL — policy violations detected"));
|
|
502
|
+
}
|
|
503
|
+
console.log();
|
|
504
|
+
// Violations
|
|
505
|
+
if (result.violations.length > 0) {
|
|
506
|
+
console.log(fmt.section("Violations"));
|
|
507
|
+
console.log();
|
|
508
|
+
for (const v of result.violations) {
|
|
509
|
+
console.log(fmt.error(` ${v.tool} [${v.severity}] — ${v.type}`));
|
|
510
|
+
console.log(` ${v.reason}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Warnings
|
|
514
|
+
if (result.warnings.length > 0) {
|
|
515
|
+
console.log(fmt.section("Warnings"));
|
|
516
|
+
console.log();
|
|
517
|
+
for (const w of result.warnings) {
|
|
518
|
+
console.log(fmt.warn(` ${w.tool} — ${w.type}`));
|
|
519
|
+
console.log(` ${w.reason}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// Coverage
|
|
523
|
+
const cov = result.coverage;
|
|
524
|
+
console.log(fmt.section("Coverage"));
|
|
525
|
+
console.log();
|
|
526
|
+
console.log(fmt.label(" Card actions:", ` ${cov.total_card_actions}`));
|
|
527
|
+
console.log(fmt.label(" Mapped: ", ` ${cov.mapped_card_actions.length}`));
|
|
528
|
+
console.log(fmt.label(" Unmapped: ", ` ${cov.unmapped_card_actions.length}`));
|
|
529
|
+
console.log(fmt.label(" Coverage: ", ` ${cov.coverage_pct.toFixed(1)}%`));
|
|
530
|
+
if (cov.unmapped_card_actions.length > 0) {
|
|
531
|
+
console.log(fmt.label(" Unmapped list:", ` ${cov.unmapped_card_actions.join(", ")}`));
|
|
532
|
+
}
|
|
533
|
+
console.log();
|
|
534
|
+
console.log(fmt.label(" Duration:", ` ${result.duration_ms}ms`));
|
|
535
|
+
console.log();
|
|
536
|
+
// 6. Exit code
|
|
537
|
+
if (result.verdict === "fail") {
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
if (options.strict && result.verdict === "warn") {
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
}
|