@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.
@@ -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 { configExists, loadConfig, requireAgent } from "../lib/config.js";
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
- // Standard AAP values that do not require custom definitions
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
- /** @deprecated Use validateCard instead */
45
- export const validateCardJson = validateCard;
46
- export function validateCard(raw) {
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
- // Check 1: Valid JSON
49
- let card;
50
- try {
51
- card = JSON.parse(raw);
52
- checks.push({ name: "Valid JSON", passed: true, message: "Parsed successfully" });
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
- catch (e) {
55
- const msg = e instanceof Error ? e.message : String(e);
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
- // Check 2: Required blocks
60
- const requiredBlocks = ["principal", "values", "autonomy_envelope", "audit_commitment"];
61
- for (const block of requiredBlocks) {
62
- if (card[block] && typeof card[block] === "object") {
63
- checks.push({ name: `Block: ${block}`, passed: true, message: "Present" });
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
- // Check 3: values.declared is non-empty array
70
- const declared = card.values?.declared;
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: "values.declared",
74
- passed: true,
75
- message: `${declared.length} value(s) declared`,
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: "values.declared",
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: "Must be a non-empty array",
116
+ message: "Legacy field rejected. Use top-level integrity_mode instead (ADR-039).",
83
117
  });
84
118
  }
85
- // Check 4: Custom values have definitions
86
- if (Array.isArray(declared)) {
87
- const definitions = card.values?.definitions || {};
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: "Custom value definitions",
93
- passed: true,
94
- message: customValues.length === 0
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
- else {
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: "Custom value definitions",
137
+ name: "enforcement.fail_open",
102
138
  passed: false,
103
- message: `Missing definitions for: ${missingDefs.join(", ")}`,
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
- // Check 5: bounded_actions is non-empty array
108
- const bounded = card.autonomy_envelope?.bounded_actions;
109
- if (Array.isArray(bounded) && bounded.length > 0) {
143
+ // _composition is system-managed
144
+ if (card._composition !== undefined) {
110
145
  checks.push({
111
- name: "bounded_actions",
112
- passed: true,
113
- message: `${bounded.length} bounded action(s)`,
146
+ name: "_composition",
147
+ passed: false,
148
+ message: "System-managed field — cannot be set on inbound cards.",
114
149
  });
115
150
  }
116
- else {
151
+ // ── principal (required: type + relationship; identifier when type != unspecified) ──
152
+ if (!isObj(card.principal)) {
117
153
  checks.push({
118
- name: "bounded_actions",
154
+ name: "principal",
119
155
  passed: false,
120
- message: "Must be a non-empty array",
156
+ message: "Required (object with at least type + relationship).",
121
157
  });
122
158
  }
123
- // Check 6: escalation_triggers conditions are evaluable
124
- // Accepts: simple identifiers, tool_matches('...') calls, and logical expressions
125
- // (comparisons with ==, !=, >, <, and/or, quoted strings, numbers)
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: "escalation_triggers",
133
- passed: true,
134
- message: `${triggers.length} trigger(s), all valid`,
163
+ name: "principal.type",
164
+ passed: false,
165
+ message: `Must be one of: ${PRINCIPAL_TYPES.join(", ")}.`,
135
166
  });
136
167
  }
137
- else {
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: "escalation_triggers",
180
+ name: "principal.identifier",
140
181
  passed: false,
141
- message: `${invalidTriggers.length} trigger(s) with invalid conditions`,
182
+ message: 'Required when principal.type is not "unspecified" (ADR-039 Decision 10).',
142
183
  });
143
184
  }
144
185
  }
145
- else if (Array.isArray(triggers) && triggers.length === 0) {
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: "escalation_triggers",
148
- passed: true,
149
- message: "No triggers defined",
189
+ name: "values.declared",
190
+ passed: false,
191
+ message: "Required (non-empty array of strings).",
150
192
  });
151
193
  }
