@mnemom/mnemom 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents.d.ts +37 -1
- package/dist/commands/agents.js +234 -9
- package/dist/commands/card.d.ts +4 -1
- package/dist/commands/card.js +134 -19
- 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 +79 -4
- package/dist/commands/status.js +12 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +52 -13
- package/dist/lib/api.d.ts +301 -111
- package/dist/lib/api.js +450 -294
- package/dist/lib/webhooks-api.js +10 -5
- package/dist/version.d.ts +1 -0
- package/dist/version.js +16 -0
- package/package.json +7 -2
|
@@ -1 +1,37 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface AgentsListOptions {
|
|
2
|
+
/** Scope to a single org. When omitted, aggregates across every org you belong to. */
|
|
3
|
+
org?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function agentsListCommand(options?: AgentsListOptions): Promise<void>;
|
|
6
|
+
export interface AgentsClaimOptions {
|
|
7
|
+
/** Org slug OR org_id to claim into. Omit → land in your personal org (the API default). */
|
|
8
|
+
org?: string;
|
|
9
|
+
/** The agent's provider API key — the CLI derives the hash proof from it. */
|
|
10
|
+
key?: string;
|
|
11
|
+
/** A pre-computed 64-hex hash proof (advanced/CI; alternative to --key). */
|
|
12
|
+
hashProof?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The name the agent was provisioned with, used in the proof derivation
|
|
15
|
+
* `sha256(`${key}|${name}`)`. Defaults to a name-shaped positional arg, else
|
|
16
|
+
* the agent is treated as an unnamed singleton (`sha256(key)`).
|
|
17
|
+
*/
|
|
18
|
+
name?: string;
|
|
19
|
+
json?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Derive the full 64-hex hash proof the claim endpoint expects:
|
|
23
|
+
* `sha256(`${apiKey}|${agentName}`)` for a named agent, or `sha256(apiKey)`
|
|
24
|
+
* for an unnamed singleton. This mirrors the gateway's provisioning
|
|
25
|
+
* derivation (gateway/src/index.ts) and agent-proof.ts on the API side.
|
|
26
|
+
*/
|
|
27
|
+
export declare function deriveHashProof(apiKey: string, agentName?: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* `mnemom agents claim <id-or-name> [--org <slug>] (--key <k> | --hash-proof <h>)`
|
|
30
|
+
*
|
|
31
|
+
* Claims an agent into an org (ADR-062). Possession of the agent's key (via
|
|
32
|
+
* `--key`, from which the CLI derives the proof) or a pre-computed
|
|
33
|
+
* `--hash-proof` authenticates the claim; `--org` selects the landing org
|
|
34
|
+
* (omit → your personal org). On a not-a-member 403 the command teaches by
|
|
35
|
+
* listing your claimable orgs.
|
|
36
|
+
*/
|
|
37
|
+
export declare function agentsClaimCommand(idOrName: string, options?: AgentsClaimOptions): Promise<void>;
|
package/dist/commands/agents.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { listOrgAgents, listMyOrgs, getAgentByName, claimAgent, MnemomApiError, } from "../lib/api.js";
|
|
2
3
|
import { requireAuth } from "../lib/auth.js";
|
|
3
4
|
import { fmt } from "../lib/format.js";
|
|
4
|
-
export async function agentsListCommand() {
|
|
5
|
+
export async function agentsListCommand(options = {}) {
|
|
5
6
|
await requireAuth();
|
|
6
7
|
console.log(fmt.header("Agents"));
|
|
7
8
|
console.log();
|
|
8
9
|
let agents;
|
|
9
10
|
try {
|
|
10
|
-
agents = await
|
|
11
|
+
agents = await listOrgAgents(options.org);
|
|
11
12
|
}
|
|
12
13
|
catch (err) {
|
|
13
14
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -15,24 +16,248 @@ export async function agentsListCommand() {
|
|
|
15
16
|
process.exit(1);
|
|
16
17
|
}
|
|
17
18
|
if (agents.length === 0) {
|
|
18
|
-
console.log(" No agents found.\n");
|
|
19
|
+
console.log(options.org ? ` No agents found in org '${options.org}'.\n` : " No agents found.\n");
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
|
-
//
|
|
22
|
+
// Show the Org column only when aggregating across orgs; with --org it's redundant.
|
|
23
|
+
const showOrg = !options.org;
|
|
22
24
|
const nameW = 24;
|
|
23
25
|
const idW = 40;
|
|
24
26
|
const seenW = 14;
|
|
25
27
|
const statusW = 14;
|
|
26
|
-
const
|
|
28
|
+
const orgW = 22;
|
|
29
|
+
let header = "Name".padEnd(nameW) +
|
|
30
|
+
"ID".padEnd(idW) +
|
|
31
|
+
"Last Seen".padEnd(seenW) +
|
|
32
|
+
"Containment".padEnd(statusW);
|
|
33
|
+
if (showOrg)
|
|
34
|
+
header += "Org";
|
|
27
35
|
console.log(` ${header}`);
|
|
28
|
-
console.log(` ${"─".repeat(nameW + idW + seenW + statusW)}`);
|
|
36
|
+
console.log(` ${"─".repeat(nameW + idW + seenW + statusW + (showOrg ? orgW : 0))}`);
|
|
29
37
|
for (const agent of agents) {
|
|
30
38
|
const name = (agent.name ?? "-").slice(0, nameW - 2).padEnd(nameW);
|
|
31
39
|
const id = agent.id.padEnd(idW);
|
|
32
40
|
const lastSeen = agent.last_seen ? new Date(agent.last_seen).toLocaleDateString() : "-";
|
|
33
41
|
const seen = lastSeen.padEnd(seenW);
|
|
34
|
-
const containment = agent.containment_status ?? "-";
|
|
35
|
-
|
|
42
|
+
const containment = (agent.containment_status ?? "-").padEnd(statusW);
|
|
43
|
+
const org = showOrg ? agent.org_name.slice(0, orgW - 2) : "";
|
|
44
|
+
console.log(` ${name}${id}${seen}${containment}${org}`);
|
|
36
45
|
}
|
|
37
46
|
console.log(`\n Total: ${agents.length} agent(s)\n`);
|
|
38
47
|
}
|
|
48
|
+
// ─── agents claim (ADR-062 claim-to-org) ──────────────────────────────────
|
|
49
|
+
/** Matches the canonical agent-id shapes (mirrors resolveAgentId in lib/api). */
|
|
50
|
+
const AGENT_ID_RE = /^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/;
|
|
51
|
+
/** A presented hash proof is the full 64-hex SHA-256 digest. */
|
|
52
|
+
const FULL_PROOF_RE = /^[0-9a-f]{64}$/;
|
|
53
|
+
// SHA-256 → lowercase hex. NOTE the apiKey-derived input below: this is NOT
|
|
54
|
+
// password-at-rest hashing — it's the platform's deterministic proof-of-
|
|
55
|
+
// possession (`hash_proof`). It MUST equal the server's stored value, which the
|
|
56
|
+
// gateway + mnemom-api/src/auth/agent-proof.ts compute as plain SHA-256 of
|
|
57
|
+
// `${apiKey}|${name}`. A slow KDF (bcrypt/scrypt) would produce a different
|
|
58
|
+
// digest and break the claim handshake entirely, so SHA-256 is mandated by the
|
|
59
|
+
// protocol — the "insufficient computational effort" alert is a false positive.
|
|
60
|
+
function sha256Hex(input) {
|
|
61
|
+
// lgtm[js/insufficient-password-hash] — protocol proof, not password storage (see note above)
|
|
62
|
+
return createHash("sha256").update(input, "utf8").digest("hex"); // codeql[js/insufficient-password-hash]
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Derive the full 64-hex hash proof the claim endpoint expects:
|
|
66
|
+
* `sha256(`${apiKey}|${agentName}`)` for a named agent, or `sha256(apiKey)`
|
|
67
|
+
* for an unnamed singleton. This mirrors the gateway's provisioning
|
|
68
|
+
* derivation (gateway/src/index.ts) and agent-proof.ts on the API side.
|
|
69
|
+
*/
|
|
70
|
+
export function deriveHashProof(apiKey, agentName) {
|
|
71
|
+
const key = apiKey.trim();
|
|
72
|
+
const name = agentName?.trim();
|
|
73
|
+
return name ? sha256Hex(`${key}|${name}`) : sha256Hex(key);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve an `--org <slug>` value to an org_id. Accepts either a slug or a raw
|
|
77
|
+
* org_id (both are matched against the caller's memberships). Returns the
|
|
78
|
+
* resolved org_id, or throws a teaching error listing the caller's orgs when
|
|
79
|
+
* the value matches none of them.
|
|
80
|
+
*/
|
|
81
|
+
function resolveOrgId(slugOrId, orgs) {
|
|
82
|
+
const needle = slugOrId.trim();
|
|
83
|
+
const match = orgs.find((o) => o.slug === needle) ?? orgs.find((o) => o.org_id === needle);
|
|
84
|
+
if (match)
|
|
85
|
+
return match.org_id;
|
|
86
|
+
const choices = orgs.map((o) => ` ${o.slug.padEnd(24)} ${o.name}`).join("\n");
|
|
87
|
+
throw new Error(`No org of yours matches '${needle}'. You can claim into one of:\n${choices}\n` +
|
|
88
|
+
` Pass --org <slug>, or omit --org to land in your personal org.`);
|
|
89
|
+
}
|
|
90
|
+
/** The not-a-member 403 code emitted by the merged endpoint. */
|
|
91
|
+
const NOT_A_MEMBER_CODE = "agent_org_not_member";
|
|
92
|
+
/**
|
|
93
|
+
* Distinguish a not-a-member 403 (the requested org isn't one of yours) from
|
|
94
|
+
* the other 403s this endpoint emits — `invalid_hash_proof` and
|
|
95
|
+
* `agent_cross_tenant` (already claimed by a different owner) — which should
|
|
96
|
+
* fall through to the generic error. Source-verified against #713: the
|
|
97
|
+
* not-a-member 403 carries `code: "agent_org_not_member"` and
|
|
98
|
+
* `details.claimable_orgs`. A loose match guards against minor drift.
|
|
99
|
+
*/
|
|
100
|
+
function isNotAMemberError(err) {
|
|
101
|
+
if (err.code === NOT_A_MEMBER_CODE)
|
|
102
|
+
return true;
|
|
103
|
+
if (extractClaimableOrgs(err.details).length > 0)
|
|
104
|
+
return true;
|
|
105
|
+
return /org_not_member|not_a_member|not_org_member/.test(err.code ?? "");
|
|
106
|
+
}
|
|
107
|
+
/** Pull the server-provided claimable-org list off a 403 body, if present. */
|
|
108
|
+
function extractClaimableOrgs(details) {
|
|
109
|
+
const list = details?.claimable_orgs;
|
|
110
|
+
if (!Array.isArray(list))
|
|
111
|
+
return [];
|
|
112
|
+
return list.filter((o) => !!o && typeof o === "object" && typeof o.org_id === "string");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* `mnemom agents claim <id-or-name> [--org <slug>] (--key <k> | --hash-proof <h>)`
|
|
116
|
+
*
|
|
117
|
+
* Claims an agent into an org (ADR-062). Possession of the agent's key (via
|
|
118
|
+
* `--key`, from which the CLI derives the proof) or a pre-computed
|
|
119
|
+
* `--hash-proof` authenticates the claim; `--org` selects the landing org
|
|
120
|
+
* (omit → your personal org). On a not-a-member 403 the command teaches by
|
|
121
|
+
* listing your claimable orgs.
|
|
122
|
+
*/
|
|
123
|
+
export async function agentsClaimCommand(idOrName, options = {}) {
|
|
124
|
+
await requireAuth();
|
|
125
|
+
// 1. Resolve the agent id to claim. An UNCLAIMED agent is not in your org
|
|
126
|
+
// fleet yet, so a name can only be resolved for agents you can already
|
|
127
|
+
// see; the reliable path for a fresh claim is to pass the agent id.
|
|
128
|
+
const looksLikeId = AGENT_ID_RE.test(idOrName);
|
|
129
|
+
let agentId = idOrName;
|
|
130
|
+
let derivationName = options.name;
|
|
131
|
+
if (!looksLikeId) {
|
|
132
|
+
// A name-shaped arg: try the fleet, else guide the user to the id.
|
|
133
|
+
const found = await getAgentByName(idOrName).catch(() => null);
|
|
134
|
+
if (found) {
|
|
135
|
+
agentId = found.id;
|
|
136
|
+
derivationName = derivationName ?? found.name ?? idOrName;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// The arg names an agent we can't see yet (unclaimed agents aren't in
|
|
140
|
+
// your fleet), so we have no id to address the claim — guide to the id.
|
|
141
|
+
console.log(fmt.error(`Could not resolve '${idOrName}' to an agent id you can see yet — unclaimed agents are not in your fleet.`));
|
|
142
|
+
console.log(fmt.dim(` Pass the agent id directly: mnemom agents claim mnm-... --key <key> [--name ${idOrName}]`) + "\n");
|
|
143
|
+
process.exit(1);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 2. Establish the hash proof: --hash-proof (raw) takes precedence, else
|
|
148
|
+
// derive from --key. Exactly one source is needed.
|
|
149
|
+
let hashProof;
|
|
150
|
+
if (options.hashProof) {
|
|
151
|
+
const proof = options.hashProof.trim().toLowerCase();
|
|
152
|
+
if (!FULL_PROOF_RE.test(proof)) {
|
|
153
|
+
console.log(fmt.error("--hash-proof should be the full 64-character hex SHA-256 proof.") + "\n");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
hashProof = proof;
|
|
158
|
+
}
|
|
159
|
+
else if (options.key) {
|
|
160
|
+
hashProof = deriveHashProof(options.key, derivationName);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(fmt.error("Claiming needs proof you hold the agent's key.") + "\n");
|
|
164
|
+
console.log(fmt.dim(" Pass --key <agent-api-key> (the CLI derives the proof), or --hash-proof <64-hex> directly.") + "\n");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// 3. Resolve --org slug → org_id (omit → server default / personal org).
|
|
169
|
+
let orgId;
|
|
170
|
+
let orgs = [];
|
|
171
|
+
if (options.org) {
|
|
172
|
+
try {
|
|
173
|
+
orgs = await listMyOrgs();
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
console.log(fmt.error(`Failed to read your orgs: ${msg}`) + "\n");
|
|
178
|
+
process.exit(1);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
orgId = resolveOrgId(options.org, orgs);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
186
|
+
console.log(fmt.error(msg) + "\n");
|
|
187
|
+
process.exit(1);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 4. Claim.
|
|
192
|
+
let result;
|
|
193
|
+
try {
|
|
194
|
+
result = await claimAgent(agentId, { hashProof, orgId });
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (err instanceof MnemomApiError && err.effectiveStatus === 403 && isNotAMemberError(err)) {
|
|
198
|
+
// Teaching error: list the orgs the caller can actually claim into.
|
|
199
|
+
// Prefer `mnemom org list` data (listMyOrgs) — it carries slugs, so we
|
|
200
|
+
// can print the actionable `--org <slug>`. The server's
|
|
201
|
+
// `details.claimable_orgs` (org_id + name, NO slug) is the fallback when
|
|
202
|
+
// the membership list is unavailable.
|
|
203
|
+
const memberOrgs = orgs.length ? orgs : await listMyOrgs().catch(() => []);
|
|
204
|
+
console.log(fmt.error(`You are not a member of '${options.org}', so the agent can't land there.`));
|
|
205
|
+
if (memberOrgs.length) {
|
|
206
|
+
console.log("\n You can claim into one of (use the slug):");
|
|
207
|
+
for (const o of memberOrgs) {
|
|
208
|
+
const tag = o.is_personal ? " (personal)" : "";
|
|
209
|
+
console.log(` ${o.slug.padEnd(24)} ${o.name}${tag}`);
|
|
210
|
+
}
|
|
211
|
+
console.log(`\n Re-run with --org <slug>, or omit --org to land in your personal org.\n`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const fromServer = extractClaimableOrgs(err.details);
|
|
215
|
+
if (fromServer.length) {
|
|
216
|
+
console.log("\n You can claim into one of (use the org id):");
|
|
217
|
+
for (const o of fromServer) {
|
|
218
|
+
const tag = o.is_personal ? " (personal)" : "";
|
|
219
|
+
console.log(` ${o.org_id.padEnd(40)} ${o.name}${tag}`);
|
|
220
|
+
}
|
|
221
|
+
console.log(`\n Re-run with --org <id>, or omit --org to land in your personal org.\n`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.log(fmt.dim(" Run `mnemom org list` to see the orgs you belong to.") + "\n");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
process.exit(1);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (err instanceof MnemomApiError && err.effectiveStatus === 503) {
|
|
231
|
+
// The platform is still provisioning the caller's personal org. Transient.
|
|
232
|
+
console.log(fmt.warn("Your personal org is still being set up. Give it a moment and run claim again."));
|
|
233
|
+
console.log("");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
238
|
+
console.log(fmt.error(`Failed to claim agent: ${msg}`) + "\n");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// 5. Report where it landed.
|
|
243
|
+
if (options.json) {
|
|
244
|
+
console.log(JSON.stringify(result, null, 2));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// Map the resolved org_id to a friendly name when we can.
|
|
248
|
+
const landed = result.org_id;
|
|
249
|
+
let where;
|
|
250
|
+
if (!landed) {
|
|
251
|
+
where = "your personal org (default scope)";
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const named = (orgs.length ? orgs : await listMyOrgs().catch(() => [])).find((o) => o.org_id === landed);
|
|
255
|
+
where = named ? `${named.name} (${named.slug})` : landed;
|
|
256
|
+
}
|
|
257
|
+
console.log(fmt.success(`Claimed ${result.agent_id}.`));
|
|
258
|
+
console.log(fmt.label(" Landed in:", where));
|
|
259
|
+
if (result.claimed_at) {
|
|
260
|
+
console.log(fmt.label(" Claimed at:", new Date(result.claimed_at).toLocaleString()));
|
|
261
|
+
}
|
|
262
|
+
console.log();
|
|
263
|
+
}
|
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();
|
|
@@ -694,9 +807,11 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
694
807
|
},
|
|
695
808
|
audit: { retention_days: 30, queryable: false, trace_format: "otel" },
|
|
696
809
|
}, { lineWidth: 120, noRefs: true });
|
|
697
|
-
// Write to temp
|
|
698
|
-
|
|
699
|
-
|
|
810
|
+
// Write to a per-invocation temp dir created with mkdtemp (mode 0700,
|
|
811
|
+
// unpredictable suffix) so the editor file can't be pre-created or
|
|
812
|
+
// symlink-raced by another user in the shared os.tmpdir().
|
|
813
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mnemom-card-"));
|
|
814
|
+
const tmpFile = path.join(tmpDir, `${agentId}.yaml`);
|
|
700
815
|
fs.writeFileSync(tmpFile, cardYaml);
|
|
701
816
|
// Open in editor
|
|
702
817
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
@@ -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
|