@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
package/dist/lib/api.js
CHANGED
|
@@ -26,15 +26,95 @@ function validateUrl(url) {
|
|
|
26
26
|
export function newIdempotencyKey() {
|
|
27
27
|
return randomUUID();
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract the human message + code + details from a raw error body,
|
|
31
|
+
* tolerating all three live envelope shapes (prefer nested).
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeApiError(raw) {
|
|
34
|
+
if (!raw || typeof raw !== "object")
|
|
35
|
+
return {};
|
|
36
|
+
let message;
|
|
37
|
+
let code;
|
|
38
|
+
let details;
|
|
39
|
+
const e = raw.error;
|
|
40
|
+
if (e && typeof e === "object") {
|
|
41
|
+
// Nested {error:{code,message,details}} (enforce-hook).
|
|
42
|
+
message = typeof e.message === "string" ? e.message : undefined;
|
|
43
|
+
code = typeof e.code === "string" ? e.code : undefined;
|
|
44
|
+
details = e.details;
|
|
45
|
+
}
|
|
46
|
+
else if (typeof e === "string") {
|
|
47
|
+
// Flat {error:"message"} (+ optional top-level code on auth routes).
|
|
48
|
+
message = e;
|
|
49
|
+
}
|
|
50
|
+
// Fallbacks: top-level message/code/details when the above didn't populate.
|
|
51
|
+
if (!message && typeof raw.message === "string")
|
|
52
|
+
message = raw.message;
|
|
53
|
+
if (!code && typeof raw.code === "string")
|
|
54
|
+
code = raw.code;
|
|
55
|
+
if (details === undefined)
|
|
56
|
+
details = raw.details;
|
|
57
|
+
const spec_deviation = raw.spec_deviation && typeof raw.spec_deviation === "object" ? raw.spec_deviation : undefined;
|
|
58
|
+
return { message, code, details, conflict_agent_id: raw.conflict_agent_id, spec_deviation };
|
|
59
|
+
}
|
|
60
|
+
/** Parse a non-OK Response body into normalized error fields (never throws). */
|
|
61
|
+
export async function parseApiErrorBody(response) {
|
|
62
|
+
const raw = (await response.json().catch(() => null));
|
|
63
|
+
return normalizeApiError(raw);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Structured API error. `message` is the human-readable server message (or a
|
|
67
|
+
* fallback); `.status`/`.code`/`.details`/`.conflictAgentId` give callers a
|
|
68
|
+
* structured surface to branch on without parsing the message string.
|
|
69
|
+
* `instanceof MnemomApiError` narrows in command-layer catch blocks.
|
|
70
|
+
*/
|
|
71
|
+
export class MnemomApiError extends Error {
|
|
72
|
+
/** The wire HTTP status — may be a synthetic 500 from the enforce hook. */
|
|
73
|
+
status;
|
|
74
|
+
/**
|
|
75
|
+
* The TRUE intended status: `spec_deviation.original_status ?? status`. The
|
|
76
|
+
* enforce hook can rewrite an undocumented status to a synthetic 500; command
|
|
77
|
+
* layers MUST branch on `effectiveStatus` (not `status`) for 404/empty-state
|
|
78
|
+
* detection or they'll be fooled by the rewrite.
|
|
79
|
+
*/
|
|
80
|
+
effectiveStatus;
|
|
81
|
+
code;
|
|
82
|
+
details;
|
|
83
|
+
conflictAgentId;
|
|
84
|
+
/** Top-level enforce-hook sibling (present only on a spec-rewritten body). */
|
|
85
|
+
specDeviation;
|
|
86
|
+
constructor(status, message, opts = {}) {
|
|
87
|
+
super(message);
|
|
88
|
+
this.name = "MnemomApiError";
|
|
89
|
+
this.status = status;
|
|
90
|
+
this.code = opts.code;
|
|
91
|
+
this.details = opts.details;
|
|
92
|
+
this.conflictAgentId = opts.conflictAgentId;
|
|
93
|
+
this.specDeviation = opts.specDeviation;
|
|
94
|
+
this.effectiveStatus = opts.specDeviation?.original_status ?? status;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Read a non-OK Response into a MnemomApiError, preferring the server message
|
|
99
|
+
* and falling back to `${fallback}: ${status}` when the body carried none.
|
|
100
|
+
* Captures the enforce-hook `spec_deviation` sibling so `.effectiveStatus`
|
|
101
|
+
* reflects the true intended status behind any synthetic-500 rewrite.
|
|
102
|
+
*/
|
|
103
|
+
export async function readApiError(response, fallback) {
|
|
104
|
+
const parsed = await parseApiErrorBody(response);
|
|
105
|
+
const message = parsed.message || `${fallback}: ${response.status}`;
|
|
106
|
+
return new MnemomApiError(response.status, message, {
|
|
107
|
+
code: parsed.code,
|
|
108
|
+
details: parsed.details,
|
|
109
|
+
conflictAgentId: parsed.conflict_agent_id,
|
|
110
|
+
specDeviation: parsed.spec_deviation,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
29
113
|
async function fetchApi(endpoint) {
|
|
30
114
|
const url = validateUrl(`${API_BASE}${endpoint}`);
|
|
31
115
|
const response = await fetch(url);
|
|
32
116
|
if (!response.ok) {
|
|
33
|
-
|
|
34
|
-
error: "unknown",
|
|
35
|
-
message: response.statusText,
|
|
36
|
-
})));
|
|
37
|
-
throw new Error(error.message || `API request failed: ${response.status}`);
|
|
117
|
+
throw await readApiError(response, "API request failed");
|
|
38
118
|
}
|
|
39
119
|
return response.json();
|
|
40
120
|
}
|
|
@@ -58,16 +138,22 @@ export async function postApi(endpoint, body, opts = {}) {
|
|
|
58
138
|
body: JSON.stringify(body),
|
|
59
139
|
});
|
|
60
140
|
if (!response.ok) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
141
|
+
const parsed = await parseApiErrorBody(response);
|
|
142
|
+
const msg = parsed.message || `HTTP ${response.status}`;
|
|
143
|
+
// Preserve the 409 conflict_agent_id surface: carry it structurally on the
|
|
144
|
+
// error (.conflictAgentId) AND keep it in the message for back-compat with
|
|
145
|
+
// any callers still string-matching the old "409: ... (conflict: ...)" form.
|
|
146
|
+
if (response.status === 409 && parsed.conflict_agent_id) {
|
|
147
|
+
throw new MnemomApiError(409, `409: ${msg} (conflict: ${parsed.conflict_agent_id})`, {
|
|
148
|
+
code: parsed.code,
|
|
149
|
+
details: parsed.details,
|
|
150
|
+
conflictAgentId: parsed.conflict_agent_id,
|
|
151
|
+
});
|
|
69
152
|
}
|
|
70
|
-
throw new
|
|
153
|
+
throw new MnemomApiError(response.status, `${response.status}: ${msg}`, {
|
|
154
|
+
code: parsed.code,
|
|
155
|
+
details: parsed.details,
|
|
156
|
+
});
|
|
71
157
|
}
|
|
72
158
|
return response.json();
|
|
73
159
|
}
|
|
@@ -119,6 +205,13 @@ async function fetchWithAuthRetry(url, buildInit) {
|
|
|
119
205
|
export async function getAgent(id) {
|
|
120
206
|
return fetchApi(`/v1/agents/${id}`);
|
|
121
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* @deprecated Legacy per-user listing — `GET /v1/agents` is scoped by the
|
|
210
|
+
* caller's `claimed_by` rows, which ADR-062 retired as an authorization /
|
|
211
|
+
* listing boundary (org_id is now the sole boundary). New code must list via
|
|
212
|
+
* {@link listOrgAgents} (org-scoped). Retained only for back-compat callers;
|
|
213
|
+
* the `mnemom agents` command and name resolution no longer use it.
|
|
214
|
+
*/
|
|
122
215
|
export async function listAgents() {
|
|
123
216
|
const url = validateUrl(`${API_BASE}/v1/agents?limit=100`);
|
|
124
217
|
const response = await fetchWithAuthRetry(url, async () => ({
|
|
@@ -126,10 +219,9 @@ export async function listAgents() {
|
|
|
126
219
|
}));
|
|
127
220
|
if (!response.ok) {
|
|
128
221
|
if (response.status === 401) {
|
|
129
|
-
throw new
|
|
222
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
130
223
|
}
|
|
131
|
-
|
|
132
|
-
throw new Error(err.message || `Failed to list agents: ${response.status}`);
|
|
224
|
+
throw await readApiError(response, "Failed to list agents");
|
|
133
225
|
}
|
|
134
226
|
const data = (await response.json());
|
|
135
227
|
return data.agents ?? [];
|
|
@@ -146,14 +238,134 @@ export async function listMyOrgs() {
|
|
|
146
238
|
}));
|
|
147
239
|
if (!response.ok) {
|
|
148
240
|
if (response.status === 401) {
|
|
149
|
-
throw new
|
|
241
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
150
242
|
}
|
|
151
|
-
|
|
152
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
243
|
+
throw await readApiError(response, "API request failed");
|
|
153
244
|
}
|
|
154
245
|
const data = (await response.json());
|
|
155
246
|
return data.orgs ?? [];
|
|
156
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* GET /v1/orgs/:org_id/agents — the org-scoped agent fleet (ADR-062). Any
|
|
250
|
+
* member of the org may read it; non-members get 403.
|
|
251
|
+
*/
|
|
252
|
+
export async function fetchOrgFleet(orgId) {
|
|
253
|
+
const url = validateUrl(`${API_BASE}/v1/orgs/${encodeURIComponent(orgId)}/agents`);
|
|
254
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
255
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
256
|
+
}));
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
if (response.status === 401) {
|
|
259
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
260
|
+
}
|
|
261
|
+
throw await readApiError(response, "Failed to list org agents");
|
|
262
|
+
}
|
|
263
|
+
const data = (await response.json());
|
|
264
|
+
return data.agents ?? [];
|
|
265
|
+
}
|
|
266
|
+
function fleetToRow(a, org) {
|
|
267
|
+
return {
|
|
268
|
+
id: a.agent_id,
|
|
269
|
+
name: a.agent_name ?? null,
|
|
270
|
+
email: a.owner_email ?? null,
|
|
271
|
+
created_at: a.created_at,
|
|
272
|
+
last_seen: a.last_seen ?? null,
|
|
273
|
+
containment_status: a.containment_status ?? null,
|
|
274
|
+
org_id: org.org_id,
|
|
275
|
+
org_name: org.name,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* List agents the ADR-062 way: scoped by org membership, never by `claimed_by`.
|
|
280
|
+
*
|
|
281
|
+
* - With `orgId`: lists that single org's fleet.
|
|
282
|
+
* - Without `orgId`: aggregates the fleet across every org the caller belongs
|
|
283
|
+
* to (the same pattern as {@link listAllTeams}), tagging each row with its
|
|
284
|
+
* org. Orgs the caller can't read (403/404) are skipped silently so one
|
|
285
|
+
* inaccessible org doesn't fail the whole listing.
|
|
286
|
+
*/
|
|
287
|
+
export async function listOrgAgents(orgId) {
|
|
288
|
+
let orgs;
|
|
289
|
+
if (orgId) {
|
|
290
|
+
// Resolve a friendly name if the caller is a member; fall back to the id.
|
|
291
|
+
const mine = await listMyOrgs().catch(() => []);
|
|
292
|
+
const match = mine.find((o) => o.org_id === orgId);
|
|
293
|
+
orgs = [{ org_id: orgId, name: match?.name ?? orgId }];
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
orgs = (await listMyOrgs()).map((o) => ({ org_id: o.org_id, name: o.name }));
|
|
297
|
+
}
|
|
298
|
+
const rows = [];
|
|
299
|
+
const seen = new Set();
|
|
300
|
+
for (const org of orgs) {
|
|
301
|
+
let fleet;
|
|
302
|
+
try {
|
|
303
|
+
fleet = await fetchOrgFleet(org.org_id);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
// A specific --org that we can't read is a real error worth surfacing;
|
|
307
|
+
// a skipped org during aggregation is not.
|
|
308
|
+
if (orgId)
|
|
309
|
+
throw err;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
for (const a of fleet) {
|
|
313
|
+
if (seen.has(a.agent_id))
|
|
314
|
+
continue;
|
|
315
|
+
seen.add(a.agent_id);
|
|
316
|
+
rows.push(fleetToRow(a, org));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return rows;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* POST /v1/agents/:id/claim — claim an agent into an org (ADR-062).
|
|
323
|
+
*
|
|
324
|
+
* Body is `{ hash_proof, org_id? }`. `hash_proof` (the agent's full SHA-256
|
|
325
|
+
* proof) authenticates the caller as the agent's owner; the user's own auth
|
|
326
|
+
* header rides along too so the platform can validate org membership and link
|
|
327
|
+
* the principal. Omit `orgId` to land in the caller's personal org (the
|
|
328
|
+
* platform default); a supplied `orgId` is validated against the caller's
|
|
329
|
+
* memberships server-side (a non-member gets a 403 the command layer turns
|
|
330
|
+
* into a teaching error listing claimable orgs).
|
|
331
|
+
*
|
|
332
|
+
* An Idempotency-Key is minted (and held across the 401-refresh retry) so a
|
|
333
|
+
* retried claim replays the same logical operation server-side.
|
|
334
|
+
*/
|
|
335
|
+
export async function claimAgent(agentId, body, opts = {}) {
|
|
336
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${encodeURIComponent(agentId)}/claim`);
|
|
337
|
+
const payload = {
|
|
338
|
+
hash_proof: sanitizeForHttp(body.hashProof),
|
|
339
|
+
};
|
|
340
|
+
if (body.orgId)
|
|
341
|
+
payload.org_id = sanitizeForHttp(body.orgId);
|
|
342
|
+
const idempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
|
|
343
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: {
|
|
346
|
+
...(await authHeaders()),
|
|
347
|
+
"Content-Type": "application/json",
|
|
348
|
+
Accept: "application/json",
|
|
349
|
+
"Idempotency-Key": idempotencyKey,
|
|
350
|
+
},
|
|
351
|
+
body: JSON.stringify(payload),
|
|
352
|
+
}));
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
if (response.status === 401) {
|
|
355
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
356
|
+
}
|
|
357
|
+
throw await readApiError(response, "Failed to claim agent");
|
|
358
|
+
}
|
|
359
|
+
const data = (await response.json());
|
|
360
|
+
return {
|
|
361
|
+
claimed: data.claimed ?? true,
|
|
362
|
+
agent_id: data.agent_id ?? agentId,
|
|
363
|
+
// Reflect what the server resolved — null (not "") when absent/unscoped,
|
|
364
|
+
// matching the locked SDK contract so the two surfaces don't drift.
|
|
365
|
+
org_id: data.org_id ?? null,
|
|
366
|
+
claimed_at: data.claimed_at ?? null,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
157
369
|
/**
|
|
158
370
|
* GET /v1/auth/me/personal-org — accessor for the user's personal org.
|
|
159
371
|
* Idempotent: lazily provisions for legacy accounts that pre-date the
|
|
@@ -166,10 +378,9 @@ export async function getMyPersonalOrg() {
|
|
|
166
378
|
}));
|
|
167
379
|
if (!response.ok) {
|
|
168
380
|
if (response.status === 401) {
|
|
169
|
-
throw new
|
|
381
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
170
382
|
}
|
|
171
|
-
|
|
172
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
383
|
+
throw await readApiError(response, "API request failed");
|
|
173
384
|
}
|
|
174
385
|
return (await response.json());
|
|
175
386
|
}
|
|
@@ -211,13 +422,12 @@ export async function getTeam(teamId) {
|
|
|
211
422
|
}));
|
|
212
423
|
if (!response.ok) {
|
|
213
424
|
if (response.status === 401) {
|
|
214
|
-
throw new
|
|
425
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
215
426
|
}
|
|
216
427
|
if (response.status === 404) {
|
|
217
|
-
throw new
|
|
428
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found or not accessible.`);
|
|
218
429
|
}
|
|
219
|
-
|
|
220
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
430
|
+
throw await readApiError(response, "API request failed");
|
|
221
431
|
}
|
|
222
432
|
return (await response.json());
|
|
223
433
|
}
|
|
@@ -234,16 +444,15 @@ export async function getTeamTemplate(teamId, kind) {
|
|
|
234
444
|
}));
|
|
235
445
|
if (!response.ok) {
|
|
236
446
|
if (response.status === 401) {
|
|
237
|
-
throw new
|
|
447
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
238
448
|
}
|
|
239
449
|
if (response.status === 403) {
|
|
240
|
-
throw new
|
|
450
|
+
throw new MnemomApiError(403, `Forbidden: not a member of team '${teamId}'s org.`);
|
|
241
451
|
}
|
|
242
452
|
if (response.status === 404) {
|
|
243
|
-
throw new
|
|
453
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
244
454
|
}
|
|
245
|
-
|
|
246
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
455
|
+
throw await readApiError(response, "API request failed");
|
|
247
456
|
}
|
|
248
457
|
return (await response.json());
|
|
249
458
|
}
|
|
@@ -271,19 +480,18 @@ export async function putTeamTemplate(teamId, kind, yamlBody) {
|
|
|
271
480
|
}));
|
|
272
481
|
if (!response.ok) {
|
|
273
482
|
if (response.status === 401) {
|
|
274
|
-
throw new
|
|
483
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
275
484
|
}
|
|
276
485
|
if (response.status === 403) {
|
|
277
|
-
throw new
|
|
486
|
+
throw new MnemomApiError(403, `Forbidden: org admin or owner role required to write a team template.`);
|
|
278
487
|
}
|
|
279
488
|
if (response.status === 404) {
|
|
280
|
-
throw new
|
|
489
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
281
490
|
}
|
|
282
491
|
if (response.status === 413) {
|
|
283
|
-
throw new
|
|
492
|
+
throw new MnemomApiError(413, `Template too large (server limit: 128 KiB alignment / 64 KiB protection).`);
|
|
284
493
|
}
|
|
285
|
-
|
|
286
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
494
|
+
throw await readApiError(response, "API request failed");
|
|
287
495
|
}
|
|
288
496
|
return (await response.json());
|
|
289
497
|
}
|
|
@@ -305,16 +513,15 @@ export async function deleteTeamTemplate(teamId, kind) {
|
|
|
305
513
|
}));
|
|
306
514
|
if (!response.ok) {
|
|
307
515
|
if (response.status === 401) {
|
|
308
|
-
throw new
|
|
516
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
309
517
|
}
|
|
310
518
|
if (response.status === 403) {
|
|
311
|
-
throw new
|
|
519
|
+
throw new MnemomApiError(403, `Forbidden: org admin or owner role required to clear a team template.`);
|
|
312
520
|
}
|
|
313
521
|
if (response.status === 404) {
|
|
314
|
-
throw new
|
|
522
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
315
523
|
}
|
|
316
|
-
|
|
317
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
524
|
+
throw await readApiError(response, "API request failed");
|
|
318
525
|
}
|
|
319
526
|
return (await response.json());
|
|
320
527
|
}
|
|
@@ -337,8 +544,7 @@ export async function previewComposeTeamTemplate(teamId, kind, yamlBody) {
|
|
|
337
544
|
body: yamlBody,
|
|
338
545
|
}));
|
|
339
546
|
if (!response.ok) {
|
|
340
|
-
|
|
341
|
-
throw new Error(err.message || `Preview failed: ${response.status}`);
|
|
547
|
+
throw await readApiError(response, "Preview failed");
|
|
342
548
|
}
|
|
343
549
|
const body = (await response.json());
|
|
344
550
|
return { composed: body.composed, conflicts: body.conflicts };
|
|
@@ -365,16 +571,15 @@ export async function grantTeamAdmin(teamId, userId) {
|
|
|
365
571
|
}));
|
|
366
572
|
if (!response.ok) {
|
|
367
573
|
if (response.status === 401) {
|
|
368
|
-
throw new
|
|
574
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
369
575
|
}
|
|
370
576
|
if (response.status === 403) {
|
|
371
|
-
throw new
|
|
577
|
+
throw new MnemomApiError(403, `Forbidden: org owner or admin role required, and target user must be in the team's org.`);
|
|
372
578
|
}
|
|
373
579
|
if (response.status === 404) {
|
|
374
|
-
throw new
|
|
580
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
375
581
|
}
|
|
376
|
-
|
|
377
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
582
|
+
throw await readApiError(response, "API request failed");
|
|
378
583
|
}
|
|
379
584
|
return (await response.json());
|
|
380
585
|
}
|
|
@@ -398,16 +603,15 @@ export async function revokeTeamAdmin(teamId, userId) {
|
|
|
398
603
|
}));
|
|
399
604
|
if (!response.ok) {
|
|
400
605
|
if (response.status === 401) {
|
|
401
|
-
throw new
|
|
606
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
402
607
|
}
|
|
403
608
|
if (response.status === 403) {
|
|
404
|
-
throw new
|
|
609
|
+
throw new MnemomApiError(403, `Forbidden: revoke requires org owner/admin, or self-revoke with an active grant.`);
|
|
405
610
|
}
|
|
406
611
|
if (response.status === 404) {
|
|
407
|
-
throw new
|
|
612
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
408
613
|
}
|
|
409
|
-
|
|
410
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
614
|
+
throw await readApiError(response, "API request failed");
|
|
411
615
|
}
|
|
412
616
|
return (await response.json());
|
|
413
617
|
}
|
|
@@ -422,28 +626,27 @@ export async function listTeamAdmins(teamId) {
|
|
|
422
626
|
}));
|
|
423
627
|
if (!response.ok) {
|
|
424
628
|
if (response.status === 401) {
|
|
425
|
-
throw new
|
|
629
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
426
630
|
}
|
|
427
631
|
if (response.status === 403) {
|
|
428
|
-
throw new
|
|
632
|
+
throw new MnemomApiError(403, `Forbidden: requires membership in the team's org.`);
|
|
429
633
|
}
|
|
430
634
|
if (response.status === 404) {
|
|
431
|
-
throw new
|
|
635
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
432
636
|
}
|
|
433
|
-
|
|
434
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
637
|
+
throw await readApiError(response, "API request failed");
|
|
435
638
|
}
|
|
436
639
|
return (await response.json());
|
|
437
640
|
}
|
|
438
641
|
/**
|
|
439
|
-
* Look up an agent
|
|
642
|
+
* Look up an agent by name across the caller's org fleets (ADR-062-scoped via
|
|
643
|
+
* {@link listOrgAgents}, not the legacy `claimed_by` list).
|
|
440
644
|
* Tries exact match first, then single partial match.
|
|
441
645
|
* Throws if multiple agents partially match (ambiguous).
|
|
442
646
|
* Returns null if no match found.
|
|
443
|
-
* Note: capped at 100 agents by listAgents().
|
|
444
647
|
*/
|
|
445
648
|
export async function getAgentByName(name) {
|
|
446
|
-
const agents = await
|
|
649
|
+
const agents = await listOrgAgents();
|
|
447
650
|
const lower = name.toLowerCase();
|
|
448
651
|
const exact = agents.find((a) => a.name?.toLowerCase() === lower);
|
|
449
652
|
if (exact)
|
|
@@ -472,11 +675,7 @@ export async function getIntegrity(id) {
|
|
|
472
675
|
headers: await authHeaders(),
|
|
473
676
|
}));
|
|
474
677
|
if (!response.ok) {
|
|
475
|
-
|
|
476
|
-
error: "unknown",
|
|
477
|
-
message: response.statusText,
|
|
478
|
-
})));
|
|
479
|
-
throw new Error(error.message || `API request failed: ${response.status}`);
|
|
678
|
+
throw await readApiError(response, "API request failed");
|
|
480
679
|
}
|
|
481
680
|
// Accept both the canonical docs shape (object) and the RPC-wrapped shape
|
|
482
681
|
// (array of one row). The latter is what prod actually returns today on
|
|
@@ -518,11 +717,7 @@ export async function getTraces(id, limit = 10) {
|
|
|
518
717
|
headers: await authHeaders(),
|
|
519
718
|
}));
|
|
520
719
|
if (!response.ok) {
|
|
521
|
-
|
|
522
|
-
error: "unknown",
|
|
523
|
-
message: response.statusText,
|
|
524
|
-
})));
|
|
525
|
-
throw new Error(error.message || `API request failed: ${response.status}`);
|
|
720
|
+
throw await readApiError(response, "API request failed");
|
|
526
721
|
}
|
|
527
722
|
const data = (await response.json());
|
|
528
723
|
// Accept both the envelope shape (current API) and a bare array (defensive
|
|
@@ -531,105 +726,6 @@ export async function getTraces(id, limit = 10) {
|
|
|
531
726
|
return data;
|
|
532
727
|
return data.traces ?? [];
|
|
533
728
|
}
|
|
534
|
-
export async function getCard(agentId) {
|
|
535
|
-
try {
|
|
536
|
-
return await fetchApi(`/v1/agents/${agentId}/card`);
|
|
537
|
-
}
|
|
538
|
-
catch (error) {
|
|
539
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
540
|
-
if (message.includes("404") || message.includes("not found")) {
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
throw error;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
export async function updateCard(agentId, cardJson, opts = {}) {
|
|
547
|
-
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/card`);
|
|
548
|
-
const response = await fetch(url, {
|
|
549
|
-
method: "PATCH",
|
|
550
|
-
headers: {
|
|
551
|
-
"Content-Type": "application/json",
|
|
552
|
-
"Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
|
|
553
|
-
...(await authHeaders()),
|
|
554
|
-
},
|
|
555
|
-
body: sanitizeForHttp(JSON.stringify({ card_json: cardJson })),
|
|
556
|
-
});
|
|
557
|
-
if (!response.ok) {
|
|
558
|
-
const error = (await response.json().catch(() => ({
|
|
559
|
-
error: "unknown",
|
|
560
|
-
message: response.statusText,
|
|
561
|
-
})));
|
|
562
|
-
throw new Error(error.message || `Card update failed: ${response.status}`);
|
|
563
|
-
}
|
|
564
|
-
return response.json();
|
|
565
|
-
}
|
|
566
|
-
export async function reverifyAgent(agentId, opts = {}) {
|
|
567
|
-
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/reverify`);
|
|
568
|
-
const response = await fetch(url, {
|
|
569
|
-
method: "POST",
|
|
570
|
-
headers: {
|
|
571
|
-
"Content-Type": "application/json",
|
|
572
|
-
"Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
|
|
573
|
-
...(await authHeaders()),
|
|
574
|
-
},
|
|
575
|
-
});
|
|
576
|
-
if (!response.ok) {
|
|
577
|
-
const error = (await response.json().catch(() => ({
|
|
578
|
-
error: "unknown",
|
|
579
|
-
message: response.statusText,
|
|
580
|
-
})));
|
|
581
|
-
throw new Error(error.message || `Reverify failed: ${response.status}`);
|
|
582
|
-
}
|
|
583
|
-
return response.json();
|
|
584
|
-
}
|
|
585
|
-
export async function getPolicy(agentId) {
|
|
586
|
-
try {
|
|
587
|
-
return await fetchApi(`/v1/agents/${agentId}/policy`);
|
|
588
|
-
}
|
|
589
|
-
catch (error) {
|
|
590
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
591
|
-
if (message.includes("404") || message.includes("not found")) {
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
throw error;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
export async function publishPolicy(agentId, policyJson, opts = {}) {
|
|
598
|
-
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/policy`);
|
|
599
|
-
const response = await fetch(url, {
|
|
600
|
-
method: "PUT",
|
|
601
|
-
headers: {
|
|
602
|
-
"Content-Type": "application/json",
|
|
603
|
-
"Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
|
|
604
|
-
...(await authHeaders()),
|
|
605
|
-
},
|
|
606
|
-
body: sanitizeForHttp(JSON.stringify({ policy_json: policyJson })),
|
|
607
|
-
});
|
|
608
|
-
if (!response.ok) {
|
|
609
|
-
const error = (await response.json().catch(() => ({
|
|
610
|
-
error: "unknown",
|
|
611
|
-
message: response.statusText,
|
|
612
|
-
})));
|
|
613
|
-
throw new Error(error.message || `Policy publish failed: ${response.status}`);
|
|
614
|
-
}
|
|
615
|
-
return response.json();
|
|
616
|
-
}
|
|
617
|
-
export async function testPolicyHistorical(agentId, policyJson, limit = 50) {
|
|
618
|
-
const url = validateUrl(`${API_BASE}/v1/policies/evaluate/historical`);
|
|
619
|
-
const response = await fetch(url, {
|
|
620
|
-
method: "POST",
|
|
621
|
-
headers: { "Content-Type": "application/json", ...(await authHeaders()) },
|
|
622
|
-
body: sanitizeForHttp(JSON.stringify({ agent_id: agentId, policy_json: policyJson, limit })),
|
|
623
|
-
});
|
|
624
|
-
if (!response.ok) {
|
|
625
|
-
const error = (await response.json().catch(() => ({
|
|
626
|
-
error: "unknown",
|
|
627
|
-
message: response.statusText,
|
|
628
|
-
})));
|
|
629
|
-
throw new Error(error.message || `Historical evaluation failed: ${response.status}`);
|
|
630
|
-
}
|
|
631
|
-
return response.json();
|
|
632
|
-
}
|
|
633
729
|
// ============================================================================
|
|
634
730
|
// Unified Card API (UC-4+)
|
|
635
731
|
// ============================================================================
|
|
@@ -639,7 +735,10 @@ export async function testPolicyHistorical(agentId, policyJson, limit = 50) {
|
|
|
639
735
|
*/
|
|
640
736
|
export async function getAlignmentCard(agentId, format = "yaml") {
|
|
641
737
|
const accept = format === "yaml" ? "text/yaml" : "application/json";
|
|
642
|
-
|
|
738
|
+
// Canonical Resources × Scope × Verb surface (ADR-062 / cards-as-primitive
|
|
739
|
+
// W1.2b). The legacy `/v1/agents/:id/alignment-card` only 308-redirects to
|
|
740
|
+
// this — first-party callers target canonical directly (no legacy shapes).
|
|
741
|
+
const url = validateUrl(`${API_BASE}/v1/alignment/agent/${encodeURIComponent(agentId)}`);
|
|
643
742
|
const response = await fetch(url, {
|
|
644
743
|
headers: { Accept: accept, ...(await authHeaders()) },
|
|
645
744
|
});
|
|
@@ -647,11 +746,7 @@ export async function getAlignmentCard(agentId, format = "yaml") {
|
|
|
647
746
|
if (response.status === 404) {
|
|
648
747
|
return { body: "", contentType: "" };
|
|
649
748
|
}
|
|
650
|
-
|
|
651
|
-
error: "unknown",
|
|
652
|
-
message: response.statusText,
|
|
653
|
-
})));
|
|
654
|
-
throw new Error(error.message || `Failed to fetch alignment card: ${response.status}`);
|
|
749
|
+
throw await readApiError(response, "Failed to fetch alignment card");
|
|
655
750
|
}
|
|
656
751
|
const ct = response.headers.get("content-type") ?? "";
|
|
657
752
|
const body = await response.text();
|
|
@@ -664,7 +759,10 @@ export const ALIGNMENT_CARD_MAX_BYTES = 128 * 1024;
|
|
|
664
759
|
* Accepts YAML or JSON body; set contentType accordingly.
|
|
665
760
|
*/
|
|
666
761
|
export async function putAlignmentCard(agentId, body, contentType = "text/yaml", opts = {}) {
|
|
667
|
-
|
|
762
|
+
// Canonical Resources × Scope × Verb surface (ADR-062 / cards-as-primitive
|
|
763
|
+
// W1.2b). The legacy `/v1/agents/:id/alignment-card` only 308-redirects to
|
|
764
|
+
// this — first-party callers target canonical directly (no legacy shapes).
|
|
765
|
+
const url = validateUrl(`${API_BASE}/v1/alignment/agent/${encodeURIComponent(agentId)}`);
|
|
668
766
|
// Lock in a single Idempotency-Key for this logical mutation. If the auth
|
|
669
767
|
// token is stale and the first attempt returns 401, fetchWithAuthRetry
|
|
670
768
|
// refreshes the token and retries — but the Idempotency-Key must be the
|
|
@@ -689,11 +787,7 @@ export async function putAlignmentCard(agentId, body, contentType = "text/yaml",
|
|
|
689
787
|
body: sanitizedBody,
|
|
690
788
|
}));
|
|
691
789
|
if (!response.ok) {
|
|
692
|
-
|
|
693
|
-
error: "unknown",
|
|
694
|
-
message: response.statusText,
|
|
695
|
-
})));
|
|
696
|
-
throw new Error(error.message || `Failed to publish alignment card: ${response.status}`);
|
|
790
|
+
throw await readApiError(response, "Failed to publish alignment card");
|
|
697
791
|
}
|
|
698
792
|
return response.json();
|
|
699
793
|
}
|
|
@@ -702,7 +796,9 @@ export async function putAlignmentCard(agentId, body, contentType = "text/yaml",
|
|
|
702
796
|
*/
|
|
703
797
|
export async function getProtectionCard(agentId, format = "yaml") {
|
|
704
798
|
const accept = format === "yaml" ? "text/yaml" : "application/json";
|
|
705
|
-
|
|
799
|
+
// Canonical surface (ADR-062 / W1.2b); legacy `/v1/agents/:id/protection-card`
|
|
800
|
+
// only 308-redirects here. First-party callers use canonical directly.
|
|
801
|
+
const url = validateUrl(`${API_BASE}/v1/protection/agent/${encodeURIComponent(agentId)}`);
|
|
706
802
|
const response = await fetch(url, {
|
|
707
803
|
headers: { Accept: accept, ...(await authHeaders()) },
|
|
708
804
|
});
|
|
@@ -710,11 +806,7 @@ export async function getProtectionCard(agentId, format = "yaml") {
|
|
|
710
806
|
if (response.status === 404) {
|
|
711
807
|
return { body: "", contentType: "" };
|
|
712
808
|
}
|
|
713
|
-
|
|
714
|
-
error: "unknown",
|
|
715
|
-
message: response.statusText,
|
|
716
|
-
})));
|
|
717
|
-
throw new Error(error.message || `Failed to fetch protection card: ${response.status}`);
|
|
809
|
+
throw await readApiError(response, "Failed to fetch protection card");
|
|
718
810
|
}
|
|
719
811
|
const ct = response.headers.get("content-type") ?? "";
|
|
720
812
|
const body = await response.text();
|
|
@@ -726,7 +818,9 @@ export const PROTECTION_CARD_MAX_BYTES = 64 * 1024;
|
|
|
726
818
|
* Publish (create or update) a protection card.
|
|
727
819
|
*/
|
|
728
820
|
export async function putProtectionCard(agentId, body, contentType = "text/yaml", opts = {}) {
|
|
729
|
-
|
|
821
|
+
// Canonical surface (ADR-062 / W1.2b); legacy `/v1/agents/:id/protection-card`
|
|
822
|
+
// only 308-redirects here. First-party callers use canonical directly.
|
|
823
|
+
const url = validateUrl(`${API_BASE}/v1/protection/agent/${encodeURIComponent(agentId)}`);
|
|
730
824
|
// See putAlignmentCard for why the Idempotency-Key is computed once outside
|
|
731
825
|
// the retry closure.
|
|
732
826
|
const idempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
|
|
@@ -744,14 +838,50 @@ export async function putProtectionCard(agentId, body, contentType = "text/yaml"
|
|
|
744
838
|
body: sanitizedBody,
|
|
745
839
|
}));
|
|
746
840
|
if (!response.ok) {
|
|
747
|
-
|
|
748
|
-
error: "unknown",
|
|
749
|
-
message: response.statusText,
|
|
750
|
-
})));
|
|
751
|
-
throw new Error(error.message || `Failed to publish protection card: ${response.status}`);
|
|
841
|
+
throw await readApiError(response, "Failed to publish protection card");
|
|
752
842
|
}
|
|
753
843
|
return response.json();
|
|
754
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* Dry-run validate + compose a card body against the server's authoritative
|
|
847
|
+
* pipeline. Returns a structured result — a 422/400 validation failure is an
|
|
848
|
+
* expected `valid:false` outcome (NOT a thrown error). 401 throws (caller
|
|
849
|
+
* should fall back to offline validation); 403/5xx throw a MnemomApiError.
|
|
850
|
+
*/
|
|
851
|
+
export async function previewComposeAgentCard(agentId, kind, body, contentType = "text/yaml") {
|
|
852
|
+
const url = validateUrl(`${API_BASE}/v1/${kind}/agent/${encodeURIComponent(agentId)}/preview-compose`);
|
|
853
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
854
|
+
method: "POST",
|
|
855
|
+
headers: { ...(await authHeaders()), "Content-Type": contentType, Accept: "application/json" },
|
|
856
|
+
body: sanitizeForHttp(body),
|
|
857
|
+
}));
|
|
858
|
+
if (response.ok) {
|
|
859
|
+
const data = (await response.json());
|
|
860
|
+
return {
|
|
861
|
+
valid: true,
|
|
862
|
+
status: response.status,
|
|
863
|
+
composed: data.composed,
|
|
864
|
+
conflicts: data.conflicts ?? [],
|
|
865
|
+
coherence_violations: data.coherence_violations ?? [],
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
// 401 → unauthenticated: signal the caller to fall back to offline validation.
|
|
869
|
+
if (response.status === 401) {
|
|
870
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
871
|
+
}
|
|
872
|
+
// 400 / 422 → the card is invalid: an expected validation outcome, not an
|
|
873
|
+
// exception. Surface the care-framed findings structurally.
|
|
874
|
+
if (response.status === 400 || response.status === 422) {
|
|
875
|
+
const parsed = await parseApiErrorBody(response);
|
|
876
|
+
return {
|
|
877
|
+
valid: false,
|
|
878
|
+
status: response.status,
|
|
879
|
+
error: { code: parsed.code, message: parsed.message, details: parsed.details },
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
// 403 / 5xx → a genuine error (e.g. not the owner, server fault).
|
|
883
|
+
throw await readApiError(response, "Card validation (preview-compose) failed");
|
|
884
|
+
}
|
|
755
885
|
// ============================================================================
|
|
756
886
|
// Agent Resolution (server-side, no local config)
|
|
757
887
|
// ============================================================================
|
|
@@ -809,13 +939,12 @@ export async function listPostures(opts) {
|
|
|
809
939
|
}));
|
|
810
940
|
if (!response.ok) {
|
|
811
941
|
if (response.status === 401) {
|
|
812
|
-
throw new
|
|
942
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
813
943
|
}
|
|
814
944
|
if (response.status === 403) {
|
|
815
|
-
throw new
|
|
945
|
+
throw new MnemomApiError(403, `Forbidden: not a member of org '${opts.orgId}'.`);
|
|
816
946
|
}
|
|
817
|
-
|
|
818
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
947
|
+
throw await readApiError(response, "API request failed");
|
|
819
948
|
}
|
|
820
949
|
const data = (await response.json());
|
|
821
950
|
return data.postures ?? [];
|
|
@@ -828,14 +957,13 @@ export async function getPosture(postureId) {
|
|
|
828
957
|
}));
|
|
829
958
|
if (!response.ok) {
|
|
830
959
|
if (response.status === 401) {
|
|
831
|
-
throw new
|
|
960
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
832
961
|
}
|
|
833
962
|
if (response.status === 404)
|
|
834
|
-
throw new
|
|
963
|
+
throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
|
|
835
964
|
if (response.status === 403)
|
|
836
|
-
throw new
|
|
837
|
-
|
|
838
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
965
|
+
throw new MnemomApiError(403, `Forbidden: not visible to your account.`);
|
|
966
|
+
throw await readApiError(response, "API request failed");
|
|
839
967
|
}
|
|
840
968
|
return (await response.json());
|
|
841
969
|
}
|
|
@@ -847,11 +975,10 @@ export async function listPostureRevisions(postureId) {
|
|
|
847
975
|
}));
|
|
848
976
|
if (!response.ok) {
|
|
849
977
|
if (response.status === 401)
|
|
850
|
-
throw new
|
|
978
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
851
979
|
if (response.status === 404)
|
|
852
|
-
throw new
|
|
853
|
-
|
|
854
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
980
|
+
throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
|
|
981
|
+
throw await readApiError(response, "API request failed");
|
|
855
982
|
}
|
|
856
983
|
const data = (await response.json());
|
|
857
984
|
return data.revisions ?? [];
|
|
@@ -864,13 +991,16 @@ export async function diffPostureRevisions(postureId, fromNo, toNo) {
|
|
|
864
991
|
}));
|
|
865
992
|
if (!response.ok) {
|
|
866
993
|
if (response.status === 401)
|
|
867
|
-
throw new
|
|
994
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
868
995
|
if (response.status === 404) {
|
|
869
|
-
const errBody =
|
|
870
|
-
throw new
|
|
996
|
+
const errBody = await parseApiErrorBody(response);
|
|
997
|
+
throw new MnemomApiError(404, errBody.message || `Posture or revision not found.`, {
|
|
998
|
+
code: errBody.code,
|
|
999
|
+
details: errBody.details,
|
|
1000
|
+
specDeviation: errBody.spec_deviation,
|
|
1001
|
+
});
|
|
871
1002
|
}
|
|
872
|
-
|
|
873
|
-
throw new Error(err.message || `Diff failed: ${response.status}`);
|
|
1003
|
+
throw await readApiError(response, "Diff failed");
|
|
874
1004
|
}
|
|
875
1005
|
return (await response.json());
|
|
876
1006
|
}
|
|
@@ -889,16 +1019,15 @@ export async function createPosture(input) {
|
|
|
889
1019
|
}));
|
|
890
1020
|
if (!response.ok) {
|
|
891
1021
|
if (response.status === 401)
|
|
892
|
-
throw new
|
|
1022
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
893
1023
|
if (response.status === 403) {
|
|
894
|
-
throw new
|
|
1024
|
+
throw new MnemomApiError(403, `Forbidden: org owner/admin role required (or scope=platform reserved for migrations).`);
|
|
895
1025
|
}
|
|
896
1026
|
if (response.status === 409)
|
|
897
|
-
throw new
|
|
1027
|
+
throw new MnemomApiError(409, `A posture with this slug already exists in the org.`);
|
|
898
1028
|
if (response.status === 413)
|
|
899
|
-
throw new
|
|
900
|
-
|
|
901
|
-
throw new Error(err.message || `Create failed: ${response.status}`);
|
|
1029
|
+
throw new MnemomApiError(413, `Posture body too large (server limit: 256 KiB).`);
|
|
1030
|
+
throw await readApiError(response, "Create failed");
|
|
902
1031
|
}
|
|
903
1032
|
return (await response.json());
|
|
904
1033
|
}
|
|
@@ -917,16 +1046,15 @@ export async function updatePosture(postureId, input) {
|
|
|
917
1046
|
}));
|
|
918
1047
|
if (!response.ok) {
|
|
919
1048
|
if (response.status === 401)
|
|
920
|
-
throw new
|
|
1049
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
921
1050
|
if (response.status === 403) {
|
|
922
|
-
throw new
|
|
1051
|
+
throw new MnemomApiError(403, `Forbidden: org owner/admin required, or this is a Mnemom-shipped platform default (immutable — clone first).`);
|
|
923
1052
|
}
|
|
924
1053
|
if (response.status === 404)
|
|
925
|
-
throw new
|
|
1054
|
+
throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
|
|
926
1055
|
if (response.status === 413)
|
|
927
|
-
throw new
|
|
928
|
-
|
|
929
|
-
throw new Error(err.message || `Update failed: ${response.status}`);
|
|
1056
|
+
throw new MnemomApiError(413, `Posture body too large (server limit: 256 KiB).`);
|
|
1057
|
+
throw await readApiError(response, "Update failed");
|
|
930
1058
|
}
|
|
931
1059
|
return (await response.json());
|
|
932
1060
|
}
|
|
@@ -945,15 +1073,14 @@ export async function clonePosture(postureId, input) {
|
|
|
945
1073
|
}));
|
|
946
1074
|
if (!response.ok) {
|
|
947
1075
|
if (response.status === 401)
|
|
948
|
-
throw new
|
|
1076
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
949
1077
|
if (response.status === 403)
|
|
950
|
-
throw new
|
|
1078
|
+
throw new MnemomApiError(403, `Forbidden: org owner/admin required on target org.`);
|
|
951
1079
|
if (response.status === 404)
|
|
952
|
-
throw new
|
|
1080
|
+
throw new MnemomApiError(404, `Source posture '${postureId}' not found.`);
|
|
953
1081
|
if (response.status === 409)
|
|
954
|
-
throw new
|
|
955
|
-
|
|
956
|
-
throw new Error(err.message || `Clone failed: ${response.status}`);
|
|
1082
|
+
throw new MnemomApiError(409, `A posture with this slug already exists in the target org.`);
|
|
1083
|
+
throw await readApiError(response, "Clone failed");
|
|
957
1084
|
}
|
|
958
1085
|
return (await response.json());
|
|
959
1086
|
}
|
|
@@ -970,16 +1097,15 @@ export async function deletePosture(postureId) {
|
|
|
970
1097
|
}));
|
|
971
1098
|
if (!response.ok) {
|
|
972
1099
|
if (response.status === 401)
|
|
973
|
-
throw new
|
|
1100
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
974
1101
|
if (response.status === 403)
|
|
975
|
-
throw new
|
|
1102
|
+
throw new MnemomApiError(403, `Forbidden: platform defaults are immutable.`);
|
|
976
1103
|
if (response.status === 404)
|
|
977
|
-
throw new
|
|
1104
|
+
throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
|
|
978
1105
|
if (response.status === 409) {
|
|
979
|
-
throw new
|
|
1106
|
+
throw new MnemomApiError(409, `Cannot delete a posture that is currently assigned to one or more teams. Unassign first.`);
|
|
980
1107
|
}
|
|
981
|
-
|
|
982
|
-
throw new Error(err.message || `Delete failed: ${response.status}`);
|
|
1108
|
+
throw await readApiError(response, "Delete failed");
|
|
983
1109
|
}
|
|
984
1110
|
}
|
|
985
1111
|
/** POST /v1/postures/:id/assign — assign to a team. */
|
|
@@ -997,15 +1123,14 @@ export async function assignPosture(postureId, teamId, pinRevisionNo) {
|
|
|
997
1123
|
}));
|
|
998
1124
|
if (!response.ok) {
|
|
999
1125
|
if (response.status === 401)
|
|
1000
|
-
throw new
|
|
1126
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1001
1127
|
if (response.status === 403)
|
|
1002
|
-
throw new
|
|
1128
|
+
throw new MnemomApiError(403, `Forbidden: org owner/admin required on team's org.`);
|
|
1003
1129
|
if (response.status === 404) {
|
|
1004
|
-
const errBody =
|
|
1005
|
-
throw new
|
|
1130
|
+
const errBody = await parseApiErrorBody(response);
|
|
1131
|
+
throw new MnemomApiError(404, errBody.message || `Posture, team, or pinned revision not found.`, { code: errBody.code, details: errBody.details, specDeviation: errBody.spec_deviation });
|
|
1006
1132
|
}
|
|
1007
|
-
|
|
1008
|
-
throw new Error(err.message || `Assign failed: ${response.status}`);
|
|
1133
|
+
throw await readApiError(response, "Assign failed");
|
|
1009
1134
|
}
|
|
1010
1135
|
return (await response.json());
|
|
1011
1136
|
}
|
|
@@ -1022,14 +1147,13 @@ export async function unassignPosture(postureId, teamId) {
|
|
|
1022
1147
|
}));
|
|
1023
1148
|
if (!response.ok) {
|
|
1024
1149
|
if (response.status === 401)
|
|
1025
|
-
throw new
|
|
1150
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1026
1151
|
if (response.status === 403)
|
|
1027
|
-
throw new
|
|
1152
|
+
throw new MnemomApiError(403, `Forbidden: org owner/admin required.`);
|
|
1028
1153
|
if (response.status === 404) {
|
|
1029
|
-
throw new
|
|
1154
|
+
throw new MnemomApiError(404, `No matching assignment to remove (posture '${postureId}' is not assigned to team '${teamId}').`);
|
|
1030
1155
|
}
|
|
1031
|
-
|
|
1032
|
-
throw new Error(err.message || `Unassign failed: ${response.status}`);
|
|
1156
|
+
throw await readApiError(response, "Unassign failed");
|
|
1033
1157
|
}
|
|
1034
1158
|
}
|
|
1035
1159
|
/** POST /v1/postures/:id/preview-compose — preview effective for a team. */
|
|
@@ -1046,13 +1170,16 @@ export async function previewComposePosture(postureId, teamId) {
|
|
|
1046
1170
|
}));
|
|
1047
1171
|
if (!response.ok) {
|
|
1048
1172
|
if (response.status === 401)
|
|
1049
|
-
throw new
|
|
1173
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1050
1174
|
if (response.status === 404) {
|
|
1051
|
-
const errBody =
|
|
1052
|
-
throw new
|
|
1175
|
+
const errBody = await parseApiErrorBody(response);
|
|
1176
|
+
throw new MnemomApiError(404, errBody.message || `Posture or team not found.`, {
|
|
1177
|
+
code: errBody.code,
|
|
1178
|
+
details: errBody.details,
|
|
1179
|
+
specDeviation: errBody.spec_deviation,
|
|
1180
|
+
});
|
|
1053
1181
|
}
|
|
1054
|
-
|
|
1055
|
-
throw new Error(err.message || `Preview-compose failed: ${response.status}`);
|
|
1182
|
+
throw await readApiError(response, "Preview-compose failed");
|
|
1056
1183
|
}
|
|
1057
1184
|
return (await response.json());
|
|
1058
1185
|
}
|
|
@@ -1076,13 +1203,12 @@ export async function listSidebandAdvisoriesForAgent(agentId, opts = {}) {
|
|
|
1076
1203
|
}));
|
|
1077
1204
|
if (!response.ok) {
|
|
1078
1205
|
if (response.status === 401)
|
|
1079
|
-
throw new
|
|
1206
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1080
1207
|
if (response.status === 403)
|
|
1081
|
-
throw new
|
|
1208
|
+
throw new MnemomApiError(403, "Permission denied (need org membership).");
|
|
1082
1209
|
if (response.status === 404)
|
|
1083
|
-
throw new
|
|
1084
|
-
|
|
1085
|
-
throw new Error(err.message || `List advisories failed: ${response.status}`);
|
|
1210
|
+
throw new MnemomApiError(404, `Agent '${agentId}' not found.`);
|
|
1211
|
+
throw await readApiError(response, "List advisories failed");
|
|
1086
1212
|
}
|
|
1087
1213
|
return (await response.json());
|
|
1088
1214
|
}
|
|
@@ -1106,13 +1232,12 @@ export async function listSidebandAdvisoriesForTeam(teamId, opts = {}) {
|
|
|
1106
1232
|
}));
|
|
1107
1233
|
if (!response.ok) {
|
|
1108
1234
|
if (response.status === 401)
|
|
1109
|
-
throw new
|
|
1235
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1110
1236
|
if (response.status === 403)
|
|
1111
|
-
throw new
|
|
1237
|
+
throw new MnemomApiError(403, "Permission denied (need org membership).");
|
|
1112
1238
|
if (response.status === 404)
|
|
1113
|
-
throw new
|
|
1114
|
-
|
|
1115
|
-
throw new Error(err.message || `List advisories failed: ${response.status}`);
|
|
1239
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
1240
|
+
throw await readApiError(response, "List advisories failed");
|
|
1116
1241
|
}
|
|
1117
1242
|
return (await response.json());
|
|
1118
1243
|
}
|
|
@@ -1131,13 +1256,12 @@ export async function getTeamSidebandCoverage(teamId) {
|
|
|
1131
1256
|
}));
|
|
1132
1257
|
if (!response.ok) {
|
|
1133
1258
|
if (response.status === 401)
|
|
1134
|
-
throw new
|
|
1259
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1135
1260
|
if (response.status === 403)
|
|
1136
|
-
throw new
|
|
1261
|
+
throw new MnemomApiError(403, "Permission denied (need org membership).");
|
|
1137
1262
|
if (response.status === 404)
|
|
1138
|
-
throw new
|
|
1139
|
-
|
|
1140
|
-
throw new Error(err.message || `Coverage fetch failed: ${response.status}`);
|
|
1263
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
1264
|
+
throw await readApiError(response, "Coverage fetch failed");
|
|
1141
1265
|
}
|
|
1142
1266
|
return (await response.json());
|
|
1143
1267
|
}
|
|
@@ -1154,11 +1278,10 @@ export async function getSafeHouseHarnessState() {
|
|
|
1154
1278
|
}));
|
|
1155
1279
|
if (!response.ok) {
|
|
1156
1280
|
if (response.status === 401)
|
|
1157
|
-
throw new
|
|
1281
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login`.");
|
|
1158
1282
|
if (response.status === 403)
|
|
1159
|
-
throw new
|
|
1160
|
-
|
|
1161
|
-
throw new Error(err.message || `Harness state fetch failed: ${response.status}`);
|
|
1283
|
+
throw new MnemomApiError(403, "Permission denied — `mnemom validate safe-house` is mnemom-staff only.");
|
|
1284
|
+
throw await readApiError(response, "Harness state fetch failed");
|
|
1162
1285
|
}
|
|
1163
1286
|
return (await response.json());
|
|
1164
1287
|
}
|
|
@@ -1195,13 +1318,12 @@ async function gFetch(path, init = {}, notFoundLabel) {
|
|
|
1195
1318
|
}));
|
|
1196
1319
|
if (!response.ok) {
|
|
1197
1320
|
if (response.status === 401)
|
|
1198
|
-
throw new
|
|
1321
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1199
1322
|
if (response.status === 403)
|
|
1200
|
-
throw new
|
|
1323
|
+
throw new MnemomApiError(403, "Permission denied (need org admin / membership).");
|
|
1201
1324
|
if (response.status === 404 && notFoundLabel)
|
|
1202
|
-
throw new
|
|
1203
|
-
|
|
1204
|
-
throw new Error(err.message || `Request failed: ${response.status}`);
|
|
1325
|
+
throw new MnemomApiError(404, `${notFoundLabel} not found.`);
|
|
1326
|
+
throw await readApiError(response, "Request failed");
|
|
1205
1327
|
}
|
|
1206
1328
|
return (await response.json());
|
|
1207
1329
|
}
|
|
@@ -1286,10 +1408,9 @@ export async function listApiKeys() {
|
|
|
1286
1408
|
}));
|
|
1287
1409
|
if (!response.ok) {
|
|
1288
1410
|
if (response.status === 401) {
|
|
1289
|
-
throw new
|
|
1411
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1290
1412
|
}
|
|
1291
|
-
|
|
1292
|
-
throw new Error(err.message || `Failed to list api keys: ${response.status}`);
|
|
1413
|
+
throw await readApiError(response, "Failed to list api keys");
|
|
1293
1414
|
}
|
|
1294
1415
|
const data = (await response.json());
|
|
1295
1416
|
return data.keys ?? [];
|
|
@@ -1310,16 +1431,17 @@ export async function createApiKey(name, scopes) {
|
|
|
1310
1431
|
}));
|
|
1311
1432
|
if (!response.ok) {
|
|
1312
1433
|
if (response.status === 401) {
|
|
1313
|
-
throw new
|
|
1434
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1314
1435
|
}
|
|
1315
|
-
const
|
|
1436
|
+
const parsed = await parseApiErrorBody(response);
|
|
1437
|
+
const detail = parsed.message || `HTTP ${response.status}`;
|
|
1316
1438
|
if (response.status === 403) {
|
|
1317
|
-
throw new
|
|
1439
|
+
throw new MnemomApiError(403, `Mint-time ceiling rejected scope: ${detail}`);
|
|
1318
1440
|
}
|
|
1319
1441
|
if (response.status === 400) {
|
|
1320
|
-
throw new
|
|
1442
|
+
throw new MnemomApiError(400, `Invalid scope(s): ${detail}`);
|
|
1321
1443
|
}
|
|
1322
|
-
throw new
|
|
1444
|
+
throw new MnemomApiError(response.status, `Failed to create api key: ${response.status} ${detail}`);
|
|
1323
1445
|
}
|
|
1324
1446
|
return (await response.json());
|
|
1325
1447
|
}
|
|
@@ -1337,12 +1459,12 @@ export async function rotateApiKey(keyId) {
|
|
|
1337
1459
|
}));
|
|
1338
1460
|
if (!response.ok) {
|
|
1339
1461
|
if (response.status === 401) {
|
|
1340
|
-
throw new
|
|
1462
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1341
1463
|
}
|
|
1342
1464
|
if (response.status === 404)
|
|
1343
|
-
throw new
|
|
1344
|
-
const
|
|
1345
|
-
throw new
|
|
1465
|
+
throw new MnemomApiError(404, `Key not found: ${keyId}`);
|
|
1466
|
+
const parsed = await parseApiErrorBody(response);
|
|
1467
|
+
throw new MnemomApiError(response.status, `Failed to rotate api key: ${response.status} ${parsed.message ?? ""}`.trim());
|
|
1346
1468
|
}
|
|
1347
1469
|
return (await response.json());
|
|
1348
1470
|
}
|
|
@@ -1358,14 +1480,48 @@ export async function revokeApiKey(keyId) {
|
|
|
1358
1480
|
}));
|
|
1359
1481
|
if (!response.ok) {
|
|
1360
1482
|
if (response.status === 401) {
|
|
1361
|
-
throw new
|
|
1483
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1362
1484
|
}
|
|
1363
1485
|
if (response.status === 404)
|
|
1364
|
-
throw new
|
|
1365
|
-
const
|
|
1366
|
-
throw new
|
|
1486
|
+
throw new MnemomApiError(404, `Key not found: ${keyId}`);
|
|
1487
|
+
const parsed = await parseApiErrorBody(response);
|
|
1488
|
+
throw new MnemomApiError(response.status, `Failed to revoke api key: ${response.status} ${parsed.message ?? ""}`.trim());
|
|
1367
1489
|
}
|
|
1368
1490
|
}
|
|
1491
|
+
/**
|
|
1492
|
+
* POST /v1/license/validate — validate a license JWT for an instance.
|
|
1493
|
+
*
|
|
1494
|
+
* UNAUTHENTICATED by design (the license JWT IS the credential; this is a
|
|
1495
|
+
* pre-login path) — sends NO auth header. Source-verified at canonical ref
|
|
1496
|
+
* c18491e6 (handleLicenseValidate): the request body keys are
|
|
1497
|
+
* `{ license, instance_id, instance_metadata }` (NOT `jwt`) — sending `jwt`
|
|
1498
|
+
* would 400 "license is required".
|
|
1499
|
+
*
|
|
1500
|
+
* Returns the parsed validation result on 2xx (including the grace-period
|
|
1501
|
+
* `valid:false` 200 body); throws MnemomApiError on non-2xx so the command
|
|
1502
|
+
* renders `.message`/`.status`. This fixes the live H1 "[object Object]" bug:
|
|
1503
|
+
* the old command-layer `err.error || "unknown"` string-coerced the nested
|
|
1504
|
+
* `{code,message}` object that the enforce hook puts on the wire.
|
|
1505
|
+
*
|
|
1506
|
+
* `license deactivate` may reuse this (best-effort, fire-and-forget) by
|
|
1507
|
+
* catching the throw at the command layer.
|
|
1508
|
+
*/
|
|
1509
|
+
export async function validateLicense(jwt, instanceId, instanceMetadata = {}) {
|
|
1510
|
+
const url = validateUrl(`${API_BASE}/v1/license/validate`);
|
|
1511
|
+
const response = await fetch(url, {
|
|
1512
|
+
method: "POST",
|
|
1513
|
+
headers: { "Content-Type": "application/json" },
|
|
1514
|
+
body: JSON.stringify({
|
|
1515
|
+
license: sanitizeForHttp(jwt),
|
|
1516
|
+
instance_id: instanceId,
|
|
1517
|
+
instance_metadata: instanceMetadata,
|
|
1518
|
+
}),
|
|
1519
|
+
});
|
|
1520
|
+
if (!response.ok) {
|
|
1521
|
+
throw await readApiError(response, "License validation failed");
|
|
1522
|
+
}
|
|
1523
|
+
return (await response.json());
|
|
1524
|
+
}
|
|
1369
1525
|
export async function reportRecipeFnFp(recipeId, input) {
|
|
1370
1526
|
return postApi(`/v1/recipes/${encodeURIComponent(recipeId)}/report`, input);
|
|
1371
1527
|
}
|