@mnemom/mnemom 0.7.2 → 0.9.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/commands/agents.d.ts +0 -14
- package/dist/commands/agents.js +35 -295
- package/dist/commands/auth.js +2 -7
- package/dist/commands/card.d.ts +33 -9
- package/dist/commands/card.js +698 -258
- package/dist/commands/integrity.js +13 -12
- package/dist/commands/license.js +14 -29
- package/dist/commands/logs.js +8 -8
- package/dist/commands/policy.d.ts +10 -23
- package/dist/commands/policy.js +20 -533
- package/dist/commands/protection.d.ts +22 -0
- package/dist/commands/protection.js +542 -0
- package/dist/commands/status.js +48 -54
- package/dist/index.js +120 -162
- package/dist/lib/api.d.ts +131 -9
- package/dist/lib/api.js +298 -12
- package/dist/lib/auth.d.ts +46 -21
- package/dist/lib/auth.js +148 -52
- package/dist/lib/config.d.ts +11 -98
- package/dist/lib/config.js +12 -220
- package/dist/lib/model-cache.js +5 -6
- package/dist/smoltbot-shim.js +1 -1
- package/package.json +2 -2
- package/dist/commands/init.d.ts +0 -7
- package/dist/commands/init.js +0 -763
- package/dist/commands/migrate-config.d.ts +0 -2
- package/dist/commands/migrate-config.js +0 -72
- package/dist/commands/register.d.ts +0 -6
- package/dist/commands/register.js +0 -362
package/dist/commands/card.js
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
3
5
|
import yaml from "js-yaml";
|
|
4
|
-
import {
|
|
5
|
-
import { getCard, updateCard, reverifyAgent, } from "../lib/api.js";
|
|
6
|
+
import { ALIGNMENT_CARD_MAX_BYTES, getAlignmentCard, putAlignmentCard, resolveAgentId, } from "../lib/api.js";
|
|
6
7
|
import { requireAuth } from "../lib/auth.js";
|
|
7
8
|
import { fmt } from "../lib/format.js";
|
|
8
9
|
import { askYesNo, isInteractive } from "../lib/prompt.js";
|
|
9
|
-
|
|
10
|
-
const STANDARD_VALUES = new Set([
|
|
11
|
-
"transparency",
|
|
12
|
-
"honesty",
|
|
13
|
-
"safety",
|
|
14
|
-
"privacy",
|
|
15
|
-
"fairness",
|
|
16
|
-
"accountability",
|
|
17
|
-
"beneficence",
|
|
18
|
-
"non-maleficence",
|
|
19
|
-
"autonomy",
|
|
20
|
-
"justice",
|
|
21
|
-
"reliability",
|
|
22
|
-
"security",
|
|
23
|
-
"human_oversight",
|
|
24
|
-
"explainability",
|
|
25
|
-
]);
|
|
10
|
+
import { evaluatePolicy, } from "@mnemom/policy-engine";
|
|
26
11
|
function detectFormat(filePath) {
|
|
27
12
|
const ext = path.extname(filePath).toLowerCase();
|
|
28
13
|
if (ext === ".yaml" || ext === ".yml")
|
|
@@ -37,287 +22,526 @@ export function parseCardFile(filePath) {
|
|
|
37
22
|
if (!parsed || typeof parsed !== "object") {
|
|
38
23
|
throw new Error("YAML did not produce a valid object");
|
|
39
24
|
}
|
|
40
|
-
return { format, parsed };
|
|
25
|
+
return { format, parsed, raw };
|
|
41
26
|
}
|
|
42
|
-
return { format, parsed: JSON.parse(raw) };
|
|
27
|
+
return { format, parsed: JSON.parse(raw), raw };
|
|
43
28
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
29
|
+
const ALIGNMENT_MODES = ["off", "observe", "nudge", "enforce"];
|
|
30
|
+
const PRINCIPAL_TYPES = ["human", "organization", "agent", "unspecified"];
|
|
31
|
+
const PRINCIPAL_RELATIONSHIPS = ["delegated_authority", "advisory", "autonomous"];
|
|
32
|
+
const VALUE_HIERARCHIES = ["lexicographic", "weighted", "contextual"];
|
|
33
|
+
const ESCALATION_ACTIONS = ["escalate", "deny", "log"];
|
|
34
|
+
const CONSCIENCE_MODES = ["augment", "replace"];
|
|
35
|
+
const CONSCIENCE_VALUE_TYPES = ["BOUNDARY", "FEAR", "COMMITMENT", "BELIEF", "HOPE"];
|
|
36
|
+
const CONSCIENCE_SEVERITIES = ["advisory", "mandatory"];
|
|
37
|
+
const TAMPER_EVIDENCE = ["append_only", "signed", "merkle"];
|
|
38
|
+
const UNMAPPED_SEVERITIES = ["low", "medium", "high", "critical"];
|
|
39
|
+
function isObj(v) {
|
|
40
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate a unified alignment card against ADR-039 canonical form.
|
|
44
|
+
*
|
|
45
|
+
* Required: card_version, agent_id, autonomy_mode, integrity_mode,
|
|
46
|
+
* principal (with identifier when type != unspecified),
|
|
47
|
+
* values.declared (non-empty), autonomy.bounded_actions (non-empty,
|
|
48
|
+
* disjoint from forbidden_actions), audit (retention_days, queryable,
|
|
49
|
+
* query_endpoint when queryable=true).
|
|
50
|
+
*
|
|
51
|
+
* Rejected legacy locations: integrity.enforcement_mode,
|
|
52
|
+
* enforcement.{mode, unmapped_tool_action, fail_open}, _composition.
|
|
53
|
+
*/
|
|
54
|
+
export function validateUnifiedCard(card) {
|
|
47
55
|
const checks = [];
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
// ── card_version (required) ──
|
|
57
|
+
if (typeof card.card_version !== "string" || card.card_version.length === 0) {
|
|
58
|
+
checks.push({
|
|
59
|
+
name: "card_version",
|
|
60
|
+
passed: false,
|
|
61
|
+
message: 'Required (string, e.g. "unified/2026-04-26").',
|
|
62
|
+
});
|
|
53
63
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
checks.push({ name: "Valid JSON", passed: false, message: `Parse error: ${msg}` });
|
|
57
|
-
return checks; // Can't continue without valid JSON
|
|
64
|
+
else {
|
|
65
|
+
checks.push({ name: "card_version", passed: true, message: card.card_version });
|
|
58
66
|
}
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
checks.push({ name: `Block: ${block}`, passed: false, message: "Missing or invalid" });
|
|
67
|
-
}
|
|
67
|
+
// ── agent_id (required) ──
|
|
68
|
+
if (typeof card.agent_id !== "string" || card.agent_id.length === 0) {
|
|
69
|
+
checks.push({ name: "agent_id", passed: false, message: "Required (string)." });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
checks.push({ name: "agent_id", passed: true, message: card.agent_id });
|
|
68
73
|
}
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
if (Array.isArray(declared) && declared.length > 0) {
|
|
74
|
+
// ── autonomy_mode (top-level master switch, ADR-039 Decision 1) ──
|
|
75
|
+
if (typeof card.autonomy_mode !== "string") {
|
|
72
76
|
checks.push({
|
|
73
|
-
name: "
|
|
74
|
-
passed:
|
|
75
|
-
message:
|
|
77
|
+
name: "autonomy_mode",
|
|
78
|
+
passed: false,
|
|
79
|
+
message: `Required (top-level master switch). Must be one of: ${ALIGNMENT_MODES.join(" | ")}. Per ADR-039 the legacy enforcement.mode location is no longer accepted.`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else if (!ALIGNMENT_MODES.includes(card.autonomy_mode)) {
|
|
83
|
+
checks.push({
|
|
84
|
+
name: "autonomy_mode",
|
|
85
|
+
passed: false,
|
|
86
|
+
message: `Invalid: "${card.autonomy_mode}". Must be one of: ${ALIGNMENT_MODES.join(" | ")}.`,
|
|
76
87
|
});
|
|
77
88
|
}
|
|
78
89
|
else {
|
|
90
|
+
checks.push({ name: "autonomy_mode", passed: true, message: card.autonomy_mode });
|
|
91
|
+
}
|
|
92
|
+
// ── integrity_mode (top-level master switch, ADR-039 Decision 1) ──
|
|
93
|
+
if (typeof card.integrity_mode !== "string") {
|
|
79
94
|
checks.push({
|
|
80
|
-
name: "
|
|
95
|
+
name: "integrity_mode",
|
|
96
|
+
passed: false,
|
|
97
|
+
message: `Required (top-level master switch). Must be one of: ${ALIGNMENT_MODES.join(" | ")}. Per ADR-039 the legacy integrity.enforcement_mode location is no longer accepted.`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else if (!ALIGNMENT_MODES.includes(card.integrity_mode)) {
|
|
101
|
+
checks.push({
|
|
102
|
+
name: "integrity_mode",
|
|
103
|
+
passed: false,
|
|
104
|
+
message: `Invalid: "${card.integrity_mode}". Must be one of: ${ALIGNMENT_MODES.join(" | ")}.`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
checks.push({ name: "integrity_mode", passed: true, message: card.integrity_mode });
|
|
109
|
+
}
|
|
110
|
+
// ── ADR-039 cutover: reject legacy locations with a pointer to the new field ──
|
|
111
|
+
const integ = card.integrity;
|
|
112
|
+
if (integ && typeof integ === "object" && integ.enforcement_mode !== undefined) {
|
|
113
|
+
checks.push({
|
|
114
|
+
name: "integrity.enforcement_mode",
|
|
81
115
|
passed: false,
|
|
82
|
-
message: "
|
|
116
|
+
message: "Legacy field rejected. Use top-level integrity_mode instead (ADR-039).",
|
|
83
117
|
});
|
|
84
118
|
}
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
const customValues = declared.filter((v) => !STANDARD_VALUES.has(v));
|
|
89
|
-
const missingDefs = customValues.filter((v) => !definitions[v]);
|
|
90
|
-
if (missingDefs.length === 0) {
|
|
119
|
+
const enf = card.enforcement;
|
|
120
|
+
if (enf && typeof enf === "object") {
|
|
121
|
+
if (enf.mode !== undefined) {
|
|
91
122
|
checks.push({
|
|
92
|
-
name: "
|
|
93
|
-
passed:
|
|
94
|
-
message:
|
|
95
|
-
? "No custom values (all standard)"
|
|
96
|
-
: `${customValues.length} custom value(s) defined`,
|
|
123
|
+
name: "enforcement.mode",
|
|
124
|
+
passed: false,
|
|
125
|
+
message: "Legacy field rejected. Use top-level autonomy_mode instead (ADR-039).",
|
|
97
126
|
});
|
|
98
127
|
}
|
|
99
|
-
|
|
128
|
+
if (enf.unmapped_tool_action !== undefined) {
|
|
129
|
+
checks.push({
|
|
130
|
+
name: "enforcement.unmapped_tool_action",
|
|
131
|
+
passed: false,
|
|
132
|
+
message: "Legacy field rejected. Derived from enforcement.allow_unmapped_tools instead (ADR-039).",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (enf.fail_open !== undefined) {
|
|
100
136
|
checks.push({
|
|
101
|
-
name: "
|
|
137
|
+
name: "enforcement.fail_open",
|
|
102
138
|
passed: false,
|
|
103
|
-
message:
|
|
139
|
+
message: "Legacy field rejected. fail_open is a runtime safety knob (gateway env config), not a card field (ADR-039).",
|
|
104
140
|
});
|
|
105
141
|
}
|
|
106
142
|
}
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
if (Array.isArray(bounded) && bounded.length > 0) {
|
|
143
|
+
// _composition is system-managed
|
|
144
|
+
if (card._composition !== undefined) {
|
|
110
145
|
checks.push({
|
|
111
|
-
name: "
|
|
112
|
-
passed:
|
|
113
|
-
message:
|
|
146
|
+
name: "_composition",
|
|
147
|
+
passed: false,
|
|
148
|
+
message: "System-managed field — cannot be set on inbound cards.",
|
|
114
149
|
});
|
|
115
150
|
}
|
|
116
|
-
|
|
151
|
+
// ── principal (required: type + relationship; identifier when type != unspecified) ──
|
|
152
|
+
if (!isObj(card.principal)) {
|
|
117
153
|
checks.push({
|
|
118
|
-
name: "
|
|
154
|
+
name: "principal",
|
|
119
155
|
passed: false,
|
|
120
|
-
message: "
|
|
156
|
+
message: "Required (object with at least type + relationship).",
|
|
121
157
|
});
|
|
122
158
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const triggers = card.autonomy_envelope?.escalation_triggers;
|
|
127
|
-
if (Array.isArray(triggers) && triggers.length > 0) {
|
|
128
|
-
const conditionPattern = /^[\w\s\(\)\*\.'",=!><&|]+$/;
|
|
129
|
-
const invalidTriggers = triggers.filter((t) => !t.condition || !conditionPattern.test(t.condition));
|
|
130
|
-
if (invalidTriggers.length === 0) {
|
|
159
|
+
else {
|
|
160
|
+
const p = card.principal;
|
|
161
|
+
if (!PRINCIPAL_TYPES.includes(String(p.type))) {
|
|
131
162
|
checks.push({
|
|
132
|
-
name: "
|
|
133
|
-
passed:
|
|
134
|
-
message:
|
|
163
|
+
name: "principal.type",
|
|
164
|
+
passed: false,
|
|
165
|
+
message: `Must be one of: ${PRINCIPAL_TYPES.join(", ")}.`,
|
|
135
166
|
});
|
|
136
167
|
}
|
|
137
|
-
|
|
168
|
+
if (!PRINCIPAL_RELATIONSHIPS.includes(String(p.relationship))) {
|
|
169
|
+
checks.push({
|
|
170
|
+
name: "principal.relationship",
|
|
171
|
+
passed: false,
|
|
172
|
+
message: `Must be one of: ${PRINCIPAL_RELATIONSHIPS.join(", ")}.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// ADR-039 Decision 10: identifier required when type != unspecified
|
|
176
|
+
if (p.type !== "unspecified" &&
|
|
177
|
+
PRINCIPAL_TYPES.includes(String(p.type)) &&
|
|
178
|
+
(typeof p.identifier !== "string" || p.identifier.length === 0)) {
|
|
138
179
|
checks.push({
|
|
139
|
-
name: "
|
|
180
|
+
name: "principal.identifier",
|
|
140
181
|
passed: false,
|
|
141
|
-
message:
|
|
182
|
+
message: 'Required when principal.type is not "unspecified" (ADR-039 Decision 10).',
|
|
142
183
|
});
|
|
143
184
|
}
|
|
144
185
|
}
|
|
145
|
-
|
|
186
|
+
// ── values.declared (required, non-empty array of strings) ──
|
|
187
|
+
if (!isObj(card.values) || !Array.isArray(card.values.declared)) {
|
|
146
188
|
checks.push({
|
|
147
|
-
name: "
|
|
148
|
-
passed:
|
|
149
|
-
message: "
|
|
189
|
+
name: "values.declared",
|
|
190
|
+
passed: false,
|
|
191
|
+
message: "Required (non-empty array of strings).",
|
|
150
192
|
});
|
|
151
193
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
194
|
+
else {
|
|
195
|
+
const v = card.values;
|
|
196
|
+
const decl = v.declared;
|
|
197
|
+
if (decl.length === 0) {
|
|
156
198
|
checks.push({
|
|
157
|
-
name: "
|
|
199
|
+
name: "values.declared",
|
|
158
200
|
passed: false,
|
|
159
|
-
message: "
|
|
201
|
+
message: "Must contain at least one value.",
|
|
160
202
|
});
|
|
161
203
|
}
|
|
162
|
-
else if (
|
|
204
|
+
else if (!decl.every(s => typeof s === "string")) {
|
|
163
205
|
checks.push({
|
|
164
|
-
name: "
|
|
206
|
+
name: "values.declared",
|
|
165
207
|
passed: false,
|
|
166
|
-
message:
|
|
208
|
+
message: "All entries must be strings.",
|
|
167
209
|
});
|
|
168
210
|
}
|
|
169
211
|
else {
|
|
170
212
|
checks.push({
|
|
171
|
-
name: "
|
|
213
|
+
name: "values.declared",
|
|
172
214
|
passed: true,
|
|
173
|
-
message:
|
|
215
|
+
message: `${decl.length} value(s) declared`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// definitions ⊆ declared (ADR-039 Decision 10)
|
|
219
|
+
if (v.definitions !== undefined) {
|
|
220
|
+
if (!isObj(v.definitions)) {
|
|
221
|
+
checks.push({
|
|
222
|
+
name: "values.definitions",
|
|
223
|
+
passed: false,
|
|
224
|
+
message: "Must be an object keyed by value names.",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const declSet = new Set(decl.filter((s) => typeof s === "string"));
|
|
229
|
+
for (const key of Object.keys(v.definitions)) {
|
|
230
|
+
if (!declSet.has(key)) {
|
|
231
|
+
checks.push({
|
|
232
|
+
name: `values.definitions.${key}`,
|
|
233
|
+
passed: false,
|
|
234
|
+
message: "Definition key not present in values.declared (ADR-039 Decision 10).",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// hierarchy enum
|
|
241
|
+
if (v.hierarchy !== undefined && !VALUE_HIERARCHIES.includes(String(v.hierarchy))) {
|
|
242
|
+
checks.push({
|
|
243
|
+
name: "values.hierarchy",
|
|
244
|
+
passed: false,
|
|
245
|
+
message: `Must be one of: ${VALUE_HIERARCHIES.join(", ")}.`,
|
|
174
246
|
});
|
|
175
247
|
}
|
|
176
248
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return validateCard(JSON.stringify(card));
|
|
185
|
-
}
|
|
186
|
-
// ============================================================================
|
|
187
|
-
// Card display
|
|
188
|
-
// ============================================================================
|
|
189
|
-
function displayCard(cardResponse) {
|
|
190
|
-
const card = cardResponse.card_json;
|
|
191
|
-
console.log(fmt.header("Alignment Card"));
|
|
192
|
-
console.log();
|
|
193
|
-
// Header info
|
|
194
|
-
console.log(fmt.label(" Card ID: ", cardResponse.card_id));
|
|
195
|
-
if (card.version) {
|
|
196
|
-
console.log(fmt.label(" Version: ", card.version));
|
|
249
|
+
// ── autonomy.bounded_actions (required, non-empty; disjoint from forbidden_actions) ──
|
|
250
|
+
if (!isObj(card.autonomy) || !Array.isArray(card.autonomy.bounded_actions)) {
|
|
251
|
+
checks.push({
|
|
252
|
+
name: "autonomy.bounded_actions",
|
|
253
|
+
passed: false,
|
|
254
|
+
message: "Required (non-empty array of strings).",
|
|
255
|
+
});
|
|
197
256
|
}
|
|
198
|
-
|
|
199
|
-
|
|
257
|
+
else {
|
|
258
|
+
const a = card.autonomy;
|
|
259
|
+
const bounded = a.bounded_actions;
|
|
260
|
+
if (bounded.length === 0) {
|
|
261
|
+
checks.push({
|
|
262
|
+
name: "autonomy.bounded_actions",
|
|
263
|
+
passed: false,
|
|
264
|
+
message: "Must contain at least one action.",
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
checks.push({
|
|
269
|
+
name: "autonomy.bounded_actions",
|
|
270
|
+
passed: true,
|
|
271
|
+
message: `${bounded.length} bounded action(s)`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
// Disjoint check (ADR-039 Decision 10)
|
|
275
|
+
if (Array.isArray(a.forbidden_actions)) {
|
|
276
|
+
const forbidden = new Set(a.forbidden_actions.filter((x) => typeof x === "string"));
|
|
277
|
+
const overlap = bounded.filter((x) => typeof x === "string" && forbidden.has(x));
|
|
278
|
+
if (overlap.length > 0) {
|
|
279
|
+
checks.push({
|
|
280
|
+
name: "autonomy.bounded_actions",
|
|
281
|
+
passed: false,
|
|
282
|
+
message: `bounded_actions and forbidden_actions must be disjoint; both contain: ${overlap.join(", ")} (ADR-039 Decision 10).`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// escalation_triggers shape (ADR-039 Decision 10)
|
|
287
|
+
if (a.escalation_triggers !== undefined) {
|
|
288
|
+
if (!Array.isArray(a.escalation_triggers)) {
|
|
289
|
+
checks.push({
|
|
290
|
+
name: "autonomy.escalation_triggers",
|
|
291
|
+
passed: false,
|
|
292
|
+
message: "Must be an array (may be empty).",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
a.escalation_triggers.forEach((t, i) => {
|
|
297
|
+
if (!isObj(t)) {
|
|
298
|
+
checks.push({
|
|
299
|
+
name: `autonomy.escalation_triggers[${i}]`,
|
|
300
|
+
passed: false,
|
|
301
|
+
message: "Must be an object.",
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const tr = t;
|
|
306
|
+
if (typeof tr.condition !== "string" || tr.condition.length === 0) {
|
|
307
|
+
checks.push({
|
|
308
|
+
name: `autonomy.escalation_triggers[${i}].condition`,
|
|
309
|
+
passed: false,
|
|
310
|
+
message: "Required (string).",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (!ESCALATION_ACTIONS.includes(String(tr.action))) {
|
|
314
|
+
checks.push({
|
|
315
|
+
name: `autonomy.escalation_triggers[${i}].action`,
|
|
316
|
+
passed: false,
|
|
317
|
+
message: `Must be one of: ${ESCALATION_ACTIONS.join(", ")}.`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (typeof tr.reason !== "string" || tr.reason.length === 0) {
|
|
321
|
+
checks.push({
|
|
322
|
+
name: `autonomy.escalation_triggers[${i}].reason`,
|
|
323
|
+
passed: false,
|
|
324
|
+
message: "Required (string).",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
200
330
|
}
|
|
201
|
-
|
|
202
|
-
|
|
331
|
+
// ── audit (required: retention_days, queryable; query_endpoint when queryable=true) ──
|
|
332
|
+
if (!isObj(card.audit)) {
|
|
333
|
+
checks.push({
|
|
334
|
+
name: "audit",
|
|
335
|
+
passed: false,
|
|
336
|
+
message: "Required (object with retention_days, queryable, trace_format).",
|
|
337
|
+
});
|
|
203
338
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
339
|
+
else {
|
|
340
|
+
const a = card.audit;
|
|
341
|
+
if (typeof a.retention_days !== "number" || a.retention_days < 0) {
|
|
342
|
+
checks.push({
|
|
343
|
+
name: "audit.retention_days",
|
|
344
|
+
passed: false,
|
|
345
|
+
message: "Required (non-negative number).",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (typeof a.queryable !== "boolean") {
|
|
349
|
+
checks.push({
|
|
350
|
+
name: "audit.queryable",
|
|
351
|
+
passed: false,
|
|
352
|
+
message: "Required (boolean).",
|
|
353
|
+
});
|
|
210
354
|
}
|
|
211
|
-
if (
|
|
212
|
-
|
|
355
|
+
if (a.queryable === true && typeof a.query_endpoint !== "string") {
|
|
356
|
+
checks.push({
|
|
357
|
+
name: "audit.query_endpoint",
|
|
358
|
+
passed: false,
|
|
359
|
+
message: "Required when audit.queryable is true.",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
// ADR-039 Decision 7: tamper_evidence enum
|
|
363
|
+
if (a.tamper_evidence !== undefined && a.tamper_evidence !== null) {
|
|
364
|
+
if (!TAMPER_EVIDENCE.includes(String(a.tamper_evidence))) {
|
|
365
|
+
checks.push({
|
|
366
|
+
name: "audit.tamper_evidence",
|
|
367
|
+
passed: false,
|
|
368
|
+
message: `Must be one of: ${TAMPER_EVIDENCE.join(", ")}, or null.`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
213
371
|
}
|
|
214
|
-
|
|
215
|
-
|
|
372
|
+
// ADR-039 cutover: audit.storage no longer accepted
|
|
373
|
+
if (a.storage !== undefined) {
|
|
374
|
+
checks.push({
|
|
375
|
+
name: "audit.storage",
|
|
376
|
+
passed: false,
|
|
377
|
+
message: "Legacy field rejected. audit.storage is no longer accepted (ADR-039).",
|
|
378
|
+
});
|
|
216
379
|
}
|
|
217
380
|
}
|
|
218
|
-
//
|
|
219
|
-
if (card.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if
|
|
225
|
-
return fmt.badge(v, "cyan");
|
|
226
|
-
}
|
|
227
|
-
return fmt.badge(v, "magenta");
|
|
381
|
+
// ── conscience (optional, BOUNDARY+advisory rejected per ADR-039 Decision 10) ──
|
|
382
|
+
if (card.conscience !== undefined) {
|
|
383
|
+
if (!isObj(card.conscience)) {
|
|
384
|
+
checks.push({
|
|
385
|
+
name: "conscience",
|
|
386
|
+
passed: false,
|
|
387
|
+
message: "Must be an object if present.",
|
|
228
388
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const cns = card.conscience;
|
|
392
|
+
if (!CONSCIENCE_MODES.includes(String(cns.mode))) {
|
|
393
|
+
checks.push({
|
|
394
|
+
name: "conscience.mode",
|
|
395
|
+
passed: false,
|
|
396
|
+
message: `Must be one of: ${CONSCIENCE_MODES.join(", ")}.`,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (!Array.isArray(cns.values)) {
|
|
400
|
+
checks.push({
|
|
401
|
+
name: "conscience.values",
|
|
402
|
+
passed: false,
|
|
403
|
+
message: "Must be an array.",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
cns.values.forEach((v, i) => {
|
|
408
|
+
if (!isObj(v)) {
|
|
409
|
+
checks.push({
|
|
410
|
+
name: `conscience.values[${i}]`,
|
|
411
|
+
passed: false,
|
|
412
|
+
message: "Must be an object with type + content.",
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
238
415
|
}
|
|
239
|
-
|
|
416
|
+
const cv = v;
|
|
417
|
+
if (!CONSCIENCE_VALUE_TYPES.includes(String(cv.type))) {
|
|
418
|
+
checks.push({
|
|
419
|
+
name: `conscience.values[${i}].type`,
|
|
420
|
+
passed: false,
|
|
421
|
+
message: `Must be one of: ${CONSCIENCE_VALUE_TYPES.join(", ")}.`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (typeof cv.content !== "string" || cv.content.length === 0) {
|
|
425
|
+
checks.push({
|
|
426
|
+
name: `conscience.values[${i}].content`,
|
|
427
|
+
passed: false,
|
|
428
|
+
message: "Required (non-empty string).",
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
if (cv.severity !== undefined && !CONSCIENCE_SEVERITIES.includes(String(cv.severity))) {
|
|
432
|
+
checks.push({
|
|
433
|
+
name: `conscience.values[${i}].severity`,
|
|
434
|
+
passed: false,
|
|
435
|
+
message: `Must be one of: ${CONSCIENCE_SEVERITIES.join(", ")}.`,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (cv.type === "BOUNDARY" && cv.severity === "advisory") {
|
|
439
|
+
checks.push({
|
|
440
|
+
name: `conscience.values[${i}]`,
|
|
441
|
+
passed: false,
|
|
442
|
+
message: "BOUNDARY entries cannot have severity=advisory; BOUNDARY is inviolable by definition (ADR-039 Decision 10).",
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
});
|
|
240
446
|
}
|
|
241
447
|
}
|
|
242
448
|
}
|
|
243
|
-
//
|
|
244
|
-
if (card.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
console.log(` ${fmt.success(action)}`);
|
|
252
|
-
}
|
|
449
|
+
// ── enforcement (optional, ADR-039 Decision 3 user-facing knobs) ──
|
|
450
|
+
if (card.enforcement !== undefined) {
|
|
451
|
+
if (!isObj(card.enforcement)) {
|
|
452
|
+
checks.push({
|
|
453
|
+
name: "enforcement",
|
|
454
|
+
passed: false,
|
|
455
|
+
message: "Must be an object if present.",
|
|
456
|
+
});
|
|
253
457
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
458
|
+
else {
|
|
459
|
+
const e = card.enforcement;
|
|
460
|
+
if (e.allow_unmapped_tools !== undefined && typeof e.allow_unmapped_tools !== "boolean") {
|
|
461
|
+
checks.push({
|
|
462
|
+
name: "enforcement.allow_unmapped_tools",
|
|
463
|
+
passed: false,
|
|
464
|
+
message: "Must be a boolean.",
|
|
465
|
+
});
|
|
259
466
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
467
|
+
if (e.default_unmapped_severity !== undefined &&
|
|
468
|
+
!UNMAPPED_SEVERITIES.includes(String(e.default_unmapped_severity))) {
|
|
469
|
+
checks.push({
|
|
470
|
+
name: "enforcement.default_unmapped_severity",
|
|
471
|
+
passed: false,
|
|
472
|
+
message: `Must be one of: ${UNMAPPED_SEVERITIES.join(", ")}.`,
|
|
473
|
+
});
|
|
267
474
|
}
|
|
268
475
|
}
|
|
269
476
|
}
|
|
270
|
-
//
|
|
271
|
-
if (card.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
console.log(fmt.label(" Retention: ", `${card.audit_commitment.retention_days} days`));
|
|
477
|
+
// ── capabilities (optional; ADR-039 cutover rejects required_actions) ──
|
|
478
|
+
if (card.capabilities !== undefined) {
|
|
479
|
+
if (!isObj(card.capabilities)) {
|
|
480
|
+
checks.push({
|
|
481
|
+
name: "capabilities",
|
|
482
|
+
passed: false,
|
|
483
|
+
message: "Must be an object if present.",
|
|
484
|
+
});
|
|
279
485
|
}
|
|
280
|
-
|
|
281
|
-
|
|
486
|
+
else {
|
|
487
|
+
for (const [name, mapping] of Object.entries(card.capabilities)) {
|
|
488
|
+
if (!isObj(mapping)) {
|
|
489
|
+
checks.push({
|
|
490
|
+
name: `capabilities.${name}`,
|
|
491
|
+
passed: false,
|
|
492
|
+
message: "Must be an object.",
|
|
493
|
+
});
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const m = mapping;
|
|
497
|
+
if (m.tools !== undefined && !Array.isArray(m.tools)) {
|
|
498
|
+
checks.push({
|
|
499
|
+
name: `capabilities.${name}.tools`,
|
|
500
|
+
passed: false,
|
|
501
|
+
message: "Must be an array.",
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (m.required_actions !== undefined) {
|
|
505
|
+
checks.push({
|
|
506
|
+
name: `capabilities.${name}.required_actions`,
|
|
507
|
+
passed: false,
|
|
508
|
+
message: "Legacy field rejected. capabilities.<n>.required_actions is no longer accepted (ADR-039).",
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
282
512
|
}
|
|
283
513
|
}
|
|
284
|
-
|
|
285
|
-
if (card.extensions && Object.keys(card.extensions).length > 0) {
|
|
286
|
-
console.log(fmt.section("Extensions"));
|
|
287
|
-
console.log();
|
|
288
|
-
console.log(fmt.json(card.extensions));
|
|
289
|
-
}
|
|
290
|
-
console.log();
|
|
514
|
+
return checks;
|
|
291
515
|
}
|
|
516
|
+
/** @deprecated Use validateUnifiedCard instead */
|
|
517
|
+
export const validateCardJson = (raw) => validateUnifiedCard(JSON.parse(raw));
|
|
292
518
|
// ============================================================================
|
|
293
519
|
// Subcommands
|
|
294
520
|
// ============================================================================
|
|
295
521
|
export async function cardShowCommand(agentName) {
|
|
296
|
-
|
|
297
|
-
console.log("\n" + fmt.error("smoltbot is not initialized") + "\n");
|
|
298
|
-
console.log("Run `smoltbot init` to get started.\n");
|
|
299
|
-
process.exit(1);
|
|
300
|
-
}
|
|
301
|
-
const config = loadConfig();
|
|
302
|
-
if (!config) {
|
|
303
|
-
console.log("\n" + fmt.error("Failed to load configuration") + "\n");
|
|
304
|
-
process.exit(1);
|
|
305
|
-
}
|
|
306
|
-
const agent = await requireAgent(agentName);
|
|
307
|
-
if (!agent) {
|
|
308
|
-
console.log("\n" + fmt.error(`Agent not found${agentName ? `: ${agentName}` : ""}`) + "\n");
|
|
309
|
-
process.exit(1);
|
|
310
|
-
}
|
|
522
|
+
const agentId = await resolveAgentId(agentName);
|
|
311
523
|
console.log("\nFetching alignment card...\n");
|
|
312
524
|
try {
|
|
313
|
-
const
|
|
314
|
-
if (!
|
|
315
|
-
console.log(fmt.warn("No
|
|
316
|
-
console.log("\nPublish
|
|
317
|
-
console.log("
|
|
525
|
+
const { body, contentType } = await getAlignmentCard(agentId);
|
|
526
|
+
if (!body) {
|
|
527
|
+
console.log(fmt.warn("No alignment card found"));
|
|
528
|
+
console.log("\nPublish one with:\n");
|
|
529
|
+
console.log(" mnemom card publish <file.yaml>\n");
|
|
318
530
|
return;
|
|
319
531
|
}
|
|
320
|
-
|
|
532
|
+
// If the API returned YAML, print directly; otherwise convert
|
|
533
|
+
if (contentType.includes("yaml") || contentType.includes("text/yaml")) {
|
|
534
|
+
console.log(fmt.header("Alignment Card"));
|
|
535
|
+
console.log();
|
|
536
|
+
console.log(body);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
// JSON response — convert to YAML for display
|
|
540
|
+
const parsed = JSON.parse(body);
|
|
541
|
+
console.log(fmt.header("Alignment Card"));
|
|
542
|
+
console.log();
|
|
543
|
+
console.log(yaml.dump(parsed, { lineWidth: 120, noRefs: true }));
|
|
544
|
+
}
|
|
321
545
|
}
|
|
322
546
|
catch (error) {
|
|
323
547
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -325,22 +549,8 @@ export async function cardShowCommand(agentName) {
|
|
|
325
549
|
process.exit(1);
|
|
326
550
|
}
|
|
327
551
|
}
|
|
328
|
-
export async function cardPublishCommand(file, agentName) {
|
|
329
|
-
|
|
330
|
-
console.log("\n" + fmt.error("smoltbot is not initialized") + "\n");
|
|
331
|
-
console.log("Run `smoltbot init` to get started.\n");
|
|
332
|
-
process.exit(1);
|
|
333
|
-
}
|
|
334
|
-
const config = loadConfig();
|
|
335
|
-
if (!config) {
|
|
336
|
-
console.log("\n" + fmt.error("Failed to load configuration") + "\n");
|
|
337
|
-
process.exit(1);
|
|
338
|
-
}
|
|
339
|
-
const agent = await requireAgent(agentName);
|
|
340
|
-
if (!agent) {
|
|
341
|
-
console.log("\n" + fmt.error(`Agent not found${agentName ? `: ${agentName}` : ""}`) + "\n");
|
|
342
|
-
process.exit(1);
|
|
343
|
-
}
|
|
552
|
+
export async function cardPublishCommand(file, agentName, options = {}) {
|
|
553
|
+
const agentId = await resolveAgentId(agentName);
|
|
344
554
|
// Resolve file path
|
|
345
555
|
const filePath = path.resolve(file);
|
|
346
556
|
if (!fs.existsSync(filePath)) {
|
|
@@ -349,11 +559,8 @@ export async function cardPublishCommand(file, agentName) {
|
|
|
349
559
|
}
|
|
350
560
|
// Parse file (JSON or YAML)
|
|
351
561
|
let parsed;
|
|
352
|
-
let format;
|
|
353
562
|
try {
|
|
354
|
-
|
|
355
|
-
parsed = result.parsed;
|
|
356
|
-
format = result.format;
|
|
563
|
+
parsed = parseCardFile(filePath);
|
|
357
564
|
}
|
|
358
565
|
catch (e) {
|
|
359
566
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -361,10 +568,10 @@ export async function cardPublishCommand(file, agentName) {
|
|
|
361
568
|
process.exit(1);
|
|
362
569
|
}
|
|
363
570
|
// Validate locally
|
|
364
|
-
const checks =
|
|
571
|
+
const checks = validateUnifiedCard(parsed.parsed);
|
|
365
572
|
const allPassed = checks.every((c) => c.passed);
|
|
366
573
|
console.log(fmt.header("Card Validation"));
|
|
367
|
-
console.log(fmt.label(" Format:", ` ${format.toUpperCase()}`));
|
|
574
|
+
console.log(fmt.label(" Format:", ` ${parsed.format.toUpperCase()}`));
|
|
368
575
|
console.log();
|
|
369
576
|
for (const check of checks) {
|
|
370
577
|
if (check.passed) {
|
|
@@ -383,27 +590,32 @@ export async function cardPublishCommand(file, agentName) {
|
|
|
383
590
|
await requireAuth();
|
|
384
591
|
// Confirm with user
|
|
385
592
|
if (isInteractive()) {
|
|
386
|
-
const confirm = await askYesNo(`Publish this card for agent ${
|
|
593
|
+
const confirm = await askYesNo(`Publish this alignment card for agent ${agentId}?`, false);
|
|
387
594
|
if (!confirm) {
|
|
388
595
|
console.log("\nPublish cancelled.\n");
|
|
389
596
|
return;
|
|
390
597
|
}
|
|
391
598
|
}
|
|
392
|
-
// Publish
|
|
599
|
+
// Publish — send in source format
|
|
393
600
|
try {
|
|
394
|
-
console.log("\nPublishing card...");
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
601
|
+
console.log("\nPublishing alignment card...");
|
|
602
|
+
const contentType = parsed.format === "yaml" ? "text/yaml" : "application/json";
|
|
603
|
+
const body = parsed.format === "yaml" ? parsed.raw : JSON.stringify(parsed.parsed);
|
|
604
|
+
const bodyBytes = Buffer.byteLength(body, "utf-8");
|
|
605
|
+
if (bodyBytes > ALIGNMENT_CARD_MAX_BYTES) {
|
|
606
|
+
console.log("\n" +
|
|
607
|
+
fmt.error(`Alignment card is ${bodyBytes} bytes; limit is ${ALIGNMENT_CARD_MAX_BYTES} bytes (128 KB). The API will return 413.`) +
|
|
608
|
+
"\n");
|
|
609
|
+
process.exit(1);
|
|
403
610
|
}
|
|
404
|
-
|
|
405
|
-
|
|
611
|
+
const result = await putAlignmentCard(agentId, body, contentType, {
|
|
612
|
+
idempotencyKey: options.idempotencyKey,
|
|
613
|
+
});
|
|
614
|
+
console.log(fmt.success("Alignment card published!"));
|
|
615
|
+
if (typeof result.card_id === "string" && result.card_id.length > 0) {
|
|
616
|
+
console.log(fmt.label(" Card ID:", ` ${result.card_id}`));
|
|
406
617
|
}
|
|
618
|
+
console.log();
|
|
407
619
|
}
|
|
408
620
|
catch (error) {
|
|
409
621
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -420,11 +632,8 @@ export async function cardValidateCommand(file) {
|
|
|
420
632
|
}
|
|
421
633
|
// Parse file (JSON or YAML)
|
|
422
634
|
let parsed;
|
|
423
|
-
let format;
|
|
424
635
|
try {
|
|
425
|
-
|
|
426
|
-
parsed = result.parsed;
|
|
427
|
-
format = result.format;
|
|
636
|
+
parsed = parseCardFile(filePath);
|
|
428
637
|
}
|
|
429
638
|
catch (e) {
|
|
430
639
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -432,14 +641,14 @@ export async function cardValidateCommand(file) {
|
|
|
432
641
|
process.exit(1);
|
|
433
642
|
}
|
|
434
643
|
// Validate the parsed object
|
|
435
|
-
const checks =
|
|
644
|
+
const checks = validateUnifiedCard(parsed.parsed);
|
|
436
645
|
const allPassed = checks.every((c) => c.passed);
|
|
437
646
|
const passCount = checks.filter((c) => c.passed).length;
|
|
438
647
|
const failCount = checks.filter((c) => !c.passed).length;
|
|
439
648
|
console.log(fmt.header("Card Validation Report"));
|
|
440
649
|
console.log();
|
|
441
650
|
console.log(fmt.label(" File:", ` ${filePath}`));
|
|
442
|
-
console.log(fmt.label(" Format:", ` ${format.toUpperCase()}`));
|
|
651
|
+
console.log(fmt.label(" Format:", ` ${parsed.format.toUpperCase()}`));
|
|
443
652
|
console.log();
|
|
444
653
|
for (const check of checks) {
|
|
445
654
|
if (check.passed) {
|
|
@@ -458,3 +667,234 @@ export async function cardValidateCommand(file) {
|
|
|
458
667
|
process.exit(1);
|
|
459
668
|
}
|
|
460
669
|
}
|
|
670
|
+
export async function cardEditCommand(agentName, options = {}) {
|
|
671
|
+
const agentId = await resolveAgentId(agentName);
|
|
672
|
+
await requireAuth();
|
|
673
|
+
// Fetch current card as YAML
|
|
674
|
+
console.log("\nFetching current alignment card...\n");
|
|
675
|
+
const { body: original } = await getAlignmentCard(agentId);
|
|
676
|
+
if (!original) {
|
|
677
|
+
console.log(fmt.warn("No alignment card found. Creating a template..."));
|
|
678
|
+
}
|
|
679
|
+
const cardYaml = original || yaml.dump({
|
|
680
|
+
card_version: "unified/2026-04-26",
|
|
681
|
+
agent_id: agentId,
|
|
682
|
+
autonomy_mode: "observe",
|
|
683
|
+
integrity_mode: "observe",
|
|
684
|
+
principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
|
|
685
|
+
values: { declared: ["transparency", "safety", "honesty"] },
|
|
686
|
+
autonomy: {
|
|
687
|
+
bounded_actions: ["respond_to_prompts"],
|
|
688
|
+
forbidden_actions: [],
|
|
689
|
+
escalation_triggers: [],
|
|
690
|
+
},
|
|
691
|
+
audit: { retention_days: 30, queryable: false, trace_format: "otel" },
|
|
692
|
+
}, { lineWidth: 120, noRefs: true });
|
|
693
|
+
// Write to temp file
|
|
694
|
+
const tmpDir = os.tmpdir();
|
|
695
|
+
const tmpFile = path.join(tmpDir, `mnemom-card-${agentId}.yaml`);
|
|
696
|
+
fs.writeFileSync(tmpFile, cardYaml);
|
|
697
|
+
// Open in editor
|
|
698
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
699
|
+
console.log(`Opening ${editor}...`);
|
|
700
|
+
const result = spawnSync(editor, [tmpFile], { stdio: "inherit" });
|
|
701
|
+
if (result.status !== 0) {
|
|
702
|
+
console.log("\n" + fmt.error("Editor exited with an error") + "\n");
|
|
703
|
+
try {
|
|
704
|
+
fs.unlinkSync(tmpFile);
|
|
705
|
+
}
|
|
706
|
+
catch { /* ignore */ }
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
// Read back and compare
|
|
710
|
+
const edited = fs.readFileSync(tmpFile, "utf-8");
|
|
711
|
+
try {
|
|
712
|
+
fs.unlinkSync(tmpFile);
|
|
713
|
+
}
|
|
714
|
+
catch { /* ignore */ }
|
|
715
|
+
if (edited === cardYaml) {
|
|
716
|
+
console.log("\nNo changes made.\n");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
// Validate
|
|
720
|
+
let parsed;
|
|
721
|
+
try {
|
|
722
|
+
parsed = yaml.load(edited);
|
|
723
|
+
if (!parsed || typeof parsed !== "object")
|
|
724
|
+
throw new Error("Invalid YAML");
|
|
725
|
+
}
|
|
726
|
+
catch (e) {
|
|
727
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
728
|
+
console.log("\n" + fmt.error(`Invalid YAML: ${msg}`) + "\n");
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
const checks = validateUnifiedCard(parsed);
|
|
732
|
+
const allPassed = checks.every((c) => c.passed);
|
|
733
|
+
if (!allPassed) {
|
|
734
|
+
console.log(fmt.header("Validation Errors"));
|
|
735
|
+
console.log();
|
|
736
|
+
for (const check of checks.filter((c) => !c.passed)) {
|
|
737
|
+
console.log(fmt.error(`${check.name}: ${check.message}`));
|
|
738
|
+
}
|
|
739
|
+
console.log();
|
|
740
|
+
console.log(fmt.error("Validation failed. Card not published.") + "\n");
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
// Confirm and publish
|
|
744
|
+
if (isInteractive()) {
|
|
745
|
+
const confirm = await askYesNo("Publish updated alignment card?", true);
|
|
746
|
+
if (!confirm) {
|
|
747
|
+
console.log("\nPublish cancelled.\n");
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
console.log("\nPublishing alignment card...");
|
|
753
|
+
const editedBytes = Buffer.byteLength(edited, "utf-8");
|
|
754
|
+
if (editedBytes > ALIGNMENT_CARD_MAX_BYTES) {
|
|
755
|
+
console.log("\n" +
|
|
756
|
+
fmt.error(`Alignment card is ${editedBytes} bytes; limit is ${ALIGNMENT_CARD_MAX_BYTES} bytes (128 KB). The API will return 413.`) +
|
|
757
|
+
"\n");
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
const putResult = await putAlignmentCard(agentId, edited, "text/yaml", {
|
|
761
|
+
idempotencyKey: options.idempotencyKey,
|
|
762
|
+
});
|
|
763
|
+
console.log(fmt.success("Alignment card published!"));
|
|
764
|
+
if (typeof putResult.card_id === "string" && putResult.card_id.length > 0) {
|
|
765
|
+
console.log(fmt.label(" Card ID:", ` ${putResult.card_id}`));
|
|
766
|
+
}
|
|
767
|
+
console.log();
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
771
|
+
console.log("\n" + fmt.error(`Failed to publish card: ${message}`) + "\n");
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* mnemom card evaluate <card-file> --tools <tools> -- local CI/CD evaluation
|
|
777
|
+
*
|
|
778
|
+
* Runs entirely locally using the embedded policy engine. No API key needed.
|
|
779
|
+
* The card IS the policy source -- capabilities and enforcement sections
|
|
780
|
+
* are extracted by @mnemom/policy-engine 0.3.0's evaluatePolicy().
|
|
781
|
+
*/
|
|
782
|
+
export async function cardEvaluateCommand(file, options) {
|
|
783
|
+
// 1. Read + validate card file
|
|
784
|
+
const cardPath = path.resolve(file);
|
|
785
|
+
if (!fs.existsSync(cardPath)) {
|
|
786
|
+
console.log("\n" + fmt.error(`Card file not found: ${cardPath}`) + "\n");
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
let parsed;
|
|
790
|
+
try {
|
|
791
|
+
parsed = parseCardFile(cardPath);
|
|
792
|
+
}
|
|
793
|
+
catch (e) {
|
|
794
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
795
|
+
console.log("\n" + fmt.error(`Could not parse card file: ${msg}`) + "\n");
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
const checks = validateUnifiedCard(parsed.parsed);
|
|
799
|
+
const allPassed = checks.every((c) => c.passed);
|
|
800
|
+
if (!allPassed) {
|
|
801
|
+
console.log(fmt.error("Card validation failed:"));
|
|
802
|
+
for (const check of checks.filter((c) => !c.passed)) {
|
|
803
|
+
console.log(fmt.error(` ${check.name}: ${check.message}`));
|
|
804
|
+
}
|
|
805
|
+
console.log();
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
// 2. Parse tool list from --tools or --tool-manifest
|
|
809
|
+
let tools = [];
|
|
810
|
+
if (options.tools) {
|
|
811
|
+
tools = options.tools.split(",").map((t) => ({ name: t.trim() })).filter((t) => t.name);
|
|
812
|
+
}
|
|
813
|
+
else if (options.toolManifest) {
|
|
814
|
+
const manifestPath = path.resolve(options.toolManifest);
|
|
815
|
+
if (!fs.existsSync(manifestPath)) {
|
|
816
|
+
console.log("\n" + fmt.error(`Tool manifest file not found: ${manifestPath}`) + "\n");
|
|
817
|
+
process.exit(1);
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const manifestRaw = fs.readFileSync(manifestPath, "utf-8");
|
|
821
|
+
const manifest = JSON.parse(manifestRaw);
|
|
822
|
+
if (Array.isArray(manifest)) {
|
|
823
|
+
tools = manifest.map((t) => typeof t === "string" ? { name: t } : { name: t.name });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
catch (e) {
|
|
827
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
828
|
+
console.log("\n" + fmt.error(`Could not read tool manifest: ${msg}`) + "\n");
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (tools.length === 0) {
|
|
833
|
+
console.log("\n" + fmt.error("No tools specified. Use --tools or --tool-manifest") + "\n");
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
// 3. Run evaluation -- card IS the policy source
|
|
837
|
+
const result = evaluatePolicy({
|
|
838
|
+
context: "cicd",
|
|
839
|
+
card: parsed.parsed,
|
|
840
|
+
tools,
|
|
841
|
+
});
|
|
842
|
+
// 4. Display results
|
|
843
|
+
console.log(fmt.header("Card Policy Evaluation"));
|
|
844
|
+
console.log();
|
|
845
|
+
const principal = parsed.parsed.principal;
|
|
846
|
+
console.log(fmt.label(" Card:", ` ${principal?.name ?? path.basename(cardPath)}`));
|
|
847
|
+
console.log(fmt.label(" Context:", " cicd"));
|
|
848
|
+
console.log(fmt.label(" Tools:", ` ${tools.length} (${tools.map((t) => t.name).join(", ")})`));
|
|
849
|
+
console.log();
|
|
850
|
+
// Verdict
|
|
851
|
+
if (result.verdict === "pass") {
|
|
852
|
+
console.log(fmt.success("PASS -- all tools comply with card policy"));
|
|
853
|
+
}
|
|
854
|
+
else if (result.verdict === "warn") {
|
|
855
|
+
console.log(fmt.warn("WARN -- policy warnings detected"));
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
console.log(fmt.error("FAIL -- policy violations detected"));
|
|
859
|
+
}
|
|
860
|
+
console.log();
|
|
861
|
+
// Violations
|
|
862
|
+
if (result.violations.length > 0) {
|
|
863
|
+
console.log(fmt.section("Violations"));
|
|
864
|
+
console.log();
|
|
865
|
+
for (const v of result.violations) {
|
|
866
|
+
console.log(fmt.error(` ${v.tool} [${v.severity}] -- ${v.type}`));
|
|
867
|
+
console.log(` ${v.reason}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// Warnings
|
|
871
|
+
if (result.warnings.length > 0) {
|
|
872
|
+
console.log(fmt.section("Warnings"));
|
|
873
|
+
console.log();
|
|
874
|
+
for (const w of result.warnings) {
|
|
875
|
+
console.log(fmt.warn(` ${w.tool} -- ${w.type}`));
|
|
876
|
+
console.log(` ${w.reason}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// Coverage
|
|
880
|
+
const cov = result.coverage;
|
|
881
|
+
console.log(fmt.section("Coverage"));
|
|
882
|
+
console.log();
|
|
883
|
+
console.log(fmt.label(" Card actions:", ` ${cov.total_card_actions}`));
|
|
884
|
+
console.log(fmt.label(" Mapped: ", ` ${cov.mapped_card_actions.length}`));
|
|
885
|
+
console.log(fmt.label(" Unmapped: ", ` ${cov.unmapped_card_actions.length}`));
|
|
886
|
+
console.log(fmt.label(" Coverage: ", ` ${cov.coverage_pct.toFixed(1)}%`));
|
|
887
|
+
if (cov.unmapped_card_actions.length > 0) {
|
|
888
|
+
console.log(fmt.label(" Unmapped list:", ` ${cov.unmapped_card_actions.join(", ")}`));
|
|
889
|
+
}
|
|
890
|
+
console.log();
|
|
891
|
+
console.log(fmt.label(" Duration:", ` ${result.duration_ms}ms`));
|
|
892
|
+
console.log();
|
|
893
|
+
// 5. Exit code
|
|
894
|
+
if (result.verdict === "fail") {
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
897
|
+
if (options.strict && result.verdict === "warn") {
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
|
900
|
+
}
|