@mnemom/mnemom 0.12.0 → 0.13.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/card.d.ts +4 -1
- package/dist/commands/card.js +129 -16
- package/dist/commands/integrity.js +7 -3
- package/dist/commands/license.js +30 -42
- package/dist/commands/logs.js +25 -9
- package/dist/commands/protection.d.ts +4 -1
- package/dist/commands/protection.js +74 -2
- package/dist/commands/recipes.js +2 -2
- package/dist/commands/status.js +12 -4
- package/dist/commands/verify-card.d.ts +102 -0
- package/dist/commands/verify-card.js +287 -0
- package/dist/index.js +52 -8
- package/dist/lib/api.d.ts +214 -110
- package/dist/lib/api.js +305 -287
- package/dist/lib/webhooks-api.js +10 -5
- package/dist/version.d.ts +1 -0
- package/dist/version.js +16 -0
- package/package.json +5 -2
package/dist/commands/card.d.ts
CHANGED
|
@@ -29,7 +29,10 @@ export declare function cardShowCommand(agentName?: string): Promise<void>;
|
|
|
29
29
|
export declare function cardPublishCommand(file: string, agentName?: string, options?: {
|
|
30
30
|
idempotencyKey?: string;
|
|
31
31
|
}): Promise<void>;
|
|
32
|
-
export declare function cardValidateCommand(file: string
|
|
32
|
+
export declare function cardValidateCommand(file: string, opts?: {
|
|
33
|
+
offline?: boolean;
|
|
34
|
+
agent?: string;
|
|
35
|
+
}): Promise<void>;
|
|
33
36
|
export declare function cardEditCommand(agentName?: string, options?: {
|
|
34
37
|
idempotencyKey?: string;
|
|
35
38
|
}): Promise<void>;
|
package/dist/commands/card.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import yaml from "js-yaml";
|
|
6
|
-
import { ALIGNMENT_CARD_MAX_BYTES, getAlignmentCard, putAlignmentCard, resolveAgentId, } from "../lib/api.js";
|
|
6
|
+
import { ALIGNMENT_CARD_MAX_BYTES, getAlignmentCard, putAlignmentCard, resolveAgentId, getAgentByName, previewComposeAgentCard, MnemomApiError, } from "../lib/api.js";
|
|
7
7
|
import { requireAuth } from "../lib/auth.js";
|
|
8
8
|
import { fmt } from "../lib/format.js";
|
|
9
9
|
import { askYesNo, isInteractive } from "../lib/prompt.js";
|
|
@@ -194,6 +194,28 @@ export function validateUnifiedCard(card) {
|
|
|
194
194
|
else {
|
|
195
195
|
const v = card.values;
|
|
196
196
|
const decl = v.declared;
|
|
197
|
+
// A declared entry is a non-empty STRING or a parameterized OBJECT
|
|
198
|
+
// { id, ...string params } — mirrors mnemom-api/src/composition/validate.ts
|
|
199
|
+
// `declaredValueRefId`. (#8 DELTA-1: the old `decl.every(typeof === "string")`
|
|
200
|
+
// rejected the valid object form, making the CLI STRICTER than the server —
|
|
201
|
+
// a false-negative that blocked good cards in pre-flight.)
|
|
202
|
+
const declRefId = (entry) => {
|
|
203
|
+
if (typeof entry === "string")
|
|
204
|
+
return entry.length > 0 ? entry : null;
|
|
205
|
+
if (!isObj(entry))
|
|
206
|
+
return null;
|
|
207
|
+
const o = entry;
|
|
208
|
+
if (typeof o.id !== "string" || o.id.length === 0)
|
|
209
|
+
return null;
|
|
210
|
+
for (const [k, pv] of Object.entries(o)) {
|
|
211
|
+
if (k === "id")
|
|
212
|
+
continue;
|
|
213
|
+
if (typeof pv !== "string")
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return o.id;
|
|
217
|
+
};
|
|
218
|
+
const declIds = [];
|
|
197
219
|
if (decl.length === 0) {
|
|
198
220
|
checks.push({
|
|
199
221
|
name: "values.declared",
|
|
@@ -201,21 +223,32 @@ export function validateUnifiedCard(card) {
|
|
|
201
223
|
message: "Must contain at least one value.",
|
|
202
224
|
});
|
|
203
225
|
}
|
|
204
|
-
else if (!decl.every((s) => typeof s === "string")) {
|
|
205
|
-
checks.push({
|
|
206
|
-
name: "values.declared",
|
|
207
|
-
passed: false,
|
|
208
|
-
message: "All entries must be strings.",
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
226
|
else {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
227
|
+
let badRef = false;
|
|
228
|
+
decl.forEach((entry, i) => {
|
|
229
|
+
const id = declRefId(entry);
|
|
230
|
+
if (id === null) {
|
|
231
|
+
checks.push({
|
|
232
|
+
name: `values.declared[${i}]`,
|
|
233
|
+
passed: false,
|
|
234
|
+
message: "Must be a non-empty string or an object with a non-empty string `id` and string-only parameters.",
|
|
235
|
+
});
|
|
236
|
+
badRef = true;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
declIds.push(id);
|
|
240
|
+
}
|
|
216
241
|
});
|
|
242
|
+
if (!badRef) {
|
|
243
|
+
checks.push({
|
|
244
|
+
name: "values.declared",
|
|
245
|
+
passed: true,
|
|
246
|
+
message: `${decl.length} value(s) declared`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
217
249
|
}
|
|
218
|
-
// definitions ⊆ declared (ADR-039 Decision 10)
|
|
250
|
+
// definitions ⊆ declared (ADR-039 Decision 10) — build the subset Set from
|
|
251
|
+
// the RESOLVED declared ids (string entries + each object's `id`).
|
|
219
252
|
if (v.definitions !== undefined) {
|
|
220
253
|
if (!isObj(v.definitions)) {
|
|
221
254
|
checks.push({
|
|
@@ -225,7 +258,7 @@ export function validateUnifiedCard(card) {
|
|
|
225
258
|
});
|
|
226
259
|
}
|
|
227
260
|
else {
|
|
228
|
-
const declSet = new Set(
|
|
261
|
+
const declSet = new Set(declIds);
|
|
229
262
|
for (const key of Object.keys(v.definitions)) {
|
|
230
263
|
if (!declSet.has(key)) {
|
|
231
264
|
checks.push({
|
|
@@ -626,7 +659,27 @@ export async function cardPublishCommand(file, agentName, options = {}) {
|
|
|
626
659
|
process.exit(1);
|
|
627
660
|
}
|
|
628
661
|
}
|
|
629
|
-
|
|
662
|
+
const AGENT_ID_RE = /^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/;
|
|
663
|
+
/**
|
|
664
|
+
* Soft agent resolution for `validate`: returns an agent id WITHOUT exiting the
|
|
665
|
+
* process (unlike resolveAgentId). Returns null when no agent is configured, or
|
|
666
|
+
* the name can't be resolved (not authenticated / not found) — the caller then
|
|
667
|
+
* falls back to offline validation.
|
|
668
|
+
*/
|
|
669
|
+
async function softResolveAgentId(agent) {
|
|
670
|
+
const name = agent ?? process.env.MNEMOM_AGENT;
|
|
671
|
+
if (!name)
|
|
672
|
+
return null;
|
|
673
|
+
if (AGENT_ID_RE.test(name))
|
|
674
|
+
return name;
|
|
675
|
+
try {
|
|
676
|
+
return (await getAgentByName(name))?.id ?? null;
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
export async function cardValidateCommand(file, opts = {}) {
|
|
630
683
|
// Resolve file path
|
|
631
684
|
const filePath = path.resolve(file);
|
|
632
685
|
if (!fs.existsSync(filePath)) {
|
|
@@ -643,7 +696,35 @@ export async function cardValidateCommand(file) {
|
|
|
643
696
|
console.log("\n" + fmt.error(`Could not parse file: ${msg}`) + "\n");
|
|
644
697
|
process.exit(1);
|
|
645
698
|
}
|
|
646
|
-
//
|
|
699
|
+
// Prefer server-authoritative validation (composes against the agent's
|
|
700
|
+
// org/platform floor — catches conflicts the offline validator cannot) when
|
|
701
|
+
// online + an agent is available. Fall back to the local validator on
|
|
702
|
+
// 401/network. `--offline` forces local-only (#9).
|
|
703
|
+
if (!opts.offline) {
|
|
704
|
+
const agentId = await softResolveAgentId(opts.agent);
|
|
705
|
+
if (agentId) {
|
|
706
|
+
const contentType = parsed.format === "yaml" ? "text/yaml" : "application/json";
|
|
707
|
+
try {
|
|
708
|
+
const result = await previewComposeAgentCard(agentId, "alignment", parsed.raw, contentType);
|
|
709
|
+
renderServerCardValidation(result, filePath, parsed.format);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
if (err instanceof MnemomApiError && err.status !== 401) {
|
|
714
|
+
// 403 / 5xx — a genuine server error, not the offline-fallback case.
|
|
715
|
+
console.log("\n" + fmt.error(`Server validation failed: ${err.message}`) + "\n");
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
// 401 or network error → fall through to offline validation.
|
|
719
|
+
process.stderr.write(fmt.warn("offline validation — server rules may differ") + "\n");
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else if (!opts.agent && !process.env.MNEMOM_AGENT) {
|
|
723
|
+
process.stderr.write(fmt.dim("tip: pass --agent <name> to validate against the server (org/platform floor)") +
|
|
724
|
+
"\n");
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
// Offline / fallback path: the local hand-rolled validator (also `--offline`).
|
|
647
728
|
const checks = validateUnifiedCard(parsed.parsed);
|
|
648
729
|
const allPassed = checks.every((c) => c.passed);
|
|
649
730
|
const passCount = checks.filter((c) => c.passed).length;
|
|
@@ -670,6 +751,38 @@ export async function cardValidateCommand(file) {
|
|
|
670
751
|
process.exit(1);
|
|
671
752
|
}
|
|
672
753
|
}
|
|
754
|
+
/** Render a server-authoritative preview-compose result; exit 1 if invalid. */
|
|
755
|
+
function renderServerCardValidation(result, filePath, format) {
|
|
756
|
+
console.log(fmt.header("Card Validation (server-authoritative)"));
|
|
757
|
+
console.log();
|
|
758
|
+
console.log(fmt.label(" File:", ` ${filePath}`));
|
|
759
|
+
console.log(fmt.label(" Format:", ` ${format.toUpperCase()}`));
|
|
760
|
+
console.log();
|
|
761
|
+
if (result.valid) {
|
|
762
|
+
console.log(fmt.success("Server accepted and composed the card."));
|
|
763
|
+
const conflicts = result.conflicts ?? [];
|
|
764
|
+
if (conflicts.length > 0) {
|
|
765
|
+
console.log(fmt.warn(`${conflicts.length} field(s) tightened by the org/platform floor:`));
|
|
766
|
+
console.log(fmt.json(conflicts));
|
|
767
|
+
}
|
|
768
|
+
const coherence = result.coherence_violations ?? [];
|
|
769
|
+
if (coherence.length > 0) {
|
|
770
|
+
console.log(fmt.warn(`${coherence.length} coherence finding(s):`));
|
|
771
|
+
console.log(fmt.json(coherence));
|
|
772
|
+
}
|
|
773
|
+
console.log();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
console.log(fmt.error(`Server rejected the card${result.error?.code ? ` (${result.error.code})` : ""}:`));
|
|
777
|
+
if (result.error?.message)
|
|
778
|
+
console.log(` ${result.error.message}`);
|
|
779
|
+
if (result.error?.details !== undefined) {
|
|
780
|
+
console.log();
|
|
781
|
+
console.log(fmt.json(result.error.details));
|
|
782
|
+
}
|
|
783
|
+
console.log();
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
673
786
|
export async function cardEditCommand(agentName, options = {}) {
|
|
674
787
|
const agentId = await resolveAgentId(agentName);
|
|
675
788
|
await requireAuth();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveAgentId, getIntegrity } from "../lib/api.js";
|
|
1
|
+
import { resolveAgentId, getIntegrity, MnemomApiError } from "../lib/api.js";
|
|
2
2
|
import { fmt } from "../lib/format.js";
|
|
3
3
|
export async function integrityCommand(agentName) {
|
|
4
4
|
const agentId = await resolveAgentId(agentName);
|
|
@@ -25,8 +25,11 @@ export async function integrityCommand(agentName) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
catch (error) {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// A 404 means no integrity record exists yet — render the friendly empty state.
|
|
29
|
+
// Branch on effectiveStatus (not status): the enforce hook rewrites an
|
|
30
|
+
// undocumented 404 to a synthetic 500 carrying spec_deviation.original_status,
|
|
31
|
+
// and effectiveStatus surfaces the true status (=== status when documented).
|
|
32
|
+
if (error instanceof MnemomApiError && error.effectiveStatus === 404) {
|
|
30
33
|
console.log(fmt.header("Integrity Score"));
|
|
31
34
|
console.log(` ${fmt.label("Score: ", "N/A")}`);
|
|
32
35
|
console.log(` ${fmt.label("Total: ", "0 traces")}`);
|
|
@@ -35,6 +38,7 @@ export async function integrityCommand(agentName) {
|
|
|
35
38
|
console.log("\nNo traces recorded yet. Start using Claude to build your integrity score.\n");
|
|
36
39
|
}
|
|
37
40
|
else {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
42
|
console.log("\n" + fmt.error(`Failed to fetch integrity score: ${message}`) + "\n");
|
|
39
43
|
process.exit(1);
|
|
40
44
|
}
|
package/dist/commands/license.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { getLicenseJwt, saveLicenseJwt, clearLicenseJwt } from "../lib/auth.js";
|
|
2
|
-
import {
|
|
2
|
+
import { validateLicense, MnemomApiError } from "../lib/api.js";
|
|
3
3
|
import { fmt } from "../lib/format.js";
|
|
4
|
-
|
|
5
|
-
function sanitizeForHttp(data) {
|
|
6
|
-
return String(data).trim();
|
|
7
|
-
}
|
|
4
|
+
import { CLI_VERSION } from "../version.js";
|
|
8
5
|
/**
|
|
9
6
|
* Decode a JWT payload without verifying the signature.
|
|
10
7
|
*/
|
|
@@ -34,39 +31,37 @@ export async function licenseActivateCommand(jwt) {
|
|
|
34
31
|
process.exit(1);
|
|
35
32
|
}
|
|
36
33
|
console.log("\nActivating enterprise license...\n");
|
|
37
|
-
// Validate against API (
|
|
34
|
+
// Validate against the API via the canonical lib helper (UNAUTH by design —
|
|
35
|
+
// the JWT is the credential). validateLicense throws MnemomApiError on non-2xx,
|
|
36
|
+
// so we render `.message` (the old hand-rolled `err.error` string-coerced the
|
|
37
|
+
// nested {code,message} envelope object → the live "[object Object]" bug).
|
|
38
38
|
const hostname = (await import("node:os")).hostname();
|
|
39
39
|
try {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
body: JSON.stringify({
|
|
45
|
-
license: jwt,
|
|
46
|
-
instance_id: hostname,
|
|
47
|
-
instance_metadata: {
|
|
48
|
-
hostname,
|
|
49
|
-
platform: process.platform,
|
|
50
|
-
cli_version: "2.1.0",
|
|
51
|
-
},
|
|
52
|
-
}),
|
|
40
|
+
const result = await validateLicense(jwt, hostname, {
|
|
41
|
+
hostname,
|
|
42
|
+
platform: process.platform,
|
|
43
|
+
cli_version: CLI_VERSION,
|
|
53
44
|
});
|
|
54
|
-
if (
|
|
55
|
-
const result = (await response.json());
|
|
45
|
+
if (result.valid) {
|
|
56
46
|
console.log(" License validated successfully!\n");
|
|
57
|
-
if (result.warning) {
|
|
58
|
-
console.log(` Warning: ${result.warning}\n`);
|
|
59
|
-
}
|
|
60
47
|
}
|
|
61
48
|
else {
|
|
62
|
-
|
|
63
|
-
console.log(
|
|
64
|
-
|
|
49
|
+
// 2xx body with valid:false — grace period / activation limit reached.
|
|
50
|
+
console.log(" Note: server returned valid:false (grace period or activation limit).\n");
|
|
51
|
+
}
|
|
52
|
+
if (result.warning) {
|
|
53
|
+
console.log(` Warning: ${result.warning}\n`);
|
|
65
54
|
}
|
|
66
55
|
}
|
|
67
|
-
catch {
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err instanceof MnemomApiError) {
|
|
58
|
+
console.log(` Warning: Validation returned ${err.status}: ${err.message}`);
|
|
59
|
+
console.log(" License stored locally (will retry validation).\n");
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.log(" Warning: Could not reach API for validation.");
|
|
63
|
+
console.log(" License stored locally (offline mode).\n");
|
|
64
|
+
}
|
|
70
65
|
}
|
|
71
66
|
// Store in auth store
|
|
72
67
|
saveLicenseJwt(jwt);
|
|
@@ -131,24 +126,17 @@ export async function licenseDeactivateCommand() {
|
|
|
131
126
|
console.log("\nNo enterprise license to deactivate.\n");
|
|
132
127
|
return;
|
|
133
128
|
}
|
|
134
|
-
// Try to deactivate via API
|
|
129
|
+
// Try to deactivate via API (best-effort, fire-and-forget — reuses the
|
|
130
|
+
// validate endpoint with deactivating:true; local removal below is the source
|
|
131
|
+
// of truth). Ignore both the result and any MnemomApiError.
|
|
135
132
|
const claims = decodeJwtPayload(licenseJwt);
|
|
136
133
|
if (claims) {
|
|
137
134
|
try {
|
|
138
135
|
const hostname = (await import("node:os")).hostname();
|
|
139
|
-
|
|
140
|
-
await fetch(deactivateUrl, {
|
|
141
|
-
method: "POST",
|
|
142
|
-
headers: { "Content-Type": "application/json" },
|
|
143
|
-
body: sanitizeForHttp(JSON.stringify({
|
|
144
|
-
license: String(licenseJwt),
|
|
145
|
-
instance_id: hostname,
|
|
146
|
-
instance_metadata: { deactivating: true },
|
|
147
|
-
})),
|
|
148
|
-
});
|
|
136
|
+
await validateLicense(String(licenseJwt), hostname, { deactivating: true });
|
|
149
137
|
}
|
|
150
138
|
catch {
|
|
151
|
-
// Best-effort
|
|
139
|
+
// Best-effort — ignore.
|
|
152
140
|
}
|
|
153
141
|
}
|
|
154
142
|
// Remove from auth store
|
package/dist/commands/logs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveAgentId, getTraces } from "../lib/api.js";
|
|
1
|
+
import { resolveAgentId, getTraces, MnemomApiError } from "../lib/api.js";
|
|
2
2
|
import { getGatewayUrl } from "../lib/config.js";
|
|
3
3
|
import { fmt } from "../lib/format.js";
|
|
4
4
|
export async function logsCommand(options = {}) {
|
|
@@ -23,12 +23,16 @@ export async function logsCommand(options = {}) {
|
|
|
23
23
|
console.log(`Dashboard: https://mnemon.ai/dashboard/${agentId}\n`);
|
|
24
24
|
}
|
|
25
25
|
catch (error) {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// A 404 means the agent isn't registered yet — treat as the empty state.
|
|
27
|
+
// Branch on effectiveStatus (not status): the enforce hook rewrites an
|
|
28
|
+
// undocumented 404 to a synthetic 500 carrying spec_deviation.original_status;
|
|
29
|
+
// effectiveStatus surfaces the true status (=== status when documented).
|
|
30
|
+
if (error instanceof MnemomApiError && error.effectiveStatus === 404) {
|
|
28
31
|
console.log(fmt.header("No traces found"));
|
|
29
32
|
console.log("\nStart using Claude to generate traces.\n");
|
|
30
33
|
}
|
|
31
34
|
else {
|
|
35
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
36
|
console.log("\n" + fmt.error(`Failed to fetch traces: ${message}`) + "\n");
|
|
33
37
|
process.exit(1);
|
|
34
38
|
}
|
|
@@ -36,16 +40,28 @@ export async function logsCommand(options = {}) {
|
|
|
36
40
|
}
|
|
37
41
|
function displayTrace(trace) {
|
|
38
42
|
const timestamp = formatTimestamp(trace.timestamp);
|
|
39
|
-
|
|
43
|
+
// `verification` is null when the trace hasn't been verified yet — that is NOT
|
|
44
|
+
// a violation. Only flag [VIOLATION] when verification exists AND verified===false.
|
|
45
|
+
// (The old flat `trace.verified` was always undefined on the nested wire → every
|
|
46
|
+
// trace was mis-flagged [VIOLATION] and the action rendered as "[object Object]".)
|
|
47
|
+
const verified = trace.verification?.verified ?? true;
|
|
48
|
+
const statusMsg = verified ? fmt.success(timestamp) : fmt.error(`${timestamp} [VIOLATION]`);
|
|
40
49
|
console.log(`\n ${statusMsg}`);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
// action.name is the canonical label (the tool name lives here in the AIP-nested
|
|
51
|
+
// shape); fall back to the action type, then a dash.
|
|
52
|
+
const actionLabel = trace.action?.name ?? trace.action?.type ?? "—";
|
|
53
|
+
console.log(` ${fmt.label("Action:", ` ${actionLabel}`)}`);
|
|
54
|
+
if (trace.action?.category) {
|
|
55
|
+
console.log(` ${fmt.label("Type: ", ` ${trace.action.category}`)}`);
|
|
44
56
|
}
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
const reasoning = trace.decision?.selection_reasoning;
|
|
58
|
+
if (reasoning) {
|
|
59
|
+
const preview = truncate(reasoning, 60);
|
|
47
60
|
console.log(` ${fmt.label("Reason:", ` ${preview}`)}`);
|
|
48
61
|
}
|
|
62
|
+
if (trace.verification && trace.verification.violations.length > 0) {
|
|
63
|
+
console.log(` ${fmt.label("Issues:", ` ${trace.verification.violations.join(", ")}`)}`);
|
|
64
|
+
}
|
|
49
65
|
}
|
|
50
66
|
function formatTimestamp(iso) {
|
|
51
67
|
try {
|
|
@@ -16,7 +16,10 @@ export declare function protectionShowCommand(agentName?: string): Promise<void>
|
|
|
16
16
|
export declare function protectionPublishCommand(file: string, agentName?: string, options?: {
|
|
17
17
|
idempotencyKey?: string;
|
|
18
18
|
}): Promise<void>;
|
|
19
|
-
export declare function protectionValidateCommand(file: string
|
|
19
|
+
export declare function protectionValidateCommand(file: string, opts?: {
|
|
20
|
+
offline?: boolean;
|
|
21
|
+
agent?: string;
|
|
22
|
+
}): Promise<void>;
|
|
20
23
|
export declare function protectionEditCommand(agentName?: string, options?: {
|
|
21
24
|
idempotencyKey?: string;
|
|
22
25
|
}): Promise<void>;
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import yaml from "js-yaml";
|
|
6
|
-
import { PROTECTION_CARD_MAX_BYTES, getProtectionCard, putProtectionCard, resolveAgentId, } from "../lib/api.js";
|
|
6
|
+
import { PROTECTION_CARD_MAX_BYTES, getProtectionCard, putProtectionCard, resolveAgentId, getAgentByName, previewComposeAgentCard, MnemomApiError, } from "../lib/api.js";
|
|
7
7
|
import { requireAuth } from "../lib/auth.js";
|
|
8
8
|
import { fmt } from "../lib/format.js";
|
|
9
9
|
import { askYesNo, isInteractive } from "../lib/prompt.js";
|
|
@@ -443,7 +443,28 @@ export async function protectionPublishCommand(file, agentName, options = {}) {
|
|
|
443
443
|
process.exit(1);
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
|
-
|
|
446
|
+
// Agent-resolution regex (distinct from the trusted-sources AGENT_ID_RE above,
|
|
447
|
+
// which validates card-embedded agent ids). This matches the gateway agent-id
|
|
448
|
+
// shapes used by resolveAgentId so we can use a value directly without a lookup.
|
|
449
|
+
const RESOLVE_AGENT_ID_RE = /^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/;
|
|
450
|
+
/**
|
|
451
|
+
* Soft agent resolution for `validate`: returns an agent id WITHOUT exiting the
|
|
452
|
+
* process. null ⇒ no agent configured / unresolvable → offline fallback.
|
|
453
|
+
*/
|
|
454
|
+
async function softResolveAgentId(agent) {
|
|
455
|
+
const name = agent ?? process.env.MNEMOM_AGENT;
|
|
456
|
+
if (!name)
|
|
457
|
+
return null;
|
|
458
|
+
if (RESOLVE_AGENT_ID_RE.test(name))
|
|
459
|
+
return name;
|
|
460
|
+
try {
|
|
461
|
+
return (await getAgentByName(name))?.id ?? null;
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
export async function protectionValidateCommand(file, opts = {}) {
|
|
447
468
|
const filePath = path.resolve(file);
|
|
448
469
|
if (!fs.existsSync(filePath)) {
|
|
449
470
|
console.log("\n" + fmt.error(`File not found: ${filePath}`) + "\n");
|
|
@@ -458,6 +479,30 @@ export async function protectionValidateCommand(file) {
|
|
|
458
479
|
console.log("\n" + fmt.error(`Could not parse file: ${msg}`) + "\n");
|
|
459
480
|
process.exit(1);
|
|
460
481
|
}
|
|
482
|
+
// Prefer server-authoritative validation when online + an agent is available;
|
|
483
|
+
// fall back to the local validator on 401/network. `--offline` forces local. (#9)
|
|
484
|
+
if (!opts.offline) {
|
|
485
|
+
const agentId = await softResolveAgentId(opts.agent);
|
|
486
|
+
if (agentId) {
|
|
487
|
+
const contentType = parsed.format === "yaml" ? "text/yaml" : "application/json";
|
|
488
|
+
try {
|
|
489
|
+
const result = await previewComposeAgentCard(agentId, "protection", parsed.raw, contentType);
|
|
490
|
+
renderServerProtectionValidation(result, filePath, parsed.format);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
if (err instanceof MnemomApiError && err.status !== 401) {
|
|
495
|
+
console.log("\n" + fmt.error(`Server validation failed: ${err.message}`) + "\n");
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
process.stderr.write(fmt.warn("offline validation — server rules may differ") + "\n");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else if (!opts.agent && !process.env.MNEMOM_AGENT) {
|
|
502
|
+
process.stderr.write(fmt.dim("tip: pass --agent <name> to validate against the server (org/platform floor)") +
|
|
503
|
+
"\n");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
461
506
|
const checks = validateProtectionCard(parsed.parsed);
|
|
462
507
|
const allPassed = checks.every((c) => c.passed);
|
|
463
508
|
const passCount = checks.filter((c) => c.passed).length;
|
|
@@ -484,6 +529,33 @@ export async function protectionValidateCommand(file) {
|
|
|
484
529
|
process.exit(1);
|
|
485
530
|
}
|
|
486
531
|
}
|
|
532
|
+
/** Render a server-authoritative preview-compose result; exit 1 if invalid. */
|
|
533
|
+
function renderServerProtectionValidation(result, filePath, format) {
|
|
534
|
+
console.log(fmt.header("Protection Card Validation (server-authoritative)"));
|
|
535
|
+
console.log();
|
|
536
|
+
console.log(fmt.label(" File:", ` ${filePath}`));
|
|
537
|
+
console.log(fmt.label(" Format:", ` ${format.toUpperCase()}`));
|
|
538
|
+
console.log();
|
|
539
|
+
if (result.valid) {
|
|
540
|
+
console.log(fmt.success("Server accepted and composed the card."));
|
|
541
|
+
const conflicts = result.conflicts ?? [];
|
|
542
|
+
if (conflicts.length > 0) {
|
|
543
|
+
console.log(fmt.warn(`${conflicts.length} field(s) tightened by the org/platform floor:`));
|
|
544
|
+
console.log(fmt.json(conflicts));
|
|
545
|
+
}
|
|
546
|
+
console.log();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
console.log(fmt.error(`Server rejected the card${result.error?.code ? ` (${result.error.code})` : ""}:`));
|
|
550
|
+
if (result.error?.message)
|
|
551
|
+
console.log(` ${result.error.message}`);
|
|
552
|
+
if (result.error?.details !== undefined) {
|
|
553
|
+
console.log();
|
|
554
|
+
console.log(fmt.json(result.error.details));
|
|
555
|
+
}
|
|
556
|
+
console.log();
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
487
559
|
export async function protectionEditCommand(agentName, options = {}) {
|
|
488
560
|
const agentId = await resolveAgentId(agentName);
|
|
489
561
|
await requireAuth();
|
package/dist/commands/recipes.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* filed" anchor and the candidate_id is the primary track-handle.
|
|
24
24
|
*/
|
|
25
25
|
import chalk from "chalk";
|
|
26
|
-
import { reportRecipeFnFp
|
|
26
|
+
import { reportRecipeFnFp } from "../lib/api.js";
|
|
27
27
|
import { requireAuth } from "../lib/auth.js";
|
|
28
28
|
async function readSummary(opts) {
|
|
29
29
|
if (typeof opts.summary === "string" && opts.summary.trim()) {
|
|
@@ -42,7 +42,7 @@ async function readSummary(opts) {
|
|
|
42
42
|
if (piped)
|
|
43
43
|
return piped;
|
|
44
44
|
}
|
|
45
|
-
throw new Error(
|
|
45
|
+
throw new Error('summary required: pass --summary "…" or pipe text on stdin.');
|
|
46
46
|
}
|
|
47
47
|
async function runReport(recipeId, type, opts) {
|
|
48
48
|
await requireAuth();
|
package/dist/commands/status.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getGatewayUrl } from "../lib/config.js";
|
|
2
|
-
import { resolveAgentId, getAgent, getIntegrity, getTraces } from "../lib/api.js";
|
|
2
|
+
import { resolveAgentId, getAgent, getIntegrity, getTraces, MnemomApiError } from "../lib/api.js";
|
|
3
3
|
import { isLoggedIn, getAuthInfo } from "../lib/auth.js";
|
|
4
4
|
import { fmt } from "../lib/format.js";
|
|
5
5
|
const DASHBOARD_URL = "https://mnemom.ai";
|
|
@@ -133,8 +133,10 @@ async function checkApiConnectivity(agentId) {
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
catch (error) {
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
// 404 = agent not registered yet. Branch on effectiveStatus (enforce hook may
|
|
137
|
+
// rewrite an undocumented 404 → synthetic 500 + spec_deviation.original_status;
|
|
138
|
+
// effectiveStatus === status when documented). getAgent → fetchApi → MnemomApiError.
|
|
139
|
+
if (error instanceof MnemomApiError && error.effectiveStatus === 404) {
|
|
138
140
|
return {
|
|
139
141
|
name: "API",
|
|
140
142
|
status: "warning",
|
|
@@ -142,7 +144,13 @@ async function checkApiConnectivity(agentId) {
|
|
|
142
144
|
details: "Will register on first traced API call",
|
|
143
145
|
};
|
|
144
146
|
}
|
|
145
|
-
|
|
147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
148
|
+
// Transport-level timeout (no HTTP status, not an envelope error) — detect by
|
|
149
|
+
// the AbortSignal.timeout error name, with a message fallback for runtimes that
|
|
150
|
+
// don't set it.
|
|
151
|
+
if ((error instanceof Error && error.name === "TimeoutError") ||
|
|
152
|
+
message.includes("timeout") ||
|
|
153
|
+
message.includes("TIMEOUT")) {
|
|
146
154
|
return {
|
|
147
155
|
name: "API",
|
|
148
156
|
status: "error",
|