@mnemom/mnemom 0.16.2 → 0.16.3
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/README.md +1 -0
- package/dist/commands/agents.d.ts +14 -0
- package/dist/commands/agents.js +100 -2
- package/dist/commands/card.d.ts +43 -0
- package/dist/commands/card.js +153 -102
- package/dist/commands/logs.js +11 -1
- package/dist/commands/onboard.d.ts +59 -0
- package/dist/commands/onboard.js +395 -0
- package/dist/commands/org.d.ts +13 -0
- package/dist/commands/org.js +63 -2
- package/dist/commands/protection.d.ts +10 -0
- package/dist/commands/protection.js +109 -0
- package/dist/commands/status.js +5 -0
- package/dist/commands/try-me.js +9 -0
- package/dist/commands/usage.d.ts +35 -0
- package/dist/commands/usage.js +265 -0
- package/dist/commands/wrap.d.ts +25 -0
- package/dist/commands/wrap.js +331 -0
- package/dist/index.js +192 -6
- package/dist/lib/agent-config.d.ts +27 -0
- package/dist/lib/agent-config.js +86 -0
- package/dist/lib/api.d.ts +122 -1
- package/dist/lib/api.js +128 -183
- package/dist/lib/auth.js +21 -1
- package/dist/lib/cli-config.d.ts +33 -0
- package/dist/lib/cli-config.js +70 -0
- package/dist/lib/config.d.ts +10 -0
- package/dist/lib/config.js +39 -3
- package/dist/lib/keyed-identity.d.ts +35 -0
- package/dist/lib/keyed-identity.js +363 -0
- package/dist/lib/oauth.d.ts +26 -4
- package/dist/lib/oauth.js +98 -29
- package/dist/lib/protection-drift.d.ts +117 -0
- package/dist/lib/protection-drift.js +180 -0
- package/dist/lib/skills.js +25 -12
- package/dist/lib/version-gate.d.ts +37 -0
- package/dist/lib/version-gate.js +84 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ That's it. `mnemom init` detects your configured AI provider API keys (Anthropic
|
|
|
43
43
|
| `mnemom card show` | Display active alignment card |
|
|
44
44
|
| `mnemom card publish <file>` | Publish alignment card from JSON file |
|
|
45
45
|
| `mnemom card validate <file>` | Validate card JSON locally |
|
|
46
|
+
| `mnemom usage --org <id>` | Show per-person token and request consumption |
|
|
46
47
|
|
|
47
48
|
## How It Works
|
|
48
49
|
|
|
@@ -35,3 +35,17 @@ export declare function deriveHashProof(apiKey: string, agentName?: string): str
|
|
|
35
35
|
* listing your claimable orgs.
|
|
36
36
|
*/
|
|
37
37
|
export declare function agentsClaimCommand(idOrName: string, options?: AgentsClaimOptions): Promise<void>;
|
|
38
|
+
export interface AgentsMoveOptions {
|
|
39
|
+
/** Destination org slug or id — REQUIRED and always explicit (a move is an
|
|
40
|
+
* administrative action; it deliberately does NOT default to the active org). */
|
|
41
|
+
to?: string;
|
|
42
|
+
json?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `mnemom agents move <id-or-name> --to <slug|id>`
|
|
46
|
+
*
|
|
47
|
+
* Role-based relocation between orgs (the counterpart to re-claim, which is
|
|
48
|
+
* possession-based). The server requires the caller to be an owner/admin of
|
|
49
|
+
* BOTH the agent's current org and the destination — no agent key involved.
|
|
50
|
+
*/
|
|
51
|
+
export declare function agentsMoveCommand(idOrName: string, options?: AgentsMoveOptions): Promise<void>;
|
package/dist/commands/agents.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { listAgents, listOrgAgents, listMyOrgs, getAgent, getAgentByName, claimAgent, MnemomApiError, } from "../lib/api.js";
|
|
2
|
+
import { listAgents, listOrgAgents, listMyOrgs, getAgent, getAgentByName, claimAgent, moveAgent, MnemomApiError, } from "../lib/api.js";
|
|
3
3
|
import { requireAuth } from "../lib/auth.js";
|
|
4
|
+
import { getActiveOrg } from "../lib/cli-config.js";
|
|
4
5
|
import { fmt } from "../lib/format.js";
|
|
5
6
|
export async function agentsListCommand(options = {}) {
|
|
6
7
|
const cred = await requireAuth();
|
|
@@ -182,7 +183,11 @@ export async function agentsClaimCommand(idOrName, options = {}) {
|
|
|
182
183
|
process.exit(1);
|
|
183
184
|
return;
|
|
184
185
|
}
|
|
185
|
-
// 3. Resolve
|
|
186
|
+
// 3. Resolve the destination org. Precedence: explicit --org > the active
|
|
187
|
+
// org (`mnemom org use`, lib/cli-config.ts) > server default (personal
|
|
188
|
+
// org) — the last LOUDLY, because login binds no org and a silent
|
|
189
|
+
// personal-org default is the classic "why is my agent in Personal?"
|
|
190
|
+
// footgun.
|
|
186
191
|
let orgId;
|
|
187
192
|
let orgs = [];
|
|
188
193
|
if (options.org) {
|
|
@@ -205,6 +210,24 @@ export async function agentsClaimCommand(idOrName, options = {}) {
|
|
|
205
210
|
return;
|
|
206
211
|
}
|
|
207
212
|
}
|
|
213
|
+
else {
|
|
214
|
+
const active = getActiveOrg();
|
|
215
|
+
if (active) {
|
|
216
|
+
// Send the stored org_id directly (validated at `org use` time); if
|
|
217
|
+
// membership has since changed the server 403s with the teaching list.
|
|
218
|
+
orgId = active.org_id;
|
|
219
|
+
// stderr, and skipped in --json mode: stdout stays machine-readable.
|
|
220
|
+
if (!options.json) {
|
|
221
|
+
console.error(fmt.dim(` Using active org ${active.name} (${active.slug}).`) + "\n");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (!options.json) {
|
|
225
|
+
console.error(fmt.warn("No --org and no active org set — this agent will land in your PERSONAL org.") +
|
|
226
|
+
"\n" +
|
|
227
|
+
fmt.dim(" Set a default with `mnemom org use <slug>`, or pass --org <slug> (see: mnemom org list).") +
|
|
228
|
+
"\n");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
208
231
|
// 4. Claim.
|
|
209
232
|
let result;
|
|
210
233
|
try {
|
|
@@ -311,3 +334,78 @@ export async function agentsClaimCommand(idOrName, options = {}) {
|
|
|
311
334
|
}
|
|
312
335
|
console.log();
|
|
313
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* `mnemom agents move <id-or-name> --to <slug|id>`
|
|
339
|
+
*
|
|
340
|
+
* Role-based relocation between orgs (the counterpart to re-claim, which is
|
|
341
|
+
* possession-based). The server requires the caller to be an owner/admin of
|
|
342
|
+
* BOTH the agent's current org and the destination — no agent key involved.
|
|
343
|
+
*/
|
|
344
|
+
export async function agentsMoveCommand(idOrName, options = {}) {
|
|
345
|
+
await requireAuth();
|
|
346
|
+
if (!options.to) {
|
|
347
|
+
console.log(fmt.error("--to <org-slug-or-id> is required.") + "\n");
|
|
348
|
+
console.log(fmt.dim(" A move is explicit by design — it never defaults to the active org. See: mnemom org list") + "\n");
|
|
349
|
+
process.exit(1);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Resolve the agent. Moves act on agents already in your fleet, so a
|
|
353
|
+
// name-shaped arg resolves through the fleet like `agents list` sees it.
|
|
354
|
+
let agentId = idOrName;
|
|
355
|
+
if (!AGENT_ID_RE.test(idOrName)) {
|
|
356
|
+
const found = await getAgentByName(idOrName).catch(() => null);
|
|
357
|
+
if (!found) {
|
|
358
|
+
console.log(fmt.error(`Could not resolve '${idOrName}' to an agent in your fleet.`) + "\n");
|
|
359
|
+
process.exit(1);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
agentId = found.id;
|
|
363
|
+
}
|
|
364
|
+
// Resolve --to against the caller's memberships (teaching error otherwise).
|
|
365
|
+
let orgs;
|
|
366
|
+
try {
|
|
367
|
+
orgs = await listMyOrgs();
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
371
|
+
console.log(fmt.error(`Failed to read your orgs: ${msg}`) + "\n");
|
|
372
|
+
process.exit(1);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
let destOrgId;
|
|
376
|
+
try {
|
|
377
|
+
destOrgId = resolveOrgId(options.to, orgs);
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
381
|
+
console.log(fmt.error(msg) + "\n");
|
|
382
|
+
process.exit(1);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const result = await moveAgent(agentId, destOrgId);
|
|
387
|
+
if (options.json) {
|
|
388
|
+
console.log(JSON.stringify(result, null, 2));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (!result.moved) {
|
|
392
|
+
console.log(fmt.warn(`Agent ${agentId} is already in ${options.to} — nothing to do.`) + "\n");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
console.log(fmt.success(`Moved ${agentId}.`));
|
|
396
|
+
console.log(fmt.label(" From:", result.from_org_id ?? "(unknown)"));
|
|
397
|
+
console.log(fmt.label(" To: ", result.to_org_id ?? destOrgId));
|
|
398
|
+
console.log(fmt.dim(" Note: the destination org's template can floor/cap the agent's composed card — its effective posture may have changed.") + "\n");
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
if (err instanceof MnemomApiError && err.effectiveStatus === 403) {
|
|
402
|
+
console.log(fmt.error("Move denied: you must be an owner/admin of BOTH the agent's current org and the destination org.") + "\n");
|
|
403
|
+
console.log(fmt.dim(" Check your roles with `mnemom org list`. If you hold the agent's key instead, re-claim can move it: mnemom agents claim <id> --org <slug> --key <key>.") + "\n");
|
|
404
|
+
process.exit(1);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
408
|
+
console.log(fmt.error(`Move failed: ${msg}`) + "\n");
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
}
|
package/dist/commands/card.d.ts
CHANGED
|
@@ -33,6 +33,49 @@ export declare function cardValidateCommand(file: string, opts?: {
|
|
|
33
33
|
offline?: boolean;
|
|
34
34
|
agent?: string;
|
|
35
35
|
}): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Strip system-managed / server-derived keys from a fetched card so it can be
|
|
38
|
+
* round-tripped back into a PUT.
|
|
39
|
+
*
|
|
40
|
+
* `card edit`/`card evaluate` read the server-COMPOSED card, which the composer
|
|
41
|
+
* decorates with system-managed fields (today: `_composition`). The
|
|
42
|
+
* inbound-card validator REJECTS those on write ("System-managed field — cannot
|
|
43
|
+
* be set on inbound cards"), so re-PUTting the composed card verbatim is refused
|
|
44
|
+
* and the edit never persists (MNE-1726). Edit the *inbound* projection, not the
|
|
45
|
+
* *composed* one.
|
|
46
|
+
*
|
|
47
|
+
* Convention: every system-managed key is `_`-prefixed. We strip by prefix (not
|
|
48
|
+
* by an explicit `_composition` allowlist) so future server-added `_`-fields
|
|
49
|
+
* round-trip safely too — the forward-compat lesson from MNE-908, where editors
|
|
50
|
+
* fought the composer over fields the user never authored.
|
|
51
|
+
*/
|
|
52
|
+
export declare function stripSystemManagedFields(card: Record<string, unknown>): Record<string, unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Reconcile the ADR-039 unmapped-tool vocabulary with the embedded policy
|
|
55
|
+
* engine, at the CLI's own validator↔engine boundary.
|
|
56
|
+
*
|
|
57
|
+
* The card validator MANDATES the ADR-039 fields
|
|
58
|
+
* (`enforcement.allow_unmapped_tools` + `enforcement.default_unmapped_severity`)
|
|
59
|
+
* and REJECTS the legacy `enforcement.unmapped_tool_action`. But the embedded
|
|
60
|
+
* `@mnemom/policy-engine` still derives its policy from that legacy field, so a
|
|
61
|
+
* validator-passing card leaves the engine at its `allow` default: `evaluate`
|
|
62
|
+
* never warns/denies on an unmapped tool, even under `--strict` (MNE-1727). The
|
|
63
|
+
* two field vocabularies never met.
|
|
64
|
+
*
|
|
65
|
+
* Project the ADR-039 fields onto the legacy action the engine reads. Severity
|
|
66
|
+
* selects the verdict tier the operator declared:
|
|
67
|
+
*
|
|
68
|
+
* allow_unmapped_tools=true → allow (PASS)
|
|
69
|
+
* allow_unmapped_tools=false + severity high|critical → deny (FAIL, always non-zero)
|
|
70
|
+
* allow_unmapped_tools=false + severity medium|low → warn (WARN, non-zero under --strict)
|
|
71
|
+
* allow_unmapped_tools=false + severity unset → deny (fail-closed default)
|
|
72
|
+
* allow_unmapped_tools unset → card returned untouched
|
|
73
|
+
*
|
|
74
|
+
* Returns a shallow clone (the user's card is never mutated); called AFTER
|
|
75
|
+
* validation, so the legacy field is only ever seen by the engine, never
|
|
76
|
+
* re-emitted onto an inbound card.
|
|
77
|
+
*/
|
|
78
|
+
export declare function bridgeUnmappedToolPolicy(card: Record<string, unknown>): Record<string, unknown>;
|
|
36
79
|
export declare function cardEditCommand(agentName?: string, options?: {
|
|
37
80
|
idempotencyKey?: string;
|
|
38
81
|
}): Promise<void>;
|
package/dist/commands/card.js
CHANGED
|
@@ -552,6 +552,43 @@ export function validateUnifiedCard(card) {
|
|
|
552
552
|
/** @deprecated Use validateUnifiedCard instead */
|
|
553
553
|
export const validateCardJson = (raw) => validateUnifiedCard(JSON.parse(raw));
|
|
554
554
|
// ============================================================================
|
|
555
|
+
// Private helpers
|
|
556
|
+
// ============================================================================
|
|
557
|
+
function exitWithError(msg) {
|
|
558
|
+
console.log("\n" + fmt.error(msg) + "\n");
|
|
559
|
+
process.exit(1);
|
|
560
|
+
return undefined; // unreachable; satisfies `never` when process lacks type declarations
|
|
561
|
+
}
|
|
562
|
+
function loadCard(file, noun = "File") {
|
|
563
|
+
const filePath = path.resolve(file);
|
|
564
|
+
if (!fs.existsSync(filePath)) {
|
|
565
|
+
exitWithError(`${noun} not found: ${filePath}`);
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
return parseCardFile(filePath);
|
|
569
|
+
}
|
|
570
|
+
catch (e) {
|
|
571
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
572
|
+
exitWithError(`Could not parse ${noun.toLowerCase()}: ${msg}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function printChecks(checks) {
|
|
576
|
+
for (const check of checks) {
|
|
577
|
+
if (check.passed) {
|
|
578
|
+
console.log(fmt.success(`${check.name}: ${check.message}`));
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
console.log(fmt.error(`${check.name}: ${check.message}`));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function checkCardBytes(body) {
|
|
586
|
+
const bodyBytes = Buffer.byteLength(body, "utf-8");
|
|
587
|
+
if (bodyBytes > ALIGNMENT_CARD_MAX_BYTES) {
|
|
588
|
+
exitWithError(`Alignment card is ${bodyBytes} bytes; limit is ${ALIGNMENT_CARD_MAX_BYTES} bytes (128 KB). The API will return 413.`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// ============================================================================
|
|
555
592
|
// Subcommands
|
|
556
593
|
// ============================================================================
|
|
557
594
|
export async function cardShowCommand(agentName) {
|
|
@@ -581,42 +618,20 @@ export async function cardShowCommand(agentName) {
|
|
|
581
618
|
}
|
|
582
619
|
catch (error) {
|
|
583
620
|
const message = error instanceof Error ? error.message : String(error);
|
|
584
|
-
|
|
585
|
-
process.exit(1);
|
|
621
|
+
exitWithError(`Failed to fetch card: ${message}`);
|
|
586
622
|
}
|
|
587
623
|
}
|
|
588
624
|
export async function cardPublishCommand(file, agentName, options = {}) {
|
|
589
625
|
const agentId = await resolveAgentId(agentName);
|
|
590
|
-
// Resolve file path
|
|
591
|
-
const filePath = path.resolve(file);
|
|
592
|
-
if (!fs.existsSync(filePath)) {
|
|
593
|
-
console.log("\n" + fmt.error(`File not found: ${filePath}`) + "\n");
|
|
594
|
-
process.exit(1);
|
|
595
|
-
}
|
|
596
626
|
// Parse file (JSON or YAML)
|
|
597
|
-
|
|
598
|
-
try {
|
|
599
|
-
parsed = parseCardFile(filePath);
|
|
600
|
-
}
|
|
601
|
-
catch (e) {
|
|
602
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
603
|
-
console.log("\n" + fmt.error(`Could not parse file: ${msg}`) + "\n");
|
|
604
|
-
process.exit(1);
|
|
605
|
-
}
|
|
627
|
+
const parsed = loadCard(file);
|
|
606
628
|
// Validate locally
|
|
607
629
|
const checks = validateUnifiedCard(parsed.parsed);
|
|
608
630
|
const allPassed = checks.every((c) => c.passed);
|
|
609
631
|
console.log(fmt.header("Card Validation"));
|
|
610
632
|
console.log(fmt.label(" Format:", ` ${parsed.format.toUpperCase()}`));
|
|
611
633
|
console.log();
|
|
612
|
-
|
|
613
|
-
if (check.passed) {
|
|
614
|
-
console.log(fmt.success(`${check.name}: ${check.message}`));
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
console.log(fmt.error(`${check.name}: ${check.message}`));
|
|
618
|
-
}
|
|
619
|
-
}
|
|
634
|
+
printChecks(checks);
|
|
620
635
|
console.log();
|
|
621
636
|
if (!allPassed) {
|
|
622
637
|
console.log(fmt.error("Validation failed. Fix the errors above before publishing.") + "\n");
|
|
@@ -637,13 +652,7 @@ export async function cardPublishCommand(file, agentName, options = {}) {
|
|
|
637
652
|
console.log("\nPublishing alignment card...");
|
|
638
653
|
const contentType = parsed.format === "yaml" ? "text/yaml" : "application/json";
|
|
639
654
|
const body = parsed.format === "yaml" ? parsed.raw : JSON.stringify(parsed.parsed);
|
|
640
|
-
|
|
641
|
-
if (bodyBytes > ALIGNMENT_CARD_MAX_BYTES) {
|
|
642
|
-
console.log("\n" +
|
|
643
|
-
fmt.error(`Alignment card is ${bodyBytes} bytes; limit is ${ALIGNMENT_CARD_MAX_BYTES} bytes (128 KB). The API will return 413.`) +
|
|
644
|
-
"\n");
|
|
645
|
-
process.exit(1);
|
|
646
|
-
}
|
|
655
|
+
checkCardBytes(body);
|
|
647
656
|
const result = await putAlignmentCard(agentId, body, contentType, {
|
|
648
657
|
idempotencyKey: options.idempotencyKey,
|
|
649
658
|
});
|
|
@@ -656,22 +665,19 @@ export async function cardPublishCommand(file, agentName, options = {}) {
|
|
|
656
665
|
catch (error) {
|
|
657
666
|
if (error instanceof MnemomApiError) {
|
|
658
667
|
if (error.effectiveStatus === 404) {
|
|
659
|
-
|
|
660
|
-
fmt.error("Agent found but not writable: you can see this agent locally, but cannot publish to it in its current organization context.") +
|
|
661
|
-
"\n");
|
|
668
|
+
exitWithError("Agent found but not writable: you can see this agent locally, but cannot publish to it in its current organization context.");
|
|
662
669
|
}
|
|
663
670
|
else if (error.effectiveStatus === 401) {
|
|
664
|
-
|
|
671
|
+
exitWithError(`Authentication failed: ${error.message}`);
|
|
665
672
|
}
|
|
666
673
|
else {
|
|
667
|
-
|
|
674
|
+
exitWithError(`Failed to publish card: ${error.message}`);
|
|
668
675
|
}
|
|
669
676
|
}
|
|
670
677
|
else {
|
|
671
678
|
const message = error instanceof Error ? error.message : String(error);
|
|
672
|
-
|
|
679
|
+
exitWithError(`Failed to publish card: ${message}`);
|
|
673
680
|
}
|
|
674
|
-
process.exit(1);
|
|
675
681
|
}
|
|
676
682
|
}
|
|
677
683
|
const AGENT_ID_RE = /^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/;
|
|
@@ -695,22 +701,9 @@ async function softResolveAgentId(agent) {
|
|
|
695
701
|
}
|
|
696
702
|
}
|
|
697
703
|
export async function cardValidateCommand(file, opts = {}) {
|
|
698
|
-
// Resolve file path
|
|
699
|
-
const filePath = path.resolve(file);
|
|
700
|
-
if (!fs.existsSync(filePath)) {
|
|
701
|
-
console.log("\n" + fmt.error(`File not found: ${filePath}`) + "\n");
|
|
702
|
-
process.exit(1);
|
|
703
|
-
}
|
|
704
704
|
// Parse file (JSON or YAML)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
parsed = parseCardFile(filePath);
|
|
708
|
-
}
|
|
709
|
-
catch (e) {
|
|
710
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
711
|
-
console.log("\n" + fmt.error(`Could not parse file: ${msg}`) + "\n");
|
|
712
|
-
process.exit(1);
|
|
713
|
-
}
|
|
705
|
+
const filePath = path.resolve(file);
|
|
706
|
+
const parsed = loadCard(file);
|
|
714
707
|
// Prefer server-authoritative validation (composes against the agent's
|
|
715
708
|
// org/platform floor — catches conflicts the offline validator cannot) when
|
|
716
709
|
// online + an agent is available. Fall back to the local validator on
|
|
@@ -727,8 +720,7 @@ export async function cardValidateCommand(file, opts = {}) {
|
|
|
727
720
|
catch (err) {
|
|
728
721
|
if (err instanceof MnemomApiError && err.status !== 401) {
|
|
729
722
|
// 403 / 5xx — a genuine server error, not the offline-fallback case.
|
|
730
|
-
|
|
731
|
-
process.exit(1);
|
|
723
|
+
exitWithError(`Server validation failed: ${err.message}`);
|
|
732
724
|
}
|
|
733
725
|
// 401 or network error → fall through to offline validation.
|
|
734
726
|
process.stderr.write(fmt.warn("offline validation — server rules may differ") + "\n");
|
|
@@ -749,14 +741,7 @@ export async function cardValidateCommand(file, opts = {}) {
|
|
|
749
741
|
console.log(fmt.label(" File:", ` ${filePath}`));
|
|
750
742
|
console.log(fmt.label(" Format:", ` ${parsed.format.toUpperCase()}`));
|
|
751
743
|
console.log();
|
|
752
|
-
|
|
753
|
-
if (check.passed) {
|
|
754
|
-
console.log(fmt.success(`${check.name}: ${check.message}`));
|
|
755
|
-
}
|
|
756
|
-
else {
|
|
757
|
-
console.log(fmt.error(`${check.name}: ${check.message}`));
|
|
758
|
-
}
|
|
759
|
-
}
|
|
744
|
+
printChecks(checks);
|
|
760
745
|
console.log();
|
|
761
746
|
if (allPassed) {
|
|
762
747
|
console.log(fmt.success(`All ${passCount} checks passed`) + "\n");
|
|
@@ -798,6 +783,76 @@ function renderServerCardValidation(result, filePath, format) {
|
|
|
798
783
|
console.log();
|
|
799
784
|
process.exit(1);
|
|
800
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Strip system-managed / server-derived keys from a fetched card so it can be
|
|
788
|
+
* round-tripped back into a PUT.
|
|
789
|
+
*
|
|
790
|
+
* `card edit`/`card evaluate` read the server-COMPOSED card, which the composer
|
|
791
|
+
* decorates with system-managed fields (today: `_composition`). The
|
|
792
|
+
* inbound-card validator REJECTS those on write ("System-managed field — cannot
|
|
793
|
+
* be set on inbound cards"), so re-PUTting the composed card verbatim is refused
|
|
794
|
+
* and the edit never persists (MNE-1726). Edit the *inbound* projection, not the
|
|
795
|
+
* *composed* one.
|
|
796
|
+
*
|
|
797
|
+
* Convention: every system-managed key is `_`-prefixed. We strip by prefix (not
|
|
798
|
+
* by an explicit `_composition` allowlist) so future server-added `_`-fields
|
|
799
|
+
* round-trip safely too — the forward-compat lesson from MNE-908, where editors
|
|
800
|
+
* fought the composer over fields the user never authored.
|
|
801
|
+
*/
|
|
802
|
+
export function stripSystemManagedFields(card) {
|
|
803
|
+
const clean = {};
|
|
804
|
+
for (const [key, value] of Object.entries(card)) {
|
|
805
|
+
if (key.startsWith("_"))
|
|
806
|
+
continue;
|
|
807
|
+
clean[key] = value;
|
|
808
|
+
}
|
|
809
|
+
return clean;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Reconcile the ADR-039 unmapped-tool vocabulary with the embedded policy
|
|
813
|
+
* engine, at the CLI's own validator↔engine boundary.
|
|
814
|
+
*
|
|
815
|
+
* The card validator MANDATES the ADR-039 fields
|
|
816
|
+
* (`enforcement.allow_unmapped_tools` + `enforcement.default_unmapped_severity`)
|
|
817
|
+
* and REJECTS the legacy `enforcement.unmapped_tool_action`. But the embedded
|
|
818
|
+
* `@mnemom/policy-engine` still derives its policy from that legacy field, so a
|
|
819
|
+
* validator-passing card leaves the engine at its `allow` default: `evaluate`
|
|
820
|
+
* never warns/denies on an unmapped tool, even under `--strict` (MNE-1727). The
|
|
821
|
+
* two field vocabularies never met.
|
|
822
|
+
*
|
|
823
|
+
* Project the ADR-039 fields onto the legacy action the engine reads. Severity
|
|
824
|
+
* selects the verdict tier the operator declared:
|
|
825
|
+
*
|
|
826
|
+
* allow_unmapped_tools=true → allow (PASS)
|
|
827
|
+
* allow_unmapped_tools=false + severity high|critical → deny (FAIL, always non-zero)
|
|
828
|
+
* allow_unmapped_tools=false + severity medium|low → warn (WARN, non-zero under --strict)
|
|
829
|
+
* allow_unmapped_tools=false + severity unset → deny (fail-closed default)
|
|
830
|
+
* allow_unmapped_tools unset → card returned untouched
|
|
831
|
+
*
|
|
832
|
+
* Returns a shallow clone (the user's card is never mutated); called AFTER
|
|
833
|
+
* validation, so the legacy field is only ever seen by the engine, never
|
|
834
|
+
* re-emitted onto an inbound card.
|
|
835
|
+
*/
|
|
836
|
+
export function bridgeUnmappedToolPolicy(card) {
|
|
837
|
+
const enforcement = card.enforcement;
|
|
838
|
+
if (!isObj(enforcement))
|
|
839
|
+
return card;
|
|
840
|
+
const allow = enforcement.allow_unmapped_tools;
|
|
841
|
+
if (typeof allow !== "boolean")
|
|
842
|
+
return card;
|
|
843
|
+
let action;
|
|
844
|
+
if (allow) {
|
|
845
|
+
action = "allow";
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
const severity = String(enforcement.default_unmapped_severity ?? "");
|
|
849
|
+
action = severity === "medium" || severity === "low" ? "warn" : "deny";
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
...card,
|
|
853
|
+
enforcement: { ...enforcement, unmapped_tool_action: action },
|
|
854
|
+
};
|
|
855
|
+
}
|
|
801
856
|
export async function cardEditCommand(agentName, options = {}) {
|
|
802
857
|
const agentId = await resolveAgentId(agentName);
|
|
803
858
|
await requireAuth();
|
|
@@ -807,8 +862,26 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
807
862
|
if (!original) {
|
|
808
863
|
console.log(fmt.warn("No alignment card found. Creating a template..."));
|
|
809
864
|
}
|
|
810
|
-
|
|
811
|
-
|
|
865
|
+
// The GET returns the server-COMPOSED card, decorated with system-managed
|
|
866
|
+
// `_`-fields (e.g. `_composition`) the inbound-card validator forbids on
|
|
867
|
+
// write. Strip them so the user edits — and re-PUTs — the inbound projection,
|
|
868
|
+
// otherwise the round-trip PUT is rejected and the edit never persists
|
|
869
|
+
// (MNE-1726).
|
|
870
|
+
let cardYaml;
|
|
871
|
+
if (original) {
|
|
872
|
+
let parsedOriginal;
|
|
873
|
+
try {
|
|
874
|
+
parsedOriginal = yaml.load(original);
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
parsedOriginal = undefined;
|
|
878
|
+
}
|
|
879
|
+
cardYaml = isObj(parsedOriginal)
|
|
880
|
+
? yaml.dump(stripSystemManagedFields(parsedOriginal), { lineWidth: 120, noRefs: true })
|
|
881
|
+
: original;
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
cardYaml = yaml.dump({
|
|
812
885
|
card_version: "unified/2026-04-26",
|
|
813
886
|
agent_id: agentId,
|
|
814
887
|
autonomy_mode: "observe",
|
|
@@ -822,6 +895,7 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
822
895
|
},
|
|
823
896
|
audit: { retention_days: 30, queryable: false, trace_format: "otel" },
|
|
824
897
|
}, { lineWidth: 120, noRefs: true });
|
|
898
|
+
}
|
|
825
899
|
// Write to a per-invocation temp dir created with mkdtemp (mode 0700,
|
|
826
900
|
// unpredictable suffix) so the editor file can't be pre-created or
|
|
827
901
|
// symlink-raced by another user in the shared os.tmpdir().
|
|
@@ -833,14 +907,13 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
833
907
|
console.log(`Opening ${editor}...`);
|
|
834
908
|
const result = spawnSync(editor, [tmpFile], { stdio: "inherit" });
|
|
835
909
|
if (result.status !== 0) {
|
|
836
|
-
console.log("\n" + fmt.error("Editor exited with an error") + "\n");
|
|
837
910
|
try {
|
|
838
911
|
fs.unlinkSync(tmpFile);
|
|
839
912
|
}
|
|
840
913
|
catch {
|
|
841
914
|
/* ignore */
|
|
842
915
|
}
|
|
843
|
-
|
|
916
|
+
exitWithError("Editor exited with an error");
|
|
844
917
|
}
|
|
845
918
|
// Read back and compare
|
|
846
919
|
const edited = fs.readFileSync(tmpFile, "utf-8");
|
|
@@ -863,8 +936,7 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
863
936
|
}
|
|
864
937
|
catch (e) {
|
|
865
938
|
const msg = e instanceof Error ? e.message : String(e);
|
|
866
|
-
|
|
867
|
-
process.exit(1);
|
|
939
|
+
exitWithError(`Invalid YAML: ${msg}`);
|
|
868
940
|
}
|
|
869
941
|
const checks = validateUnifiedCard(parsed);
|
|
870
942
|
const allPassed = checks.every((c) => c.passed);
|
|
@@ -875,8 +947,7 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
875
947
|
console.log(fmt.error(`${check.name}: ${check.message}`));
|
|
876
948
|
}
|
|
877
949
|
console.log();
|
|
878
|
-
|
|
879
|
-
process.exit(1);
|
|
950
|
+
exitWithError("Validation failed. Card not published.");
|
|
880
951
|
}
|
|
881
952
|
// Confirm and publish
|
|
882
953
|
if (isInteractive()) {
|
|
@@ -888,13 +959,7 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
888
959
|
}
|
|
889
960
|
try {
|
|
890
961
|
console.log("\nPublishing alignment card...");
|
|
891
|
-
|
|
892
|
-
if (editedBytes > ALIGNMENT_CARD_MAX_BYTES) {
|
|
893
|
-
console.log("\n" +
|
|
894
|
-
fmt.error(`Alignment card is ${editedBytes} bytes; limit is ${ALIGNMENT_CARD_MAX_BYTES} bytes (128 KB). The API will return 413.`) +
|
|
895
|
-
"\n");
|
|
896
|
-
process.exit(1);
|
|
897
|
-
}
|
|
962
|
+
checkCardBytes(edited);
|
|
898
963
|
const putResult = await putAlignmentCard(agentId, edited, "text/yaml", {
|
|
899
964
|
idempotencyKey: options.idempotencyKey,
|
|
900
965
|
});
|
|
@@ -906,8 +971,7 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
906
971
|
}
|
|
907
972
|
catch (error) {
|
|
908
973
|
const message = error instanceof Error ? error.message : String(error);
|
|
909
|
-
|
|
910
|
-
process.exit(1);
|
|
974
|
+
exitWithError(`Failed to publish card: ${message}`);
|
|
911
975
|
}
|
|
912
976
|
}
|
|
913
977
|
/**
|
|
@@ -920,19 +984,7 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
920
984
|
export async function cardEvaluateCommand(file, options) {
|
|
921
985
|
// 1. Read + validate card file
|
|
922
986
|
const cardPath = path.resolve(file);
|
|
923
|
-
|
|
924
|
-
console.log("\n" + fmt.error(`Card file not found: ${cardPath}`) + "\n");
|
|
925
|
-
process.exit(1);
|
|
926
|
-
}
|
|
927
|
-
let parsed;
|
|
928
|
-
try {
|
|
929
|
-
parsed = parseCardFile(cardPath);
|
|
930
|
-
}
|
|
931
|
-
catch (e) {
|
|
932
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
933
|
-
console.log("\n" + fmt.error(`Could not parse card file: ${msg}`) + "\n");
|
|
934
|
-
process.exit(1);
|
|
935
|
-
}
|
|
987
|
+
const parsed = loadCard(file, "Card file");
|
|
936
988
|
const checks = validateUnifiedCard(parsed.parsed);
|
|
937
989
|
const allPassed = checks.every((c) => c.passed);
|
|
938
990
|
if (!allPassed) {
|
|
@@ -954,8 +1006,7 @@ export async function cardEvaluateCommand(file, options) {
|
|
|
954
1006
|
else if (options.toolManifest) {
|
|
955
1007
|
const manifestPath = path.resolve(options.toolManifest);
|
|
956
1008
|
if (!fs.existsSync(manifestPath)) {
|
|
957
|
-
|
|
958
|
-
process.exit(1);
|
|
1009
|
+
exitWithError(`Tool manifest file not found: ${manifestPath}`);
|
|
959
1010
|
}
|
|
960
1011
|
try {
|
|
961
1012
|
const manifestRaw = fs.readFileSync(manifestPath, "utf-8");
|
|
@@ -966,18 +1017,18 @@ export async function cardEvaluateCommand(file, options) {
|
|
|
966
1017
|
}
|
|
967
1018
|
catch (e) {
|
|
968
1019
|
const msg = e instanceof Error ? e.message : String(e);
|
|
969
|
-
|
|
970
|
-
process.exit(1);
|
|
1020
|
+
exitWithError(`Could not read tool manifest: ${msg}`);
|
|
971
1021
|
}
|
|
972
1022
|
}
|
|
973
1023
|
if (tools.length === 0) {
|
|
974
|
-
|
|
975
|
-
process.exit(1);
|
|
1024
|
+
exitWithError("No tools specified. Use --tools or --tool-manifest");
|
|
976
1025
|
}
|
|
977
|
-
// 3. Run evaluation -- card IS the policy source
|
|
1026
|
+
// 3. Run evaluation -- card IS the policy source. Bridge the ADR-039
|
|
1027
|
+
// unmapped-tool fields onto the legacy action the embedded engine reads so
|
|
1028
|
+
// `--strict` actually surfaces unmapped-tool warnings (MNE-1727).
|
|
978
1029
|
const result = evaluatePolicy({
|
|
979
1030
|
context: "cicd",
|
|
980
|
-
card: parsed.parsed,
|
|
1031
|
+
card: bridgeUnmappedToolPolicy(parsed.parsed),
|
|
981
1032
|
tools,
|
|
982
1033
|
});
|
|
983
1034
|
// 4. Display results
|
package/dist/commands/logs.js
CHANGED
|
@@ -46,7 +46,17 @@ function displayTrace(trace) {
|
|
|
46
46
|
// (The old flat `trace.verified` was always undefined on the nested wire → every
|
|
47
47
|
// trace was mis-flagged [VIOLATION] and the action rendered as "[object Object]".)
|
|
48
48
|
const verified = trace.verification?.verified ?? true;
|
|
49
|
-
|
|
49
|
+
// MNE-596 — AAP flagged a structural violation, but the observer's DDR
|
|
50
|
+
// cross-check confirmed AIP independently saw `clear` and no bounded action
|
|
51
|
+
// was ever executed: the agent was attacked and correctly refused. Render
|
|
52
|
+
// this distinctly from a real [VIOLATION] — it's good behavior, not bad —
|
|
53
|
+
// while still surfacing it (never silently swallowed).
|
|
54
|
+
const isBoundedRefusal = trace.verification?.classification === "bounded_refusal";
|
|
55
|
+
const statusMsg = verified
|
|
56
|
+
? fmt.success(timestamp)
|
|
57
|
+
: isBoundedRefusal
|
|
58
|
+
? fmt.warn(`${timestamp} [BOUNDED REFUSAL — policy-enforced, not counted]`)
|
|
59
|
+
: fmt.error(`${timestamp} [VIOLATION]`);
|
|
50
60
|
console.log(`\n ${statusMsg}`);
|
|
51
61
|
// action.name is the canonical label (the tool name lives here in the AIP-nested
|
|
52
62
|
// shape); fall back to the action type, then a dash.
|