@mnemom/mnemom 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents.d.ts +0 -14
- package/dist/commands/agents.js +35 -295
- package/dist/commands/auth.js +2 -7
- package/dist/commands/card.d.ts +33 -9
- package/dist/commands/card.js +698 -258
- package/dist/commands/integrity.js +13 -12
- package/dist/commands/license.js +14 -29
- package/dist/commands/logs.js +8 -8
- package/dist/commands/policy.d.ts +10 -23
- package/dist/commands/policy.js +20 -533
- package/dist/commands/protection.d.ts +22 -0
- package/dist/commands/protection.js +542 -0
- package/dist/commands/status.js +48 -54
- package/dist/index.js +120 -162
- package/dist/lib/api.d.ts +131 -9
- package/dist/lib/api.js +298 -12
- package/dist/lib/auth.d.ts +46 -21
- package/dist/lib/auth.js +148 -52
- package/dist/lib/config.d.ts +11 -98
- package/dist/lib/config.js +12 -220
- package/dist/lib/model-cache.js +5 -6
- package/dist/smoltbot-shim.js +1 -1
- package/package.json +2 -2
- package/dist/commands/init.d.ts +0 -7
- package/dist/commands/init.js +0 -763
- package/dist/commands/migrate-config.d.ts +0 -2
- package/dist/commands/migrate-config.js +0 -72
- package/dist/commands/register.d.ts +0 -6
- package/dist/commands/register.js +0 -362
package/dist/lib/api.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export declare const API_BASE: string;
|
|
2
|
+
/**
|
|
3
|
+
* Generate a fresh Idempotency-Key for a mutation request.
|
|
4
|
+
*
|
|
5
|
+
* The server's beginIdempotentMutation contract (mnemom-api/src/idempotency.ts)
|
|
6
|
+
* requires this header on every PUT/POST/DELETE to a mutation endpoint;
|
|
7
|
+
* without it the server short-circuits with a 400 "Idempotency-Key header is
|
|
8
|
+
* required". Callers may pass an explicit key (for retries that must hit the
|
|
9
|
+
* cached reservation) — when omitted, we mint a UUIDv4.
|
|
10
|
+
*/
|
|
11
|
+
export declare function newIdempotencyKey(): string;
|
|
2
12
|
export interface Agent {
|
|
3
13
|
id: string;
|
|
4
14
|
gateway: string;
|
|
@@ -7,13 +17,33 @@ export interface Agent {
|
|
|
7
17
|
email?: string;
|
|
8
18
|
created_at: string;
|
|
9
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Per docs.mnemom.ai/api-reference/openapi.json#components/schemas/IntegrityScore:
|
|
22
|
+
*
|
|
23
|
+
* { agent_id, total_traces, verified_traces, violation_count, integrity_score }
|
|
24
|
+
*
|
|
25
|
+
* `integrity_score` is a value in [0, 1].
|
|
26
|
+
*
|
|
27
|
+
* Pre-this-fix the CLI's interface declared `score` / `verified` /
|
|
28
|
+
* `violations` / `last_updated` — none of which exist on the wire. The
|
|
29
|
+
* `mnemom integrity` command rendered every field as `undefined` (and the
|
|
30
|
+
* score as `NaN%`) against any agent. Field names now match the docs
|
|
31
|
+
* verbatim.
|
|
32
|
+
*
|
|
33
|
+
* NOTE — API shape divergence (flagged for a follow-up mnemom-api PR):
|
|
34
|
+
* the RPC path in handleGetIntegrity returns an *array* of one object
|
|
35
|
+
* (Supabase wraps TABLE-returning RPCs as arrays) without `agent_id`;
|
|
36
|
+
* the manual fallback returns the *object* with `agent_id`. The docs
|
|
37
|
+
* canonical is the object shape. We accept both shapes defensively in
|
|
38
|
+
* getIntegrity below so this CLI works against today's prod API and
|
|
39
|
+
* keeps working when the API normalizes to a single shape.
|
|
40
|
+
*/
|
|
10
41
|
export interface IntegrityScore {
|
|
11
|
-
agent_id
|
|
12
|
-
score: number;
|
|
42
|
+
agent_id?: string;
|
|
13
43
|
total_traces: number;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
44
|
+
verified_traces: number;
|
|
45
|
+
violation_count: number;
|
|
46
|
+
integrity_score: number;
|
|
17
47
|
}
|
|
18
48
|
export interface Trace {
|
|
19
49
|
id: string;
|
|
@@ -29,7 +59,9 @@ export interface ApiError {
|
|
|
29
59
|
error: string;
|
|
30
60
|
message: string;
|
|
31
61
|
}
|
|
32
|
-
export declare function postApi<T>(endpoint: string, body: unknown
|
|
62
|
+
export declare function postApi<T>(endpoint: string, body: unknown, opts?: {
|
|
63
|
+
idempotencyKey?: string;
|
|
64
|
+
}): Promise<T>;
|
|
33
65
|
export declare function verifyBinding(agentId: string, keyHash: string): Promise<{
|
|
34
66
|
bound: boolean;
|
|
35
67
|
key_prefix: string | null;
|
|
@@ -53,7 +85,30 @@ export declare function listAgents(): Promise<AgentListItem[]>;
|
|
|
53
85
|
* Note: capped at 100 agents by listAgents().
|
|
54
86
|
*/
|
|
55
87
|
export declare function getAgentByName(name: string): Promise<AgentListItem | null>;
|
|
88
|
+
/**
|
|
89
|
+
* Fetch the integrity score for an agent.
|
|
90
|
+
*
|
|
91
|
+
* `/v1/integrity/:id` is the public-when-agent-is-public, owner-otherwise
|
|
92
|
+
* data route. The CLI must send the owner's auth header so private claimed
|
|
93
|
+
* agents (the common case for customers and internal users) return 200
|
|
94
|
+
* instead of 401. fetchWithAuthRetry inherits the 401-retry-with-refresh
|
|
95
|
+
* path from PR #200 so a stale local expiresAt heals transparently.
|
|
96
|
+
*/
|
|
56
97
|
export declare function getIntegrity(id: string): Promise<IntegrityScore>;
|
|
98
|
+
/**
|
|
99
|
+
* Fetch recent traces for an agent.
|
|
100
|
+
*
|
|
101
|
+
* The API returns an envelope `{ traces, limit, offset }` (see
|
|
102
|
+
* mnemom-api/src/index.ts:handleGetTraces). Pre-this-fix the CLI typed
|
|
103
|
+
* the response as `Trace[]` and downstream code crashed with "traces is
|
|
104
|
+
* not iterable" because the envelope is not iterable. We unwrap the
|
|
105
|
+
* envelope here and return the bare array so callers (logs.ts) can keep
|
|
106
|
+
* the simpler shape.
|
|
107
|
+
*
|
|
108
|
+
* Sends auth headers — same reasoning as getIntegrity. The pre-PR-A
|
|
109
|
+
* `{ traces: [], private: true }` leaky envelope branch is gone, so we
|
|
110
|
+
* don't need a special case for it.
|
|
111
|
+
*/
|
|
57
112
|
export declare function getTraces(id: string, limit?: number): Promise<Trace[]>;
|
|
58
113
|
export interface AlignmentCard {
|
|
59
114
|
card_id?: string;
|
|
@@ -93,11 +148,15 @@ export interface CardResponse {
|
|
|
93
148
|
updated_at: string;
|
|
94
149
|
}
|
|
95
150
|
export declare function getCard(agentId: string): Promise<CardResponse | null>;
|
|
96
|
-
export declare function updateCard(agentId: string, cardJson: AlignmentCard
|
|
151
|
+
export declare function updateCard(agentId: string, cardJson: AlignmentCard, opts?: {
|
|
152
|
+
idempotencyKey?: string;
|
|
153
|
+
}): Promise<{
|
|
97
154
|
updated: boolean;
|
|
98
155
|
card_id: string;
|
|
99
156
|
}>;
|
|
100
|
-
export declare function reverifyAgent(agentId: string
|
|
157
|
+
export declare function reverifyAgent(agentId: string, opts?: {
|
|
158
|
+
idempotencyKey?: string;
|
|
159
|
+
}): Promise<{
|
|
101
160
|
reverified: number;
|
|
102
161
|
}>;
|
|
103
162
|
export interface PolicyResponse {
|
|
@@ -114,7 +173,9 @@ export interface PolicyListResponse {
|
|
|
114
173
|
policy: PolicyResponse | null;
|
|
115
174
|
}
|
|
116
175
|
export declare function getPolicy(agentId: string): Promise<PolicyListResponse | null>;
|
|
117
|
-
export declare function publishPolicy(agentId: string, policyJson: Record<string, unknown
|
|
176
|
+
export declare function publishPolicy(agentId: string, policyJson: Record<string, unknown>, opts?: {
|
|
177
|
+
idempotencyKey?: string;
|
|
178
|
+
}): Promise<{
|
|
118
179
|
id: string;
|
|
119
180
|
version: number;
|
|
120
181
|
created: boolean;
|
|
@@ -131,3 +192,64 @@ export declare function testPolicyHistorical(agentId: string, policyJson: Record
|
|
|
131
192
|
skipped: number;
|
|
132
193
|
};
|
|
133
194
|
}>;
|
|
195
|
+
/**
|
|
196
|
+
* Fetch the canonical alignment card as YAML (or JSON fallback).
|
|
197
|
+
* Returns the raw response body as a string.
|
|
198
|
+
*/
|
|
199
|
+
export declare function getAlignmentCard(agentId: string, format?: "yaml" | "json"): Promise<{
|
|
200
|
+
body: string;
|
|
201
|
+
contentType: string;
|
|
202
|
+
}>;
|
|
203
|
+
/** Hard cap enforced by the API on inbound alignment-card bodies (413 otherwise). */
|
|
204
|
+
export declare const ALIGNMENT_CARD_MAX_BYTES: number;
|
|
205
|
+
/**
|
|
206
|
+
* Shape of a successful PUT response — the canonical card the composer
|
|
207
|
+
* just wrote, serialized as JSON via mnemom-api/src/composition/response.ts:
|
|
208
|
+
* respondCard with `_composition` stripped. We type the fields we display;
|
|
209
|
+
* the rest of the canonical card is allowed (and ignored at this layer).
|
|
210
|
+
*
|
|
211
|
+
* Pre-this-fix the typed return was `{ card_id, composed: boolean }` —
|
|
212
|
+
* a fictional wrapper. The API never returned a `composed` field; the CLI
|
|
213
|
+
* was rendering "Canonical card recomposed" gated on a value that, on the
|
|
214
|
+
* real wire, is always undefined. Drop the lie; reflect the actual shape.
|
|
215
|
+
*/
|
|
216
|
+
export interface PublishedCardResponse {
|
|
217
|
+
card_id?: string;
|
|
218
|
+
agent_id?: string;
|
|
219
|
+
card_version?: string;
|
|
220
|
+
issued_at?: string;
|
|
221
|
+
[key: string]: unknown;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Publish (create or update) an alignment card.
|
|
225
|
+
* Accepts YAML or JSON body; set contentType accordingly.
|
|
226
|
+
*/
|
|
227
|
+
export declare function putAlignmentCard(agentId: string, body: string, contentType?: "text/yaml" | "application/json", opts?: {
|
|
228
|
+
idempotencyKey?: string;
|
|
229
|
+
}): Promise<PublishedCardResponse>;
|
|
230
|
+
/**
|
|
231
|
+
* Fetch the canonical protection card as YAML (or JSON fallback).
|
|
232
|
+
*/
|
|
233
|
+
export declare function getProtectionCard(agentId: string, format?: "yaml" | "json"): Promise<{
|
|
234
|
+
body: string;
|
|
235
|
+
contentType: string;
|
|
236
|
+
}>;
|
|
237
|
+
/** Hard cap enforced by the API on inbound protection-card bodies (413 otherwise). */
|
|
238
|
+
export declare const PROTECTION_CARD_MAX_BYTES: number;
|
|
239
|
+
/**
|
|
240
|
+
* Publish (create or update) a protection card.
|
|
241
|
+
*/
|
|
242
|
+
export declare function putProtectionCard(agentId: string, body: string, contentType?: "text/yaml" | "application/json", opts?: {
|
|
243
|
+
idempotencyKey?: string;
|
|
244
|
+
}): Promise<PublishedCardResponse>;
|
|
245
|
+
/**
|
|
246
|
+
* Resolve an agent name or ID to a server agent ID.
|
|
247
|
+
*
|
|
248
|
+
* Resolution order:
|
|
249
|
+
* 1. Explicit agentName parameter (--agent flag)
|
|
250
|
+
* 2. MNEMOM_AGENT environment variable
|
|
251
|
+
*
|
|
252
|
+
* If the value looks like an agent ID (smolt-* or mnm-*), uses it directly.
|
|
253
|
+
* Otherwise resolves the name from the authenticated user's agent list.
|
|
254
|
+
*/
|
|
255
|
+
export declare function resolveAgentId(agentName?: string): Promise<string>;
|
package/dist/lib/api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { getApiUrl } from "./config.js";
|
|
2
|
-
import { resolveAuth } from "./auth.js";
|
|
3
|
+
import { forceRefreshAccessToken, resolveAuth } from "./auth.js";
|
|
3
4
|
export const API_BASE = getApiUrl();
|
|
4
5
|
/** Sanitize file-sourced data before use in outbound HTTP requests. */
|
|
5
6
|
function sanitizeForHttp(data) {
|
|
@@ -13,6 +14,18 @@ function validateUrl(url) {
|
|
|
13
14
|
}
|
|
14
15
|
return parsed.href;
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate a fresh Idempotency-Key for a mutation request.
|
|
19
|
+
*
|
|
20
|
+
* The server's beginIdempotentMutation contract (mnemom-api/src/idempotency.ts)
|
|
21
|
+
* requires this header on every PUT/POST/DELETE to a mutation endpoint;
|
|
22
|
+
* without it the server short-circuits with a 400 "Idempotency-Key header is
|
|
23
|
+
* required". Callers may pass an explicit key (for retries that must hit the
|
|
24
|
+
* cached reservation) — when omitted, we mint a UUIDv4.
|
|
25
|
+
*/
|
|
26
|
+
export function newIdempotencyKey() {
|
|
27
|
+
return randomUUID();
|
|
28
|
+
}
|
|
16
29
|
async function fetchApi(endpoint) {
|
|
17
30
|
const url = validateUrl(`${API_BASE}${endpoint}`);
|
|
18
31
|
const response = await fetch(url);
|
|
@@ -25,11 +38,12 @@ async function fetchApi(endpoint) {
|
|
|
25
38
|
}
|
|
26
39
|
return response.json();
|
|
27
40
|
}
|
|
28
|
-
export async function postApi(endpoint, body) {
|
|
41
|
+
export async function postApi(endpoint, body, opts = {}) {
|
|
29
42
|
const url = validateUrl(`${API_BASE}${endpoint}`);
|
|
30
43
|
const cred = await resolveAuth();
|
|
31
44
|
const headers = {
|
|
32
45
|
"Content-Type": "application/json",
|
|
46
|
+
"Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
|
|
33
47
|
};
|
|
34
48
|
if (cred.type === "jwt") {
|
|
35
49
|
headers["Authorization"] = `Bearer ${cred.token}`;
|
|
@@ -74,15 +88,44 @@ async function authHeaders() {
|
|
|
74
88
|
return {};
|
|
75
89
|
}
|
|
76
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Issue an authenticated fetch, and on a 401 response transparently force a
|
|
93
|
+
* token refresh and retry once.
|
|
94
|
+
*
|
|
95
|
+
* We hit this in the wild when the locally-stored expiresAt diverges from
|
|
96
|
+
* the JWT's actual exp claim (e.g. Supabase has been observed reporting
|
|
97
|
+
* expires_in values longer than the JWT's exp). Without this retry,
|
|
98
|
+
* `whoami` cheerfully reports a "valid" token while every authenticated
|
|
99
|
+
* call gets 401 — and the user has no path forward besides
|
|
100
|
+
* `mnemom logout && mnemom login`. The retry heals stale auth files
|
|
101
|
+
* transparently for users who haven't re-logged-in since the
|
|
102
|
+
* computeExpiresAt fix landed.
|
|
103
|
+
*
|
|
104
|
+
* `buildInit` is invoked fresh for each attempt so the retry picks up the
|
|
105
|
+
* new Authorization header from the refreshed token. We do NOT mint a new
|
|
106
|
+
* Idempotency-Key on the retry — the same key keys back into the same
|
|
107
|
+
* server-side reservation by design.
|
|
108
|
+
*/
|
|
109
|
+
async function fetchWithAuthRetry(url, buildInit) {
|
|
110
|
+
const first = await fetch(url, await buildInit());
|
|
111
|
+
if (first.status !== 401)
|
|
112
|
+
return first;
|
|
113
|
+
const refreshed = await forceRefreshAccessToken();
|
|
114
|
+
if (!refreshed)
|
|
115
|
+
return first;
|
|
116
|
+
return fetch(url, await buildInit());
|
|
117
|
+
}
|
|
77
118
|
export async function getAgent(id) {
|
|
78
119
|
return fetchApi(`/v1/agents/${id}`);
|
|
79
120
|
}
|
|
80
121
|
export async function listAgents() {
|
|
81
122
|
const url = validateUrl(`${API_BASE}/v1/agents?limit=100`);
|
|
82
|
-
const response = await
|
|
123
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
124
|
+
headers: await authHeaders(),
|
|
125
|
+
}));
|
|
83
126
|
if (!response.ok) {
|
|
84
127
|
if (response.status === 401) {
|
|
85
|
-
throw new Error("Not authenticated. Run `
|
|
128
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
86
129
|
}
|
|
87
130
|
const err = await response.json().catch(() => ({ error: "unknown" }));
|
|
88
131
|
throw new Error(err.message || `Failed to list agents: ${response.status}`);
|
|
@@ -112,11 +155,79 @@ export async function getAgentByName(name) {
|
|
|
112
155
|
}
|
|
113
156
|
return null;
|
|
114
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Fetch the integrity score for an agent.
|
|
160
|
+
*
|
|
161
|
+
* `/v1/integrity/:id` is the public-when-agent-is-public, owner-otherwise
|
|
162
|
+
* data route. The CLI must send the owner's auth header so private claimed
|
|
163
|
+
* agents (the common case for customers and internal users) return 200
|
|
164
|
+
* instead of 401. fetchWithAuthRetry inherits the 401-retry-with-refresh
|
|
165
|
+
* path from PR #200 so a stale local expiresAt heals transparently.
|
|
166
|
+
*/
|
|
115
167
|
export async function getIntegrity(id) {
|
|
116
|
-
|
|
168
|
+
const url = validateUrl(`${API_BASE}/v1/integrity/${id}`);
|
|
169
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
170
|
+
headers: await authHeaders(),
|
|
171
|
+
}));
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
const error = (await response.json().catch(() => ({
|
|
174
|
+
error: "unknown",
|
|
175
|
+
message: response.statusText,
|
|
176
|
+
})));
|
|
177
|
+
throw new Error(error.message || `API request failed: ${response.status}`);
|
|
178
|
+
}
|
|
179
|
+
// Accept both the canonical docs shape (object) and the RPC-wrapped shape
|
|
180
|
+
// (array of one row). The latter is what prod actually returns today on
|
|
181
|
+
// the RPC path; flagged for a follow-up API normalization.
|
|
182
|
+
const body = (await response.json());
|
|
183
|
+
const row = Array.isArray(body) ? (body[0] ?? emptyIntegrityRow(id)) : body;
|
|
184
|
+
// Fill in agent_id when the API didn't (RPC path) so the field is always
|
|
185
|
+
// populated for consumers regardless of which API path served the request.
|
|
186
|
+
if (!row.agent_id)
|
|
187
|
+
row.agent_id = id;
|
|
188
|
+
return row;
|
|
189
|
+
}
|
|
190
|
+
function emptyIntegrityRow(agentId) {
|
|
191
|
+
return {
|
|
192
|
+
agent_id: agentId,
|
|
193
|
+
total_traces: 0,
|
|
194
|
+
verified_traces: 0,
|
|
195
|
+
violation_count: 0,
|
|
196
|
+
integrity_score: 1,
|
|
197
|
+
};
|
|
117
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Fetch recent traces for an agent.
|
|
201
|
+
*
|
|
202
|
+
* The API returns an envelope `{ traces, limit, offset }` (see
|
|
203
|
+
* mnemom-api/src/index.ts:handleGetTraces). Pre-this-fix the CLI typed
|
|
204
|
+
* the response as `Trace[]` and downstream code crashed with "traces is
|
|
205
|
+
* not iterable" because the envelope is not iterable. We unwrap the
|
|
206
|
+
* envelope here and return the bare array so callers (logs.ts) can keep
|
|
207
|
+
* the simpler shape.
|
|
208
|
+
*
|
|
209
|
+
* Sends auth headers — same reasoning as getIntegrity. The pre-PR-A
|
|
210
|
+
* `{ traces: [], private: true }` leaky envelope branch is gone, so we
|
|
211
|
+
* don't need a special case for it.
|
|
212
|
+
*/
|
|
118
213
|
export async function getTraces(id, limit = 10) {
|
|
119
|
-
|
|
214
|
+
const url = validateUrl(`${API_BASE}/v1/traces?agent_id=${id}&limit=${limit}`);
|
|
215
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
216
|
+
headers: await authHeaders(),
|
|
217
|
+
}));
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
const error = (await response.json().catch(() => ({
|
|
220
|
+
error: "unknown",
|
|
221
|
+
message: response.statusText,
|
|
222
|
+
})));
|
|
223
|
+
throw new Error(error.message || `API request failed: ${response.status}`);
|
|
224
|
+
}
|
|
225
|
+
const data = (await response.json());
|
|
226
|
+
// Accept both the envelope shape (current API) and a bare array (defensive
|
|
227
|
+
// — staging is mid-deploy when a CLI smoke runs against an older API).
|
|
228
|
+
if (Array.isArray(data))
|
|
229
|
+
return data;
|
|
230
|
+
return data.traces ?? [];
|
|
120
231
|
}
|
|
121
232
|
export async function getCard(agentId) {
|
|
122
233
|
try {
|
|
@@ -130,11 +241,15 @@ export async function getCard(agentId) {
|
|
|
130
241
|
throw error;
|
|
131
242
|
}
|
|
132
243
|
}
|
|
133
|
-
export async function updateCard(agentId, cardJson) {
|
|
244
|
+
export async function updateCard(agentId, cardJson, opts = {}) {
|
|
134
245
|
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/card`);
|
|
135
246
|
const response = await fetch(url, {
|
|
136
247
|
method: "PATCH",
|
|
137
|
-
headers: {
|
|
248
|
+
headers: {
|
|
249
|
+
"Content-Type": "application/json",
|
|
250
|
+
"Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
|
|
251
|
+
...(await authHeaders()),
|
|
252
|
+
},
|
|
138
253
|
body: sanitizeForHttp(JSON.stringify({ card_json: cardJson })),
|
|
139
254
|
});
|
|
140
255
|
if (!response.ok) {
|
|
@@ -146,11 +261,15 @@ export async function updateCard(agentId, cardJson) {
|
|
|
146
261
|
}
|
|
147
262
|
return response.json();
|
|
148
263
|
}
|
|
149
|
-
export async function reverifyAgent(agentId) {
|
|
264
|
+
export async function reverifyAgent(agentId, opts = {}) {
|
|
150
265
|
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/reverify`);
|
|
151
266
|
const response = await fetch(url, {
|
|
152
267
|
method: "POST",
|
|
153
|
-
headers: {
|
|
268
|
+
headers: {
|
|
269
|
+
"Content-Type": "application/json",
|
|
270
|
+
"Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
|
|
271
|
+
...(await authHeaders()),
|
|
272
|
+
},
|
|
154
273
|
});
|
|
155
274
|
if (!response.ok) {
|
|
156
275
|
const error = (await response.json().catch(() => ({
|
|
@@ -173,11 +292,15 @@ export async function getPolicy(agentId) {
|
|
|
173
292
|
throw error;
|
|
174
293
|
}
|
|
175
294
|
}
|
|
176
|
-
export async function publishPolicy(agentId, policyJson) {
|
|
295
|
+
export async function publishPolicy(agentId, policyJson, opts = {}) {
|
|
177
296
|
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/policy`);
|
|
178
297
|
const response = await fetch(url, {
|
|
179
298
|
method: "PUT",
|
|
180
|
-
headers: {
|
|
299
|
+
headers: {
|
|
300
|
+
"Content-Type": "application/json",
|
|
301
|
+
"Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
|
|
302
|
+
...(await authHeaders()),
|
|
303
|
+
},
|
|
181
304
|
body: sanitizeForHttp(JSON.stringify({ policy_json: policyJson })),
|
|
182
305
|
});
|
|
183
306
|
if (!response.ok) {
|
|
@@ -205,3 +328,166 @@ export async function testPolicyHistorical(agentId, policyJson, limit = 50) {
|
|
|
205
328
|
}
|
|
206
329
|
return response.json();
|
|
207
330
|
}
|
|
331
|
+
// ============================================================================
|
|
332
|
+
// Unified Card API (UC-4+)
|
|
333
|
+
// ============================================================================
|
|
334
|
+
/**
|
|
335
|
+
* Fetch the canonical alignment card as YAML (or JSON fallback).
|
|
336
|
+
* Returns the raw response body as a string.
|
|
337
|
+
*/
|
|
338
|
+
export async function getAlignmentCard(agentId, format = "yaml") {
|
|
339
|
+
const accept = format === "yaml" ? "text/yaml" : "application/json";
|
|
340
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/alignment-card`);
|
|
341
|
+
const response = await fetch(url, {
|
|
342
|
+
headers: { Accept: accept, ...(await authHeaders()) },
|
|
343
|
+
});
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
if (response.status === 404) {
|
|
346
|
+
return { body: "", contentType: "" };
|
|
347
|
+
}
|
|
348
|
+
const error = (await response.json().catch(() => ({
|
|
349
|
+
error: "unknown",
|
|
350
|
+
message: response.statusText,
|
|
351
|
+
})));
|
|
352
|
+
throw new Error(error.message || `Failed to fetch alignment card: ${response.status}`);
|
|
353
|
+
}
|
|
354
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
355
|
+
const body = await response.text();
|
|
356
|
+
return { body, contentType: ct };
|
|
357
|
+
}
|
|
358
|
+
/** Hard cap enforced by the API on inbound alignment-card bodies (413 otherwise). */
|
|
359
|
+
export const ALIGNMENT_CARD_MAX_BYTES = 128 * 1024;
|
|
360
|
+
/**
|
|
361
|
+
* Publish (create or update) an alignment card.
|
|
362
|
+
* Accepts YAML or JSON body; set contentType accordingly.
|
|
363
|
+
*/
|
|
364
|
+
export async function putAlignmentCard(agentId, body, contentType = "text/yaml", opts = {}) {
|
|
365
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/alignment-card`);
|
|
366
|
+
// Lock in a single Idempotency-Key for this logical mutation. If the auth
|
|
367
|
+
// token is stale and the first attempt returns 401, fetchWithAuthRetry
|
|
368
|
+
// refreshes the token and retries — but the Idempotency-Key must be the
|
|
369
|
+
// same key on both attempts so the server's reservation table sees them as
|
|
370
|
+
// a single mutation, not two competing PUTs.
|
|
371
|
+
const idempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
|
|
372
|
+
const sanitizedBody = sanitizeForHttp(body);
|
|
373
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
374
|
+
method: "PUT",
|
|
375
|
+
headers: {
|
|
376
|
+
"Content-Type": contentType,
|
|
377
|
+
// The API content-negotiates the response body via Accept (see
|
|
378
|
+
// mnemom-api/src/composition/response.ts:respondYamlJson). We parse the
|
|
379
|
+
// response as JSON below, so we have to ask for JSON explicitly —
|
|
380
|
+
// Node's default Accept: */* would otherwise yield a YAML body and
|
|
381
|
+
// response.json() would crash with "Unexpected token 'a' is not valid
|
|
382
|
+
// JSON" against the canonical card we just wrote.
|
|
383
|
+
Accept: "application/json",
|
|
384
|
+
"Idempotency-Key": idempotencyKey,
|
|
385
|
+
...(await authHeaders()),
|
|
386
|
+
},
|
|
387
|
+
body: sanitizedBody,
|
|
388
|
+
}));
|
|
389
|
+
if (!response.ok) {
|
|
390
|
+
const error = (await response.json().catch(() => ({
|
|
391
|
+
error: "unknown",
|
|
392
|
+
message: response.statusText,
|
|
393
|
+
})));
|
|
394
|
+
throw new Error(error.message || `Failed to publish alignment card: ${response.status}`);
|
|
395
|
+
}
|
|
396
|
+
return response.json();
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Fetch the canonical protection card as YAML (or JSON fallback).
|
|
400
|
+
*/
|
|
401
|
+
export async function getProtectionCard(agentId, format = "yaml") {
|
|
402
|
+
const accept = format === "yaml" ? "text/yaml" : "application/json";
|
|
403
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/protection-card`);
|
|
404
|
+
const response = await fetch(url, {
|
|
405
|
+
headers: { Accept: accept, ...(await authHeaders()) },
|
|
406
|
+
});
|
|
407
|
+
if (!response.ok) {
|
|
408
|
+
if (response.status === 404) {
|
|
409
|
+
return { body: "", contentType: "" };
|
|
410
|
+
}
|
|
411
|
+
const error = (await response.json().catch(() => ({
|
|
412
|
+
error: "unknown",
|
|
413
|
+
message: response.statusText,
|
|
414
|
+
})));
|
|
415
|
+
throw new Error(error.message || `Failed to fetch protection card: ${response.status}`);
|
|
416
|
+
}
|
|
417
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
418
|
+
const body = await response.text();
|
|
419
|
+
return { body, contentType: ct };
|
|
420
|
+
}
|
|
421
|
+
/** Hard cap enforced by the API on inbound protection-card bodies (413 otherwise). */
|
|
422
|
+
export const PROTECTION_CARD_MAX_BYTES = 64 * 1024;
|
|
423
|
+
/**
|
|
424
|
+
* Publish (create or update) a protection card.
|
|
425
|
+
*/
|
|
426
|
+
export async function putProtectionCard(agentId, body, contentType = "text/yaml", opts = {}) {
|
|
427
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/protection-card`);
|
|
428
|
+
// See putAlignmentCard for why the Idempotency-Key is computed once outside
|
|
429
|
+
// the retry closure.
|
|
430
|
+
const idempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
|
|
431
|
+
const sanitizedBody = sanitizeForHttp(body);
|
|
432
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
433
|
+
method: "PUT",
|
|
434
|
+
headers: {
|
|
435
|
+
"Content-Type": contentType,
|
|
436
|
+
// See putAlignmentCard for the rationale — the API negotiates response
|
|
437
|
+
// body format via Accept and we parse the response as JSON below.
|
|
438
|
+
Accept: "application/json",
|
|
439
|
+
"Idempotency-Key": idempotencyKey,
|
|
440
|
+
...(await authHeaders()),
|
|
441
|
+
},
|
|
442
|
+
body: sanitizedBody,
|
|
443
|
+
}));
|
|
444
|
+
if (!response.ok) {
|
|
445
|
+
const error = (await response.json().catch(() => ({
|
|
446
|
+
error: "unknown",
|
|
447
|
+
message: response.statusText,
|
|
448
|
+
})));
|
|
449
|
+
throw new Error(error.message || `Failed to publish protection card: ${response.status}`);
|
|
450
|
+
}
|
|
451
|
+
return response.json();
|
|
452
|
+
}
|
|
453
|
+
// ============================================================================
|
|
454
|
+
// Agent Resolution (server-side, no local config)
|
|
455
|
+
// ============================================================================
|
|
456
|
+
/**
|
|
457
|
+
* Resolve an agent name or ID to a server agent ID.
|
|
458
|
+
*
|
|
459
|
+
* Resolution order:
|
|
460
|
+
* 1. Explicit agentName parameter (--agent flag)
|
|
461
|
+
* 2. MNEMOM_AGENT environment variable
|
|
462
|
+
*
|
|
463
|
+
* If the value looks like an agent ID (smolt-* or mnm-*), uses it directly.
|
|
464
|
+
* Otherwise resolves the name from the authenticated user's agent list.
|
|
465
|
+
*/
|
|
466
|
+
export async function resolveAgentId(agentName) {
|
|
467
|
+
const name = agentName ?? process.env.MNEMOM_AGENT;
|
|
468
|
+
if (!name) {
|
|
469
|
+
console.error("\nAgent required. Use --agent <name> or set MNEMOM_AGENT.\n");
|
|
470
|
+
console.error("List your agents with: mnemom agents\n");
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
// If it looks like an agent ID, use directly (no server call needed)
|
|
474
|
+
if (/^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/.test(name)) {
|
|
475
|
+
return name;
|
|
476
|
+
}
|
|
477
|
+
// Resolve name from server (requires auth)
|
|
478
|
+
try {
|
|
479
|
+
const agent = await getAgentByName(name);
|
|
480
|
+
if (agent)
|
|
481
|
+
return agent.id;
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
485
|
+
if (msg.includes("Not authenticated")) {
|
|
486
|
+
console.error(`\nNot authenticated. Run \`mnemom login\` first.\n`);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
console.error(`\nAgent not found: ${name}`);
|
|
491
|
+
console.error("List your agents with: mnemom agents\n");
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Auth credential management.
|
|
3
|
+
*
|
|
4
|
+
* Stores auth tokens in ~/.mnemom/auth.json (UC-9: no more config.json).
|
|
5
|
+
* License JWTs are stored alongside auth tokens.
|
|
6
|
+
*/
|
|
7
|
+
export interface AuthTokens {
|
|
8
|
+
accessToken: string;
|
|
9
|
+
refreshToken: string;
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
userId: string;
|
|
12
|
+
email: string;
|
|
13
|
+
}
|
|
14
|
+
export interface AuthStore {
|
|
15
|
+
auth?: AuthTokens;
|
|
16
|
+
licenseJwt?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function saveAuthTokens(tokens: AuthTokens): void;
|
|
19
|
+
export declare function clearAuthTokens(): void;
|
|
20
|
+
export declare function getAuthInfo(): AuthTokens | null;
|
|
21
|
+
export declare function saveLicenseJwt(jwt: string): void;
|
|
22
|
+
export declare function clearLicenseJwt(): void;
|
|
23
|
+
export declare function getLicenseJwt(): string | null;
|
|
2
24
|
export type AuthCredential = {
|
|
3
25
|
type: "jwt";
|
|
4
26
|
token: string;
|
|
@@ -8,32 +30,44 @@ export type AuthCredential = {
|
|
|
8
30
|
} | {
|
|
9
31
|
type: "none";
|
|
10
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Compute the effective expiresAt for a freshly issued access token.
|
|
35
|
+
* Prefers the JWT's own `exp` claim; falls back to `now + expires_in` if the
|
|
36
|
+
* token can't be parsed (e.g. an opaque token).
|
|
37
|
+
*/
|
|
38
|
+
export declare function computeExpiresAt(accessToken: string, expiresInSeconds: number): number;
|
|
11
39
|
/**
|
|
12
40
|
* Get a valid access token, or null if not authenticated.
|
|
13
41
|
*
|
|
14
42
|
* Resolution order:
|
|
15
|
-
* 1.
|
|
16
|
-
* 2. Stored token from
|
|
43
|
+
* 1. MNEMOM_TOKEN environment variable (CI / non-interactive)
|
|
44
|
+
* 2. Stored token from auth store (auto-refreshes if expired)
|
|
17
45
|
*/
|
|
18
46
|
export declare function getAccessToken(): Promise<string | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Force a refresh of the stored access token regardless of local expiry, and
|
|
49
|
+
* return the new access token (or null if refresh failed).
|
|
50
|
+
*
|
|
51
|
+
* Intended as a 401-recovery hook for callers: if an authenticated request
|
|
52
|
+
* comes back unauthorized despite the local cache claiming a valid token, the
|
|
53
|
+
* cache is stale (clock skew, divergence between expires_in and the JWT's
|
|
54
|
+
* actual exp, or server-side revocation). Force a refresh and retry once.
|
|
55
|
+
*/
|
|
56
|
+
export declare function forceRefreshAccessToken(): Promise<string | null>;
|
|
19
57
|
/**
|
|
20
58
|
* Get a valid access token or exit with a helpful message.
|
|
21
59
|
*/
|
|
22
60
|
export declare function requireAccessToken(): Promise<string>;
|
|
23
61
|
/**
|
|
24
|
-
* Get the Mnemom API key from env var
|
|
25
|
-
*
|
|
26
|
-
* Resolution order:
|
|
27
|
-
* 1. MNEMOM_API_KEY environment variable
|
|
28
|
-
* 2. Stored mnemomApiKey from config
|
|
62
|
+
* Get the Mnemom API key from env var.
|
|
29
63
|
*/
|
|
30
64
|
export declare function getMnemomApiKey(): string | null;
|
|
31
65
|
/**
|
|
32
66
|
* Resolve the best available auth credential.
|
|
33
67
|
*
|
|
34
68
|
* Resolution order:
|
|
35
|
-
* 1. JWT (
|
|
36
|
-
* 2. API key (MNEMOM_API_KEY env
|
|
69
|
+
* 1. JWT (MNEMOM_TOKEN env or stored token with auto-refresh)
|
|
70
|
+
* 2. API key (MNEMOM_API_KEY env)
|
|
37
71
|
* 3. None
|
|
38
72
|
*/
|
|
39
73
|
export declare function resolveAuth(): Promise<AuthCredential>;
|
|
@@ -44,17 +78,8 @@ export declare function requireAuth(): Promise<AuthCredential & {
|
|
|
44
78
|
type: "jwt" | "api-key";
|
|
45
79
|
}>;
|
|
46
80
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* 1. Start a local HTTP server on a random port
|
|
50
|
-
* 2. Generate a random `state` nonce for CSRF protection
|
|
51
|
-
* 3. Open the browser to the API's CLI login page
|
|
52
|
-
* 4. Wait for the login page to POST tokens back to localhost
|
|
53
|
-
* 5. Verify state, store tokens, and close the server
|
|
81
|
+
* Check if the user is logged in (has any credential).
|
|
54
82
|
*/
|
|
83
|
+
export declare function isLoggedIn(): Promise<boolean>;
|
|
55
84
|
export declare function loginWithBrowser(): Promise<AuthTokens>;
|
|
56
|
-
/**
|
|
57
|
-
* Authenticate with email + password via the API auth proxy.
|
|
58
|
-
* Used by --no-browser fallback.
|
|
59
|
-
*/
|
|
60
85
|
export declare function loginWithPassword(email: string, password: string): Promise<AuthTokens>;
|