152
- // Check 7: expires_at not already expired
153
- if (card.expires_at) {
154
- const expiresDate = new Date(card.expires_at);
155
- if (isNaN(expiresDate.getTime())) {
194
+ else {
195
+ const v = card.values;
196
+ const decl = v.declared;
197
+ if (decl.length === 0) {
156
198
  checks.push({
157
- name: "expires_at",
199
+ name: "values.declared",
158
200
  passed: false,
159
- message: "Invalid date format",
201
+ message: "Must contain at least one value.",
160
202
  });
161
203
  }
162
- else if (expiresDate.getTime() < Date.now()) {
204
+ else if (!decl.every(s => typeof s === "string")) {
163
205
  checks.push({
164
- name: "expires_at",
206
+ name: "values.declared",
165
207
  passed: false,
166
- message: `Already expired: ${card.expires_at}`,
208
+ message: "All entries must be strings.",
167
209
  });
168
210
  }
169
211
  else {
170
212
  checks.push({
171
- name: "expires_at",
213
+ name: "values.declared",
172
214
  passed: true,
173
- message: `Valid until ${card.expires_at}`,
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
- return checks;
178
- }
179
- /**
180
- * Validate a pre-parsed card object (used for YAML cards).
181
- * Re-serializes to JSON so the same checks run identically.
182
- */
183
- export function validateCardObject(card) {
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
- if (card.issued_at) {
199
- console.log(fmt.label(" Issued: ", card.issued_at));
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
- if (card.expires_at) {
202
- console.log(fmt.label(" Expires: ", card.expires_at));
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
- // Principal
205
- if (card.principal) {
206
- console.log(fmt.section("Principal"));
207
- console.log();
208
- if (card.principal.name) {
209
- console.log(fmt.label(" Name: ", card.principal.name));
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 (card.principal.type) {
212
- console.log(fmt.label(" Type: ", card.principal.type));
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
- if (card.principal.organization) {
215
- console.log(fmt.label(" Organization:", ` ${card.principal.organization}`));
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
- // Values
219
- if (card.values) {
220
- console.log(fmt.section("Values"));
221
- console.log();
222
- if (Array.isArray(card.values.declared) && card.values.declared.length > 0) {
223
- const badges = card.values.declared.map((v) => {
224
- if (STANDARD_VALUES.has(v)) {
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
- console.log(` ${badges.join(" ")}`);
230
- // Show definitions for custom values
231
- const definitions = card.values.definitions || {};
232
- const customValues = card.values.declared.filter((v) => !STANDARD_VALUES.has(v));
233
- if (customValues.length > 0) {
234
- console.log();
235
- for (const v of customValues) {
236
- if (definitions[v]) {
237
- console.log(fmt.label(` ${v}:`, ` ${definitions[v]}`));
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
- // Autonomy envelope
244
- if (card.autonomy_envelope) {
245
- console.log(fmt.section("Autonomy Envelope"));
246
- console.log();
247
- const bounded = card.autonomy_envelope.bounded_actions;
248
- if (Array.isArray(bounded) && bounded.length > 0) {
249
- console.log(" Bounded actions:");
250
- for (const action of bounded) {
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
- const forbidden = card.autonomy_envelope.forbidden_actions;
255
- if (Array.isArray(forbidden) && forbidden.length > 0) {
256
- console.log(" Forbidden actions:");
257
- for (const action of forbidden) {
258
- console.log(` ${fmt.error(action)}`);
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
- const triggers = card.autonomy_envelope.escalation_triggers;
262
- if (Array.isArray(triggers) && triggers.length > 0) {
263
- console.log(" Escalation triggers:");
264
- for (const trigger of triggers) {
265
- const action = trigger.action ? ` -> ${trigger.action}` : "";
266
- console.log(` ${fmt.warn(`${trigger.condition}${action}`)}`);
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
- // Audit commitment
271
- if (card.audit_commitment) {
272
- console.log(fmt.section("Audit Commitment"));
273
- console.log();
274
- if (card.audit_commitment.log_level) {
275
- console.log(fmt.label(" Log level: ", card.audit_commitment.log_level));
276
- }
277
- if (card.audit_commitment.retention_days != null) {
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
- if (card.audit_commitment.access_policy) {
281
- console.log(fmt.label(" Access policy: ", card.audit_commitment.access_policy));
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
- // Extensions
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
- if (!configExists()) {
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 cardResponse = await getCard(agent.agentId);
314
- if (!cardResponse) {
315
- console.log(fmt.warn("No custom card -- using default"));
316
- console.log("\nPublish a custom card with:\n");
317
- console.log(" smoltbot card publish <file.json>\n");
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
- displayCard(cardResponse);
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
- if (!configExists()) {
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
- const result = parseCardFile(filePath);
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 = validateCardObject(parsed);
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 ${agent.agentId}?`, false);
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 (always sends JSON to API regardless of source format)
599
+ // Publish send in source format
393
600
  try {
394
- console.log("\nPublishing card...");
395
- const result = await updateCard(agent.agentId, parsed);
396
- console.log(fmt.success(`Card published successfully!`));
397
- console.log(fmt.label(" Card ID:", ` ${result.card_id}`) + "\n");
398
- // Trigger re-verification
399
- try {
400
- console.log("Triggering re-verification...");
401
- const reverifyResult = await reverifyAgent(agent.agentId);
402
- console.log(fmt.success(`Re-verification started (${reverifyResult.reverified} traces queued)`) + "\n");
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
- catch {
405
- console.log(fmt.warn("Could not trigger re-verification (non-critical)") + "\n");
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
- const result = parseCardFile(filePath);
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 = validateCardObject(parsed);
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
+ }