@mnemom/mnemom 0.10.0 → 0.11.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.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `mnemom api-key ...` commands — capability-based API key management
3
+ * (ADR-049). Mirrors the dashboard scope picker and the API mint-time
4
+ * vocabulary; one CLI surface for the same key lifecycle operations.
5
+ *
6
+ * mnemom api-key list — list active personal keys
7
+ * mnemom api-key create --name <name> [--scopes a,b,c]
8
+ * — mint a new key
9
+ * mnemom api-key rotate <key_id> — atomic mint-new + revoke-old
10
+ * mnemom api-key revoke <key_id> — soft-delete
11
+ *
12
+ * Auth: any of the standard CLI auth methods — JWT (`mnemom login`),
13
+ * MNEMOM_API_KEY env, or a stored API key. Note the chicken-and-egg:
14
+ * to mint your first key you'll need a JWT (the website's create-key
15
+ * dialog is the alternative). After that, an `api:write` key can mint
16
+ * additional keys for itself.
17
+ */
18
+ import { type ApiKeyScope } from "../lib/api.js";
19
+ /**
20
+ * Exported for tests. Parses a comma-separated `--scopes` flag value,
21
+ * accepts legacy `api` for backward compat, and returns a typed array.
22
+ * Throws on unknown scope names so users get a clear error instead of
23
+ * silently dropping intent.
24
+ */
25
+ export declare function parseScopes(input: string | undefined): ApiKeyScope[];
26
+ export declare function apiKeyListCommand(opts: {
27
+ json?: boolean;
28
+ }): Promise<void>;
29
+ export declare function apiKeyCreateCommand(opts: {
30
+ name?: string;
31
+ scopes?: string;
32
+ json?: boolean;
33
+ }): Promise<void>;
34
+ export declare function apiKeyRotateCommand(keyId: string | undefined, opts: {
35
+ json?: boolean;
36
+ }): Promise<void>;
37
+ export declare function apiKeyRevokeCommand(keyId: string | undefined): Promise<void>;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * `mnemom api-key ...` commands — capability-based API key management
3
+ * (ADR-049). Mirrors the dashboard scope picker and the API mint-time
4
+ * vocabulary; one CLI surface for the same key lifecycle operations.
5
+ *
6
+ * mnemom api-key list — list active personal keys
7
+ * mnemom api-key create --name <name> [--scopes a,b,c]
8
+ * — mint a new key
9
+ * mnemom api-key rotate <key_id> — atomic mint-new + revoke-old
10
+ * mnemom api-key revoke <key_id> — soft-delete
11
+ *
12
+ * Auth: any of the standard CLI auth methods — JWT (`mnemom login`),
13
+ * MNEMOM_API_KEY env, or a stored API key. Note the chicken-and-egg:
14
+ * to mint your first key you'll need a JWT (the website's create-key
15
+ * dialog is the alternative). After that, an `api:write` key can mint
16
+ * additional keys for itself.
17
+ */
18
+ import { listApiKeys, createApiKey, rotateApiKey, revokeApiKey, isLegacyScopeSet, API_KEY_SCOPES, DEFAULT_API_KEY_SCOPES, } from "../lib/api.js";
19
+ import { requireAuth } from "../lib/auth.js";
20
+ import { fmt } from "../lib/format.js";
21
+ // ─── Helpers ─────────────────────────────────────────────────────────────
22
+ /**
23
+ * Exported for tests. Parses a comma-separated `--scopes` flag value,
24
+ * accepts legacy `api` for backward compat, and returns a typed array.
25
+ * Throws on unknown scope names so users get a clear error instead of
26
+ * silently dropping intent.
27
+ */
28
+ export function parseScopes(input) {
29
+ if (!input)
30
+ return [...DEFAULT_API_KEY_SCOPES];
31
+ const raw = input
32
+ .split(",")
33
+ .map((s) => s.trim())
34
+ .filter((s) => s.length > 0);
35
+ if (raw.length === 0)
36
+ return [...DEFAULT_API_KEY_SCOPES];
37
+ const valid = new Set(API_KEY_SCOPES);
38
+ // Accept legacy 'api' input here so users who copy-paste from older
39
+ // docs/scripts get a clean canonicalization. The API also aliases.
40
+ valid.add("api");
41
+ const unknown = raw.filter((s) => !valid.has(s));
42
+ if (unknown.length > 0) {
43
+ throw new Error(`Unknown scope(s): ${unknown.join(", ")}. ` +
44
+ `Valid: ${Array.from(API_KEY_SCOPES).join(", ")}`);
45
+ }
46
+ return raw;
47
+ }
48
+ function renderScopeBadge(scope) {
49
+ // Admin scopes get a visual marker so the audit trail in the CLI
50
+ // makes elevated keys obvious at a glance.
51
+ if (scope === "admin:platform" || scope === "admin:org") {
52
+ return fmt.warn(`[${scope}]`);
53
+ }
54
+ return `[${scope}]`;
55
+ }
56
+ function formatRow(key) {
57
+ const created = key.created_at
58
+ ? new Date(key.created_at).toISOString().slice(0, 10)
59
+ : "?";
60
+ const lastUsed = key.last_used_at
61
+ ? new Date(key.last_used_at).toISOString().slice(0, 10)
62
+ : "never";
63
+ const scopes = (key.scopes ?? []).map(renderScopeBadge).join(" ");
64
+ const legacy = isLegacyScopeSet(key.scopes) ? fmt.dim(" (legacy)") : "";
65
+ return ` ${key.key_id} ${key.key_prefix}… ${key.name.padEnd(24)} ${scopes}${legacy}\n created ${created} · last used ${lastUsed}`;
66
+ }
67
+ // ─── mnemom api-key list ─────────────────────────────────────────────────
68
+ export async function apiKeyListCommand(opts) {
69
+ await requireAuth();
70
+ let keys;
71
+ try {
72
+ keys = await listApiKeys();
73
+ }
74
+ catch (err) {
75
+ console.error(fmt.error(`Failed to list api keys: ${err instanceof Error ? err.message : err}`));
76
+ process.exit(1);
77
+ }
78
+ if (opts.json) {
79
+ console.log(JSON.stringify(keys, null, 2));
80
+ return;
81
+ }
82
+ if (keys.length === 0) {
83
+ console.log("\nNo active api keys.\n");
84
+ console.log("Create one with `mnemom api-key create --name <name>`.\n");
85
+ return;
86
+ }
87
+ console.log();
88
+ console.log(fmt.header(`Active api keys (${keys.length})`));
89
+ console.log();
90
+ for (const k of keys) {
91
+ console.log(formatRow(k));
92
+ console.log();
93
+ }
94
+ }
95
+ // ─── mnemom api-key create ───────────────────────────────────────────────
96
+ export async function apiKeyCreateCommand(opts) {
97
+ await requireAuth();
98
+ if (!opts.name || opts.name.trim() === "") {
99
+ console.error(fmt.error("--name is required. Example: --name 'ci-prod'"));
100
+ process.exit(1);
101
+ }
102
+ let scopes;
103
+ try {
104
+ scopes = parseScopes(opts.scopes);
105
+ }
106
+ catch (err) {
107
+ console.error(fmt.error(err instanceof Error ? err.message : String(err)));
108
+ process.exit(1);
109
+ }
110
+ let created;
111
+ try {
112
+ created = await createApiKey(opts.name.trim(), scopes);
113
+ }
114
+ catch (err) {
115
+ console.error(fmt.error(`Create failed: ${err instanceof Error ? err.message : err}`));
116
+ process.exit(1);
117
+ }
118
+ if (opts.json) {
119
+ console.log(JSON.stringify(created, null, 2));
120
+ return;
121
+ }
122
+ console.log();
123
+ console.log(fmt.success(`Created ${created.key_id}`));
124
+ console.log();
125
+ console.log(fmt.label(" Name: ", ` ${created.name}`));
126
+ console.log(fmt.label(" Scopes: ", ` ${(created.scopes ?? []).join(", ")}`));
127
+ console.log(fmt.label(" Created: ", ` ${created.created_at}`));
128
+ console.log();
129
+ console.log(fmt.warn(" Cleartext (shown once — copy now):"));
130
+ console.log(` ${created.key}`);
131
+ console.log();
132
+ console.log(fmt.dim(" This is the only time the cleartext is returned."));
133
+ console.log(fmt.dim(" Mnemom stores only the SHA-256 hash; the key cannot be retrieved later."));
134
+ console.log();
135
+ }
136
+ // ─── mnemom api-key rotate ───────────────────────────────────────────────
137
+ export async function apiKeyRotateCommand(keyId, opts) {
138
+ await requireAuth();
139
+ if (!keyId) {
140
+ console.error(fmt.error("Usage: mnemom api-key rotate <key_id>"));
141
+ process.exit(1);
142
+ }
143
+ let rotated;
144
+ try {
145
+ rotated = await rotateApiKey(keyId);
146
+ }
147
+ catch (err) {
148
+ console.error(fmt.error(`Rotate failed: ${err instanceof Error ? err.message : err}`));
149
+ process.exit(1);
150
+ }
151
+ if (opts.json) {
152
+ console.log(JSON.stringify(rotated, null, 2));
153
+ return;
154
+ }
155
+ console.log();
156
+ console.log(fmt.success(`Rotated. Old key ${keyId} is now revoked.`));
157
+ console.log();
158
+ console.log(fmt.label(" New key id:", ` ${rotated.key_id}`));
159
+ console.log(fmt.label(" Name: ", ` ${rotated.name}`));
160
+ console.log(fmt.label(" Scopes: ", ` ${(rotated.scopes ?? []).join(", ")}`));
161
+ console.log();
162
+ console.log(fmt.warn(" New cleartext (shown once — copy now):"));
163
+ console.log(` ${rotated.key}`);
164
+ console.log();
165
+ }
166
+ // ─── mnemom api-key revoke ───────────────────────────────────────────────
167
+ export async function apiKeyRevokeCommand(keyId) {
168
+ await requireAuth();
169
+ if (!keyId) {
170
+ console.error(fmt.error("Usage: mnemom api-key revoke <key_id>"));
171
+ process.exit(1);
172
+ }
173
+ try {
174
+ await revokeApiKey(keyId);
175
+ }
176
+ catch (err) {
177
+ console.error(fmt.error(`Revoke failed: ${err instanceof Error ? err.message : err}`));
178
+ process.exit(1);
179
+ }
180
+ console.log(fmt.success(`Revoked ${keyId}.`));
181
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `mnemom governance` — operator CLI for governance signals (ADR-048).
3
+ *
4
+ * Subcommands:
5
+ *
6
+ * signals list | show | ack | resolve | dismiss
7
+ * destinations list | add | remove | test
8
+ * rules list | add | remove
9
+ *
10
+ * The signal surface is operator-actionable: cron-driven sideband
11
+ * detections, future protection / posture observations. Distinct from
12
+ * `mnemom advisories list` (now narrowed to runtime.* / manual.* per
13
+ * ADR-048 §1).
14
+ */
15
+ interface ListOpts {
16
+ org?: string;
17
+ team?: string;
18
+ agent?: string;
19
+ source?: string;
20
+ severity?: string;
21
+ status?: string;
22
+ scope?: string;
23
+ patternType?: string;
24
+ limit?: string;
25
+ since?: string;
26
+ json?: boolean;
27
+ }
28
+ interface AckOpts {
29
+ action?: string;
30
+ json?: boolean;
31
+ }
32
+ interface ResolveOpts {
33
+ status: string;
34
+ action?: string;
35
+ json?: boolean;
36
+ }
37
+ interface DismissOpts {
38
+ reason?: string;
39
+ json?: boolean;
40
+ }
41
+ interface DestAddOpts {
42
+ org: string;
43
+ channel: string;
44
+ config: string;
45
+ name?: string;
46
+ filter?: string;
47
+ json?: boolean;
48
+ }
49
+ interface RuleAddOpts {
50
+ org: string;
51
+ name: string;
52
+ predicate: string;
53
+ destinations: string;
54
+ json?: boolean;
55
+ }
56
+ export declare function governanceSignalsListCommand(opts: ListOpts): Promise<void>;
57
+ export declare function governanceSignalsShowCommand(signalId: string, opts: {
58
+ json?: boolean;
59
+ }): Promise<void>;
60
+ export declare function governanceSignalsAckCommand(signalId: string, opts: AckOpts): Promise<void>;
61
+ export declare function governanceSignalsResolveCommand(signalId: string, opts: ResolveOpts): Promise<void>;
62
+ export declare function governanceSignalsDismissCommand(signalId: string, opts: DismissOpts): Promise<void>;
63
+ export declare function governanceDestinationsListCommand(opts: {
64
+ org: string;
65
+ json?: boolean;
66
+ }): Promise<void>;
67
+ export declare function governanceDestinationsAddCommand(opts: DestAddOpts): Promise<void>;
68
+ export declare function governanceDestinationsRemoveCommand(destinationId: string, opts: {
69
+ org: string;
70
+ json?: boolean;
71
+ }): Promise<void>;
72
+ export declare function governanceDestinationsTestCommand(destinationId: string, opts: {
73
+ org: string;
74
+ json?: boolean;
75
+ }): Promise<void>;
76
+ export declare function governanceRulesListCommand(opts: {
77
+ org: string;
78
+ json?: boolean;
79
+ }): Promise<void>;
80
+ export declare function governanceRulesAddCommand(opts: RuleAddOpts): Promise<void>;
81
+ export declare function governanceRulesRemoveCommand(ruleId: string, opts: {
82
+ org: string;
83
+ json?: boolean;
84
+ }): Promise<void>;
85
+ export {};
@@ -0,0 +1,331 @@
1
+ /**
2
+ * `mnemom governance` — operator CLI for governance signals (ADR-048).
3
+ *
4
+ * Subcommands:
5
+ *
6
+ * signals list | show | ack | resolve | dismiss
7
+ * destinations list | add | remove | test
8
+ * rules list | add | remove
9
+ *
10
+ * The signal surface is operator-actionable: cron-driven sideband
11
+ * detections, future protection / posture observations. Distinct from
12
+ * `mnemom advisories list` (now narrowed to runtime.* / manual.* per
13
+ * ADR-048 §1).
14
+ */
15
+ import chalk from "chalk";
16
+ import { acknowledgeGovernanceSignal, createGovernanceDestination, createGovernanceRule, deleteGovernanceDestination, deleteGovernanceRule, dismissGovernanceSignal, getGovernanceSignal, listGovernanceDestinations, listGovernanceRules, listGovernanceSignalsForAgent, listGovernanceSignalsForOrg, listGovernanceSignalsForTeam, resolveGovernanceSignal, testGovernanceDestination, } from "../lib/api.js";
17
+ import { requireAuth } from "../lib/auth.js";
18
+ // ─── Formatting helpers ──────────────────────────────────────────────────
19
+ function shortId(id) {
20
+ return id.length <= 16 ? id : id.slice(0, 13) + "...";
21
+ }
22
+ function severityColor(s) {
23
+ switch (s) {
24
+ case "critical":
25
+ return chalk.bgRed.white;
26
+ case "high":
27
+ return chalk.red;
28
+ case "warn":
29
+ return chalk.yellow;
30
+ default:
31
+ return chalk.cyan;
32
+ }
33
+ }
34
+ function statusBadge(s) {
35
+ switch (s) {
36
+ case "open":
37
+ return chalk.bold.red("OPEN");
38
+ case "acknowledged":
39
+ return chalk.yellow("ACK");
40
+ case "resolved":
41
+ return chalk.green("RESOLVED");
42
+ case "dismissed":
43
+ return chalk.dim("DISMISSED");
44
+ case "expired":
45
+ return chalk.dim("expired");
46
+ default:
47
+ return chalk.dim(s.toUpperCase());
48
+ }
49
+ }
50
+ function formatSignalRow(s) {
51
+ const sev = severityColor(s.severity)(s.severity.toUpperCase().padEnd(8));
52
+ return [
53
+ chalk.dim(shortId(s.id).padEnd(16)),
54
+ sev,
55
+ chalk.cyan(s.source.padEnd(22)),
56
+ chalk.magenta((s.pattern_type || "—").slice(0, 28).padEnd(28)),
57
+ statusBadge(s.status).padEnd(20),
58
+ chalk.dim(s.detected_at.replace("T", " ").replace(/\.\d+Z$/, "Z")),
59
+ ].join(" ");
60
+ }
61
+ // ─── signals list ────────────────────────────────────────────────────────
62
+ export async function governanceSignalsListCommand(opts) {
63
+ await requireAuth();
64
+ const filters = {
65
+ source: opts.source,
66
+ severity: opts.severity,
67
+ status: opts.status,
68
+ scope: opts.scope,
69
+ pattern_type: opts.patternType,
70
+ since: opts.since,
71
+ limit: opts.limit ? parseInt(opts.limit, 10) : 100,
72
+ };
73
+ let result;
74
+ if (opts.org) {
75
+ result = await listGovernanceSignalsForOrg(opts.org, filters);
76
+ }
77
+ else if (opts.team) {
78
+ result = await listGovernanceSignalsForTeam(opts.team, filters);
79
+ }
80
+ else if (opts.agent) {
81
+ result = await listGovernanceSignalsForAgent(opts.agent, filters);
82
+ }
83
+ else {
84
+ throw new Error("One of --org, --team, --agent is required.");
85
+ }
86
+ if (opts.json) {
87
+ console.log(JSON.stringify(result, null, 2));
88
+ return;
89
+ }
90
+ const signals = result.signals;
91
+ if (signals.length === 0) {
92
+ console.log(chalk.dim("(no governance signals match)"));
93
+ return;
94
+ }
95
+ console.log([
96
+ chalk.bold("ID".padEnd(16)),
97
+ chalk.bold("SEVERITY".padEnd(8)),
98
+ chalk.bold("SOURCE".padEnd(22)),
99
+ chalk.bold("PATTERN".padEnd(28)),
100
+ chalk.bold("STATUS".padEnd(20)),
101
+ chalk.bold("DETECTED"),
102
+ ].join(" "));
103
+ for (const s of signals)
104
+ console.log(formatSignalRow(s));
105
+ console.log("");
106
+ console.log(chalk.dim(`${signals.length} signal(s).`));
107
+ }
108
+ // ─── signals show ────────────────────────────────────────────────────────
109
+ export async function governanceSignalsShowCommand(signalId, opts) {
110
+ await requireAuth();
111
+ const signal = await getGovernanceSignal(signalId);
112
+ if (opts.json) {
113
+ console.log(JSON.stringify(signal, null, 2));
114
+ return;
115
+ }
116
+ console.log(chalk.bold(`Signal ${signal.id}`));
117
+ console.log(` scope: ${signal.scope} (${signal.scope_id})`);
118
+ console.log(` source: ${chalk.cyan(signal.source)}`);
119
+ console.log(` pattern: ${chalk.magenta(signal.pattern_type)}`);
120
+ console.log(` severity: ${severityColor(signal.severity)(signal.severity)}`);
121
+ console.log(` status: ${statusBadge(signal.status)}`);
122
+ console.log(` detected: ${signal.detected_at} by ${signal.detected_by}`);
123
+ console.log(` org: ${signal.org_id}`);
124
+ if (signal.team_id)
125
+ console.log(` team: ${signal.team_id}`);
126
+ console.log(` agents: ${signal.agent_ids.length === 0 ? "(none)" : signal.agent_ids.join(", ")}`);
127
+ if (signal.acknowledged_at) {
128
+ console.log(` ack: ${signal.acknowledged_at} (${signal.acknowledged_actor_role ?? "?"})${signal.acknowledged_by ? ` by ${signal.acknowledged_by}` : ""}`);
129
+ }
130
+ if (signal.resolved_at) {
131
+ console.log(` resolved: ${signal.resolved_at} (${signal.resolution_status ?? "?"})${signal.action_taken ? ` — ${signal.action_taken}` : ""}`);
132
+ }
133
+ if (Object.keys(signal.detail ?? {}).length > 0) {
134
+ console.log(chalk.dim(" detail:"));
135
+ console.log(JSON.stringify(signal.detail, null, 2)
136
+ .split("\n")
137
+ .map((l) => " " + l)
138
+ .join("\n"));
139
+ }
140
+ }
141
+ // ─── signals ack/resolve/dismiss ─────────────────────────────────────────
142
+ export async function governanceSignalsAckCommand(signalId, opts) {
143
+ await requireAuth();
144
+ const signal = await acknowledgeGovernanceSignal(signalId, {
145
+ action_taken: opts.action,
146
+ });
147
+ if (opts.json) {
148
+ console.log(JSON.stringify(signal, null, 2));
149
+ return;
150
+ }
151
+ console.log(chalk.green(`✓ Acknowledged ${signal.id} as ${signal.acknowledged_actor_role}.`));
152
+ }
153
+ export async function governanceSignalsResolveCommand(signalId, opts) {
154
+ await requireAuth();
155
+ const valid = [
156
+ "action_taken",
157
+ "wont_fix",
158
+ "duplicate",
159
+ "false_positive",
160
+ "self_resolved",
161
+ ];
162
+ if (!valid.includes(opts.status)) {
163
+ throw new Error(`--status must be one of: ${valid.join(", ")}`);
164
+ }
165
+ const signal = await resolveGovernanceSignal(signalId, {
166
+ resolution_status: opts.status,
167
+ action_taken: opts.action,
168
+ });
169
+ if (opts.json) {
170
+ console.log(JSON.stringify(signal, null, 2));
171
+ return;
172
+ }
173
+ console.log(chalk.green(`✓ Resolved ${signal.id} (${signal.resolution_status}).`));
174
+ }
175
+ export async function governanceSignalsDismissCommand(signalId, opts) {
176
+ await requireAuth();
177
+ const signal = await dismissGovernanceSignal(signalId, { reason: opts.reason });
178
+ if (opts.json) {
179
+ console.log(JSON.stringify(signal, null, 2));
180
+ return;
181
+ }
182
+ console.log(chalk.dim(`✓ Dismissed ${signal.id}.`));
183
+ }
184
+ // ─── destinations ────────────────────────────────────────────────────────
185
+ function formatDestinationRow(d) {
186
+ return [
187
+ chalk.dim(shortId(d.id).padEnd(16)),
188
+ chalk.cyan(d.channel.padEnd(10)),
189
+ (d.display_name ?? "(unnamed)").slice(0, 32).padEnd(32),
190
+ d.enabled ? chalk.green("enabled") : chalk.dim("disabled"),
191
+ d.last_test_status === "ok"
192
+ ? chalk.green(`tested ✓ ${d.last_tested_at?.slice(0, 19) ?? ""}`)
193
+ : d.last_test_status === "failed"
194
+ ? chalk.red(`tested ✗ ${d.last_test_error?.slice(0, 64) ?? ""}`)
195
+ : chalk.dim("untested"),
196
+ ].join(" ");
197
+ }
198
+ export async function governanceDestinationsListCommand(opts) {
199
+ await requireAuth();
200
+ const result = await listGovernanceDestinations(opts.org);
201
+ if (opts.json) {
202
+ console.log(JSON.stringify(result, null, 2));
203
+ return;
204
+ }
205
+ if (result.destinations.length === 0) {
206
+ console.log(chalk.dim("(no destinations configured for this org)"));
207
+ return;
208
+ }
209
+ for (const d of result.destinations)
210
+ console.log(formatDestinationRow(d));
211
+ }
212
+ export async function governanceDestinationsAddCommand(opts) {
213
+ await requireAuth();
214
+ const validChannels = [
215
+ "webhook",
216
+ "slack",
217
+ "email",
218
+ "pagerduty",
219
+ ];
220
+ if (!validChannels.includes(opts.channel)) {
221
+ throw new Error(`--channel must be one of: ${validChannels.join(", ")}`);
222
+ }
223
+ let config;
224
+ try {
225
+ config = JSON.parse(opts.config);
226
+ }
227
+ catch {
228
+ throw new Error("--config must be valid JSON");
229
+ }
230
+ let filter;
231
+ if (opts.filter) {
232
+ try {
233
+ filter = JSON.parse(opts.filter);
234
+ }
235
+ catch {
236
+ throw new Error("--filter must be valid JSON");
237
+ }
238
+ }
239
+ const dest = await createGovernanceDestination(opts.org, {
240
+ channel: opts.channel,
241
+ config,
242
+ filter,
243
+ display_name: opts.name,
244
+ });
245
+ if (opts.json) {
246
+ console.log(JSON.stringify(dest, null, 2));
247
+ return;
248
+ }
249
+ console.log(chalk.green(`✓ Created destination ${dest.id} (${dest.channel}).`));
250
+ }
251
+ export async function governanceDestinationsRemoveCommand(destinationId, opts) {
252
+ await requireAuth();
253
+ await deleteGovernanceDestination(opts.org, destinationId);
254
+ if (opts.json) {
255
+ console.log(JSON.stringify({ ok: true, removed: destinationId }, null, 2));
256
+ return;
257
+ }
258
+ console.log(chalk.green(`✓ Removed destination ${destinationId}.`));
259
+ }
260
+ export async function governanceDestinationsTestCommand(destinationId, opts) {
261
+ await requireAuth();
262
+ const result = await testGovernanceDestination(opts.org, destinationId);
263
+ if (opts.json) {
264
+ console.log(JSON.stringify(result, null, 2));
265
+ return;
266
+ }
267
+ if (result.result.ok) {
268
+ console.log(chalk.green(`✓ Test signal delivered via ${result.channel} (${result.result.attempts} attempt).`));
269
+ }
270
+ else {
271
+ console.log(chalk.red(`✗ Test failed via ${result.channel}: ${result.result.last_error ?? "unknown"}`));
272
+ process.exitCode = 1;
273
+ }
274
+ }
275
+ // ─── rules ───────────────────────────────────────────────────────────────
276
+ function formatRuleRow(r) {
277
+ return [
278
+ chalk.dim(shortId(r.id).padEnd(16)),
279
+ r.name.slice(0, 32).padEnd(32),
280
+ r.enabled ? chalk.green("enabled") : chalk.dim("disabled"),
281
+ chalk.dim(`fired ${r.fire_count}×`),
282
+ r.last_fired_at ? chalk.dim(`last ${r.last_fired_at.slice(0, 19)}`) : "",
283
+ ].join(" ");
284
+ }
285
+ export async function governanceRulesListCommand(opts) {
286
+ await requireAuth();
287
+ const result = await listGovernanceRules(opts.org);
288
+ if (opts.json) {
289
+ console.log(JSON.stringify(result, null, 2));
290
+ return;
291
+ }
292
+ if (result.rules.length === 0) {
293
+ console.log(chalk.dim("(no escalation rules configured for this org)"));
294
+ return;
295
+ }
296
+ for (const r of result.rules)
297
+ console.log(formatRuleRow(r));
298
+ }
299
+ export async function governanceRulesAddCommand(opts) {
300
+ await requireAuth();
301
+ let predicate;
302
+ try {
303
+ predicate = JSON.parse(opts.predicate);
304
+ }
305
+ catch {
306
+ throw new Error("--predicate must be valid JSON");
307
+ }
308
+ const destinationIds = opts.destinations.split(",").map((s) => s.trim()).filter(Boolean);
309
+ if (destinationIds.length === 0) {
310
+ throw new Error("--destinations must list at least one destination ID (comma-separated)");
311
+ }
312
+ const rule = await createGovernanceRule(opts.org, {
313
+ name: opts.name,
314
+ predicate,
315
+ destination_ids: destinationIds,
316
+ });
317
+ if (opts.json) {
318
+ console.log(JSON.stringify(rule, null, 2));
319
+ return;
320
+ }
321
+ console.log(chalk.green(`✓ Created rule ${rule.id} ("${rule.name}").`));
322
+ }
323
+ export async function governanceRulesRemoveCommand(ruleId, opts) {
324
+ await requireAuth();
325
+ await deleteGovernanceRule(opts.org, ruleId);
326
+ if (opts.json) {
327
+ console.log(JSON.stringify({ ok: true, removed: ruleId }, null, 2));
328
+ return;
329
+ }
330
+ console.log(chalk.green(`✓ Removed rule ${ruleId}.`));
331
+ }
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { advisoriesListCommand, advisoriesShowCommand, } from "./commands/adviso
14
14
  import { postureListCommand, postureShowCommand, postureCreateCommand, postureUpdateCommand, postureCloneCommand, postureRevisionsCommand, postureDiffCommand, postureAssignCommand, postureUnassignCommand, posturePreviewComposeCommand, postureDeleteCommand, } from "./commands/posture.js";
15
15
  import { loginCommand, logoutCommand, whoamiCommand } from "./commands/auth.js";
16
16
  import { validateSafeHouseCommand } from "./commands/validate.js";
17
+ import { apiKeyListCommand, apiKeyCreateCommand, apiKeyRotateCommand, apiKeyRevokeCommand, } from "./commands/api-key.js";
17
18
  program
18
19
  .name("mnemom")
19
20
  .description("Transparent AI agent tracing")
@@ -521,6 +522,212 @@ advisoriesCmd
521
522
  }
522
523
  });
523
524
  // ============================================================================
525
+ // Governance signals (ADR-048) — operator-actionable observation surface
526
+ //
527
+ // Distinct from `mnemom advisories list` (which now narrows to runtime.* /
528
+ // manual.* per ADR-048 §1). Surfaces sideband.* + future protection.* /
529
+ // posture.* signals from governance_signals: ack/resolve/dismiss workflow,
530
+ // notification destinations (slack/email/pagerduty/webhook), escalation
531
+ // rules.
532
+ // ============================================================================
533
+ import { governanceSignalsListCommand, governanceSignalsShowCommand, governanceSignalsAckCommand, governanceSignalsResolveCommand, governanceSignalsDismissCommand, governanceDestinationsListCommand, governanceDestinationsAddCommand, governanceDestinationsRemoveCommand, governanceDestinationsTestCommand, governanceRulesListCommand, governanceRulesAddCommand, governanceRulesRemoveCommand, } from "./commands/governance.js";
534
+ const governanceCmd = program
535
+ .command("governance")
536
+ .description("Operator workflow for governance_signals (ADR-048)");
537
+ const govSignalsCmd = governanceCmd
538
+ .command("signals")
539
+ .description("List, inspect, and transition governance_signals");
540
+ govSignalsCmd
541
+ .command("list")
542
+ .description("List governance signals at platform/org/team/agent scope")
543
+ .option("--org <id>", "Org scope")
544
+ .option("--team <id>", "Team scope")
545
+ .option("--agent <id>", "Per-agent slice (signals affecting this agent)")
546
+ .option("--source <s>", "Filter by source (sideband.{drift,coherence,fault_line,fleet})")
547
+ .option("--severity <s>", "Filter by severity (info|warn|high|critical)")
548
+ .option("--status <s>", "Filter by status (open|acknowledged|resolved|dismissed|expired)")
549
+ .option("--scope <s>", "Filter by scope (platform|org|team|agent)")
550
+ .option("--pattern-type <s>", "Filter by pattern_type")
551
+ .option("--since <iso>", "Detected since (ISO timestamp)")
552
+ .option("--limit <n>", "Max rows (default 100, max 500)")
553
+ .option("--json", "Output JSON")
554
+ .action(async (options) => {
555
+ try {
556
+ await governanceSignalsListCommand(options);
557
+ }
558
+ catch (error) {
559
+ console.error("Error:", error instanceof Error ? error.message : error);
560
+ process.exit(1);
561
+ }
562
+ });
563
+ govSignalsCmd
564
+ .command("show <signal_id>")
565
+ .description("Show one signal in detail")
566
+ .option("--json", "Output JSON")
567
+ .action(async (signalId, options) => {
568
+ try {
569
+ await governanceSignalsShowCommand(signalId, options);
570
+ }
571
+ catch (error) {
572
+ console.error("Error:", error instanceof Error ? error.message : error);
573
+ process.exit(1);
574
+ }
575
+ });
576
+ govSignalsCmd
577
+ .command("ack <signal_id>")
578
+ .description("Acknowledge an open signal (org_admin / org_owner)")
579
+ .option("--action <text>", "Operator-authored action note")
580
+ .option("--json", "Output JSON")
581
+ .action(async (signalId, options) => {
582
+ try {
583
+ await governanceSignalsAckCommand(signalId, options);
584
+ }
585
+ catch (error) {
586
+ console.error("Error:", error instanceof Error ? error.message : error);
587
+ process.exit(1);
588
+ }
589
+ });
590
+ govSignalsCmd
591
+ .command("resolve <signal_id>")
592
+ .description("Resolve a signal with a resolution status")
593
+ .requiredOption("--status <s>", "Resolution status (action_taken|wont_fix|duplicate|false_positive|self_resolved)")
594
+ .option("--action <text>", "Operator-authored action note")
595
+ .option("--json", "Output JSON")
596
+ .action(async (signalId, options) => {
597
+ try {
598
+ await governanceSignalsResolveCommand(signalId, options);
599
+ }
600
+ catch (error) {
601
+ console.error("Error:", error instanceof Error ? error.message : error);
602
+ process.exit(1);
603
+ }
604
+ });
605
+ govSignalsCmd
606
+ .command("dismiss <signal_id>")
607
+ .description("Dismiss a signal as not actionable")
608
+ .option("--reason <text>", "Operator-authored dismissal reason")
609
+ .option("--json", "Output JSON")
610
+ .action(async (signalId, options) => {
611
+ try {
612
+ await governanceSignalsDismissCommand(signalId, options);
613
+ }
614
+ catch (error) {
615
+ console.error("Error:", error instanceof Error ? error.message : error);
616
+ process.exit(1);
617
+ }
618
+ });
619
+ const govDestCmd = governanceCmd
620
+ .command("destinations")
621
+ .description("Manage notification destinations (slack/email/pagerduty/webhook)");
622
+ govDestCmd
623
+ .command("list")
624
+ .description("List destinations for an org")
625
+ .requiredOption("--org <id>", "Org ID")
626
+ .option("--json", "Output JSON")
627
+ .action(async (options) => {
628
+ try {
629
+ await governanceDestinationsListCommand(options);
630
+ }
631
+ catch (error) {
632
+ console.error("Error:", error instanceof Error ? error.message : error);
633
+ process.exit(1);
634
+ }
635
+ });
636
+ govDestCmd
637
+ .command("add")
638
+ .description("Add a destination (channel + JSON config)")
639
+ .requiredOption("--org <id>", "Org ID")
640
+ .requiredOption("--channel <c>", "webhook|slack|email|pagerduty")
641
+ .requiredOption("--config <json>", "Channel config (e.g., '{\"url\":\"...\",\"signing_secret\":\"...\"}')")
642
+ .option("--name <n>", "Display name")
643
+ .option("--filter <json>", "Filter narrowing (sources/severities/etc)")
644
+ .option("--json", "Output JSON")
645
+ .action(async (options) => {
646
+ try {
647
+ await governanceDestinationsAddCommand(options);
648
+ }
649
+ catch (error) {
650
+ console.error("Error:", error instanceof Error ? error.message : error);
651
+ process.exit(1);
652
+ }
653
+ });
654
+ govDestCmd
655
+ .command("remove <destination_id>")
656
+ .description("Remove a destination")
657
+ .requiredOption("--org <id>", "Org ID")
658
+ .option("--json", "Output JSON")
659
+ .action(async (id, options) => {
660
+ try {
661
+ await governanceDestinationsRemoveCommand(id, options);
662
+ }
663
+ catch (error) {
664
+ console.error("Error:", error instanceof Error ? error.message : error);
665
+ process.exit(1);
666
+ }
667
+ });
668
+ govDestCmd
669
+ .command("test <destination_id>")
670
+ .description("Send a synthetic test signal through this destination")
671
+ .requiredOption("--org <id>", "Org ID")
672
+ .option("--json", "Output JSON")
673
+ .action(async (id, options) => {
674
+ try {
675
+ await governanceDestinationsTestCommand(id, options);
676
+ }
677
+ catch (error) {
678
+ console.error("Error:", error instanceof Error ? error.message : error);
679
+ process.exit(1);
680
+ }
681
+ });
682
+ const govRulesCmd = governanceCmd
683
+ .command("rules")
684
+ .description("Manage escalation rules (predicate → destinations)");
685
+ govRulesCmd
686
+ .command("list")
687
+ .description("List escalation rules for an org")
688
+ .requiredOption("--org <id>", "Org ID")
689
+ .option("--json", "Output JSON")
690
+ .action(async (options) => {
691
+ try {
692
+ await governanceRulesListCommand(options);
693
+ }
694
+ catch (error) {
695
+ console.error("Error:", error instanceof Error ? error.message : error);
696
+ process.exit(1);
697
+ }
698
+ });
699
+ govRulesCmd
700
+ .command("add")
701
+ .description("Add an escalation rule (predicate JSON + destination IDs)")
702
+ .requiredOption("--org <id>", "Org ID")
703
+ .requiredOption("--name <n>", "Rule name")
704
+ .requiredOption("--predicate <json>", "Predicate JSON (e.g., '{\"source\":\"sideband.fleet\",\"severity_min\":\"high\"}')")
705
+ .requiredOption("--destinations <ids>", "Comma-separated destination IDs")
706
+ .option("--json", "Output JSON")
707
+ .action(async (options) => {
708
+ try {
709
+ await governanceRulesAddCommand(options);
710
+ }
711
+ catch (error) {
712
+ console.error("Error:", error instanceof Error ? error.message : error);
713
+ process.exit(1);
714
+ }
715
+ });
716
+ govRulesCmd
717
+ .command("remove <rule_id>")
718
+ .description("Remove an escalation rule")
719
+ .requiredOption("--org <id>", "Org ID")
720
+ .option("--json", "Output JSON")
721
+ .action(async (id, options) => {
722
+ try {
723
+ await governanceRulesRemoveCommand(id, options);
724
+ }
725
+ catch (error) {
726
+ console.error("Error:", error instanceof Error ? error.message : error);
727
+ process.exit(1);
728
+ }
729
+ });
730
+ // ============================================================================
524
731
  // Trust Posture (Piece 3 of T1-3.1, ADR-045)
525
732
  //
526
733
  // Postures are team-scoped policy input that drives the observer's sideband
@@ -764,4 +971,68 @@ validateCmd
764
971
  process.exit(1);
765
972
  }
766
973
  });
974
+ // ── api-key (ADR-049 capability-based scopes) ────────────────────────────
975
+ const apiKeyCmd = program
976
+ .command("api-key")
977
+ .description("Manage personal API keys (capability-based scopes — ADR-049)");
978
+ apiKeyCmd
979
+ .command("list")
980
+ .description("List your active personal API keys with their scope sets")
981
+ .option("--json", "Emit raw JSON instead of rendered output")
982
+ .action(async (options) => {
983
+ try {
984
+ await apiKeyListCommand({ json: options.json });
985
+ }
986
+ catch (error) {
987
+ console.error("Error:", error instanceof Error ? error.message : error);
988
+ process.exit(1);
989
+ }
990
+ });
991
+ apiKeyCmd
992
+ .command("create")
993
+ .description("Mint a new personal API key. Default scopes: gateway, api:read, api:write. " +
994
+ "Admin scopes (admin:org, admin:platform) are role-gated at mint time.")
995
+ .requiredOption("--name <name>", "Friendly name for the key (e.g. 'ci-prod')")
996
+ .option("--scopes <list>", "Comma-separated capability scopes. Valid: " +
997
+ "gateway, api:read, api:write, admin:org, admin:platform. " +
998
+ "Default: gateway,api:read,api:write")
999
+ .option("--json", "Emit raw JSON instead of rendered output")
1000
+ .action(async (options) => {
1001
+ try {
1002
+ await apiKeyCreateCommand({
1003
+ name: options.name,
1004
+ scopes: options.scopes,
1005
+ json: options.json,
1006
+ });
1007
+ }
1008
+ catch (error) {
1009
+ console.error("Error:", error instanceof Error ? error.message : error);
1010
+ process.exit(1);
1011
+ }
1012
+ });
1013
+ apiKeyCmd
1014
+ .command("rotate <key_id>")
1015
+ .description("Atomic mint-new + revoke-old. Returns the new cleartext once.")
1016
+ .option("--json", "Emit raw JSON instead of rendered output")
1017
+ .action(async (keyId, options) => {
1018
+ try {
1019
+ await apiKeyRotateCommand(keyId, { json: options.json });
1020
+ }
1021
+ catch (error) {
1022
+ console.error("Error:", error instanceof Error ? error.message : error);
1023
+ process.exit(1);
1024
+ }
1025
+ });
1026
+ apiKeyCmd
1027
+ .command("revoke <key_id>")
1028
+ .description("Soft-revoke an active key. Action is immediate and irreversible.")
1029
+ .action(async (keyId) => {
1030
+ try {
1031
+ await apiKeyRevokeCommand(keyId);
1032
+ }
1033
+ catch (error) {
1034
+ console.error("Error:", error instanceof Error ? error.message : error);
1035
+ process.exit(1);
1036
+ }
1037
+ });
767
1038
  program.parse();
package/dist/lib/api.d.ts CHANGED
@@ -645,4 +645,187 @@ export declare function getSafeHouseHarnessState(): Promise<{
645
645
  full: HarnessRunSummary | null;
646
646
  fast: HarnessRunSummary | null;
647
647
  }>;
648
+ export type GovernanceSignalScope = "platform" | "org" | "team" | "agent";
649
+ export type GovernanceSignalSource = "sideband.drift" | "sideband.coherence" | "sideband.fault_line" | "sideband.fleet";
650
+ export type GovernanceSignalSeverity = "info" | "warn" | "high" | "critical";
651
+ export type GovernanceSignalStatus = "open" | "acknowledged" | "resolved" | "dismissed" | "expired";
652
+ export type GovernanceResolutionStatus = "action_taken" | "wont_fix" | "duplicate" | "false_positive" | "self_resolved";
653
+ export type GovernanceNotificationChannel = "webhook" | "slack" | "email" | "pagerduty";
654
+ export interface GovernanceSignal {
655
+ id: string;
656
+ scope: GovernanceSignalScope;
657
+ scope_id: string;
658
+ source: GovernanceSignalSource;
659
+ pattern_type: string;
660
+ severity: GovernanceSignalSeverity;
661
+ detected_at: string;
662
+ detected_by: string;
663
+ org_id: string;
664
+ team_id: string | null;
665
+ agent_ids: string[];
666
+ detail: Record<string, unknown>;
667
+ source_ref: Record<string, unknown>;
668
+ status: GovernanceSignalStatus;
669
+ acknowledged_by: string | null;
670
+ acknowledged_at: string | null;
671
+ acknowledged_actor_role: string | null;
672
+ resolution_status: GovernanceResolutionStatus | null;
673
+ action_taken: string | null;
674
+ resolved_by: string | null;
675
+ resolved_at: string | null;
676
+ expires_at: string | null;
677
+ notification_state: Record<string, unknown>;
678
+ created_at: string;
679
+ updated_at: string;
680
+ }
681
+ export interface GovernanceDestination {
682
+ id: string;
683
+ org_id: string;
684
+ channel: GovernanceNotificationChannel;
685
+ config: Record<string, unknown>;
686
+ filter: Record<string, unknown>;
687
+ enabled: boolean;
688
+ display_name: string | null;
689
+ last_tested_at: string | null;
690
+ last_test_status: "ok" | "failed" | null;
691
+ last_test_error: string | null;
692
+ created_at: string;
693
+ updated_at: string;
694
+ }
695
+ export interface GovernanceRule {
696
+ id: string;
697
+ org_id: string;
698
+ name: string;
699
+ predicate: Record<string, unknown>;
700
+ destination_ids: string[];
701
+ enabled: boolean;
702
+ last_fired_at: string | null;
703
+ fire_count: number;
704
+ created_at: string;
705
+ updated_at: string;
706
+ }
707
+ interface SignalListFilters {
708
+ source?: string;
709
+ severity?: string;
710
+ status?: string;
711
+ scope?: string;
712
+ pattern_type?: string;
713
+ since?: string;
714
+ limit?: number;
715
+ }
716
+ export declare function listGovernanceSignalsForOrg(orgId: string, opts?: SignalListFilters): Promise<{
717
+ org_id: string;
718
+ signals: GovernanceSignal[];
719
+ }>;
720
+ export declare function listGovernanceSignalsForTeam(teamId: string, opts?: SignalListFilters): Promise<{
721
+ team_id: string;
722
+ signals: GovernanceSignal[];
723
+ }>;
724
+ export declare function listGovernanceSignalsForAgent(agentId: string, opts?: SignalListFilters): Promise<{
725
+ agent_id: string;
726
+ signals: GovernanceSignal[];
727
+ }>;
728
+ export declare function getGovernanceSignal(id: string): Promise<GovernanceSignal>;
729
+ export declare function acknowledgeGovernanceSignal(id: string, body?: {
730
+ action_taken?: string;
731
+ }): Promise<GovernanceSignal>;
732
+ export declare function resolveGovernanceSignal(id: string, body: {
733
+ resolution_status: GovernanceResolutionStatus;
734
+ action_taken?: string;
735
+ }): Promise<GovernanceSignal>;
736
+ export declare function dismissGovernanceSignal(id: string, body?: {
737
+ reason?: string;
738
+ }): Promise<GovernanceSignal>;
739
+ export declare function listGovernanceDestinations(orgId: string): Promise<{
740
+ org_id: string;
741
+ destinations: GovernanceDestination[];
742
+ }>;
743
+ export declare function createGovernanceDestination(orgId: string, body: {
744
+ channel: GovernanceNotificationChannel;
745
+ config: Record<string, unknown>;
746
+ filter?: Record<string, unknown>;
747
+ display_name?: string;
748
+ enabled?: boolean;
749
+ }): Promise<GovernanceDestination>;
750
+ export declare function deleteGovernanceDestination(orgId: string, destinationId: string): Promise<{
751
+ ok: true;
752
+ }>;
753
+ export declare function testGovernanceDestination(orgId: string, destinationId: string): Promise<{
754
+ destination_id: string;
755
+ channel: GovernanceNotificationChannel;
756
+ result: {
757
+ ok: boolean;
758
+ attempts: number;
759
+ delivered_at?: string;
760
+ last_error?: string;
761
+ };
762
+ }>;
763
+ export declare function listGovernanceRules(orgId: string): Promise<{
764
+ org_id: string;
765
+ rules: GovernanceRule[];
766
+ }>;
767
+ export declare function createGovernanceRule(orgId: string, body: {
768
+ name: string;
769
+ predicate: Record<string, unknown>;
770
+ destination_ids: string[];
771
+ enabled?: boolean;
772
+ }): Promise<GovernanceRule>;
773
+ export declare function deleteGovernanceRule(orgId: string, ruleId: string): Promise<{
774
+ ok: true;
775
+ }>;
776
+ /**
777
+ * Capability-based scope vocabulary (ADR-049). Mirrors the API's
778
+ * VALID_SCOPES exactly. Update both surfaces together when the
779
+ * vocabulary changes.
780
+ */
781
+ export declare const API_KEY_SCOPES: readonly ["gateway", "api:read", "api:write", "admin:org", "admin:platform"];
782
+ export type ApiKeyScope = (typeof API_KEY_SCOPES)[number];
783
+ export declare const DEFAULT_API_KEY_SCOPES: ApiKeyScope[];
784
+ /**
785
+ * Recognize legacy two-scope sets so the CLI can annotate pre-ADR-049
786
+ * keys appropriately. Mirror of mnemom-api `expandLegacyScopes` logic
787
+ * for display purposes; the auth gate handles the actual aliasing.
788
+ */
789
+ export declare function isLegacyScopeSet(scopes: string[] | undefined): boolean;
790
+ export interface ApiKeyListItem {
791
+ key_id: string;
792
+ key_prefix: string;
793
+ name: string;
794
+ scopes: string[];
795
+ created_at: string;
796
+ last_used_at: string | null;
797
+ org_id?: string | null;
798
+ }
799
+ export interface ApiKeyCreated {
800
+ key_id: string;
801
+ key: string;
802
+ key_prefix: string;
803
+ name: string;
804
+ scopes: string[];
805
+ created_at: string;
806
+ }
807
+ /**
808
+ * GET /v1/api-keys — list the caller's active personal API keys.
809
+ */
810
+ export declare function listApiKeys(): Promise<ApiKeyListItem[]>;
811
+ /**
812
+ * POST /v1/api-keys — mint a new personal API key with explicit scopes.
813
+ *
814
+ * Returns the full secret only on this call. The mint-time ceiling
815
+ * rejects admin scopes the caller is not eligible for (admin:platform
816
+ * for non-staff, admin:org for users not in any org-admin role).
817
+ */
818
+ export declare function createApiKey(name: string, scopes: ApiKeyScope[]): Promise<ApiKeyCreated>;
819
+ /**
820
+ * POST /v1/api-keys/{key_id}/rotate — atomic mint-new + revoke-old.
821
+ * Returns the full new secret only on this call. The new key inherits
822
+ * the old key's name and scopes verbatim; the old key is revoked the
823
+ * moment this returns.
824
+ */
825
+ export declare function rotateApiKey(keyId: string): Promise<ApiKeyCreated>;
826
+ /**
827
+ * DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
828
+ * audit; `is_active` flips to false and `revoked_at` is timestamped.
829
+ */
830
+ export declare function revokeApiKey(keyId: string): Promise<void>;
648
831
  export {};
package/dist/lib/api.js CHANGED
@@ -1162,3 +1162,211 @@ export async function getSafeHouseHarnessState() {
1162
1162
  }
1163
1163
  return (await response.json());
1164
1164
  }
1165
+ function buildSignalListQuery(opts = {}) {
1166
+ const params = new URLSearchParams();
1167
+ if (opts.source)
1168
+ params.set("source", opts.source);
1169
+ if (opts.severity)
1170
+ params.set("severity", opts.severity);
1171
+ if (opts.status)
1172
+ params.set("status", opts.status);
1173
+ if (opts.scope)
1174
+ params.set("scope", opts.scope);
1175
+ if (opts.pattern_type)
1176
+ params.set("pattern_type", opts.pattern_type);
1177
+ if (opts.since)
1178
+ params.set("since", opts.since);
1179
+ if (opts.limit)
1180
+ params.set("limit", String(opts.limit));
1181
+ const qs = params.toString();
1182
+ return qs ? `?${qs}` : "";
1183
+ }
1184
+ async function gFetch(path, init = {}, notFoundLabel) {
1185
+ const url = validateUrl(`${API_BASE}${path}`);
1186
+ const response = await fetchWithAuthRetry(url, async () => ({
1187
+ method: init.method ?? "GET",
1188
+ headers: {
1189
+ ...(await authHeaders()),
1190
+ Accept: "application/json",
1191
+ ...(init.body ? { "Content-Type": "application/json" } : {}),
1192
+ ...(init.headers ?? {}),
1193
+ },
1194
+ body: init.body,
1195
+ }));
1196
+ if (!response.ok) {
1197
+ if (response.status === 401)
1198
+ throw new Error("Not authenticated.");
1199
+ if (response.status === 403)
1200
+ throw new Error("Permission denied (need org admin / membership).");
1201
+ if (response.status === 404 && notFoundLabel)
1202
+ throw new Error(`${notFoundLabel} not found.`);
1203
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1204
+ throw new Error(err.message || `Request failed: ${response.status}`);
1205
+ }
1206
+ return (await response.json());
1207
+ }
1208
+ export async function listGovernanceSignalsForOrg(orgId, opts = {}) {
1209
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Org");
1210
+ }
1211
+ export async function listGovernanceSignalsForTeam(teamId, opts = {}) {
1212
+ return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Team");
1213
+ }
1214
+ export async function listGovernanceSignalsForAgent(agentId, opts = {}) {
1215
+ return gFetch(`/v1/agents/${encodeURIComponent(agentId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Agent");
1216
+ }
1217
+ export async function getGovernanceSignal(id) {
1218
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}`, {}, "Signal");
1219
+ }
1220
+ export async function acknowledgeGovernanceSignal(id, body = {}) {
1221
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/acknowledge`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1222
+ }
1223
+ export async function resolveGovernanceSignal(id, body) {
1224
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/resolve`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1225
+ }
1226
+ export async function dismissGovernanceSignal(id, body = {}) {
1227
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/dismiss`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1228
+ }
1229
+ export async function listGovernanceDestinations(orgId) {
1230
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, {}, "Org");
1231
+ }
1232
+ export async function createGovernanceDestination(orgId, body) {
1233
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, { method: "POST", body: JSON.stringify(body) }, "Org");
1234
+ }
1235
+ export async function deleteGovernanceDestination(orgId, destinationId) {
1236
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}`, { method: "DELETE" }, "Destination");
1237
+ }
1238
+ export async function testGovernanceDestination(orgId, destinationId) {
1239
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}/test`, { method: "POST", body: JSON.stringify({}) }, "Destination");
1240
+ }
1241
+ export async function listGovernanceRules(orgId) {
1242
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, {}, "Org");
1243
+ }
1244
+ export async function createGovernanceRule(orgId, body) {
1245
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, { method: "POST", body: JSON.stringify(body) }, "Org");
1246
+ }
1247
+ export async function deleteGovernanceRule(orgId, ruleId) {
1248
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules/${encodeURIComponent(ruleId)}`, { method: "DELETE" }, "Rule");
1249
+ }
1250
+ // ─── api-keys (ADR-049) ─────────────────────────────────────────────────
1251
+ /**
1252
+ * Capability-based scope vocabulary (ADR-049). Mirrors the API's
1253
+ * VALID_SCOPES exactly. Update both surfaces together when the
1254
+ * vocabulary changes.
1255
+ */
1256
+ export const API_KEY_SCOPES = [
1257
+ "gateway",
1258
+ "api:read",
1259
+ "api:write",
1260
+ "admin:org",
1261
+ "admin:platform",
1262
+ ];
1263
+ export const DEFAULT_API_KEY_SCOPES = [
1264
+ "gateway",
1265
+ "api:read",
1266
+ "api:write",
1267
+ ];
1268
+ /**
1269
+ * Recognize legacy two-scope sets so the CLI can annotate pre-ADR-049
1270
+ * keys appropriately. Mirror of mnemom-api `expandLegacyScopes` logic
1271
+ * for display purposes; the auth gate handles the actual aliasing.
1272
+ */
1273
+ export function isLegacyScopeSet(scopes) {
1274
+ if (!scopes || scopes.length === 0)
1275
+ return false;
1276
+ if (scopes.length === 2 && scopes.includes("gateway") && scopes.includes("api")) {
1277
+ return true;
1278
+ }
1279
+ if (scopes.length === 1 && scopes[0] === "api")
1280
+ return true;
1281
+ return false;
1282
+ }
1283
+ /**
1284
+ * GET /v1/api-keys — list the caller's active personal API keys.
1285
+ */
1286
+ export async function listApiKeys() {
1287
+ const url = validateUrl(`${API_BASE}/v1/api-keys`);
1288
+ const response = await fetchWithAuthRetry(url, async () => ({
1289
+ headers: await authHeaders(),
1290
+ }));
1291
+ if (!response.ok) {
1292
+ if (response.status === 401) {
1293
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1294
+ }
1295
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1296
+ throw new Error(err.message || `Failed to list api keys: ${response.status}`);
1297
+ }
1298
+ const data = (await response.json());
1299
+ return data.keys ?? [];
1300
+ }
1301
+ /**
1302
+ * POST /v1/api-keys — mint a new personal API key with explicit scopes.
1303
+ *
1304
+ * Returns the full secret only on this call. The mint-time ceiling
1305
+ * rejects admin scopes the caller is not eligible for (admin:platform
1306
+ * for non-staff, admin:org for users not in any org-admin role).
1307
+ */
1308
+ export async function createApiKey(name, scopes) {
1309
+ const url = validateUrl(`${API_BASE}/v1/api-keys`);
1310
+ const response = await fetchWithAuthRetry(url, async () => ({
1311
+ method: "POST",
1312
+ headers: { ...(await authHeaders()), "Content-Type": "application/json" },
1313
+ body: JSON.stringify({ name, scopes }),
1314
+ }));
1315
+ if (!response.ok) {
1316
+ if (response.status === 401) {
1317
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1318
+ }
1319
+ const body = await response.text().catch(() => "");
1320
+ if (response.status === 403) {
1321
+ throw new Error(`Mint-time ceiling rejected scope: ${body}`);
1322
+ }
1323
+ if (response.status === 400) {
1324
+ throw new Error(`Invalid scope(s): ${body}`);
1325
+ }
1326
+ throw new Error(`Failed to create api key: ${response.status} ${body}`);
1327
+ }
1328
+ return (await response.json());
1329
+ }
1330
+ /**
1331
+ * POST /v1/api-keys/{key_id}/rotate — atomic mint-new + revoke-old.
1332
+ * Returns the full new secret only on this call. The new key inherits
1333
+ * the old key's name and scopes verbatim; the old key is revoked the
1334
+ * moment this returns.
1335
+ */
1336
+ export async function rotateApiKey(keyId) {
1337
+ const url = validateUrl(`${API_BASE}/v1/api-keys/${encodeURIComponent(keyId)}/rotate`);
1338
+ const response = await fetchWithAuthRetry(url, async () => ({
1339
+ method: "POST",
1340
+ headers: { ...(await authHeaders()), "Content-Type": "application/json" },
1341
+ }));
1342
+ if (!response.ok) {
1343
+ if (response.status === 401) {
1344
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1345
+ }
1346
+ if (response.status === 404)
1347
+ throw new Error(`Key not found: ${keyId}`);
1348
+ const body = await response.text().catch(() => "");
1349
+ throw new Error(`Failed to rotate api key: ${response.status} ${body}`);
1350
+ }
1351
+ return (await response.json());
1352
+ }
1353
+ /**
1354
+ * DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
1355
+ * audit; `is_active` flips to false and `revoked_at` is timestamped.
1356
+ */
1357
+ export async function revokeApiKey(keyId) {
1358
+ const url = validateUrl(`${API_BASE}/v1/api-keys/${encodeURIComponent(keyId)}`);
1359
+ const response = await fetchWithAuthRetry(url, async () => ({
1360
+ method: "DELETE",
1361
+ headers: await authHeaders(),
1362
+ }));
1363
+ if (!response.ok) {
1364
+ if (response.status === 401) {
1365
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1366
+ }
1367
+ if (response.status === 404)
1368
+ throw new Error(`Key not found: ${keyId}`);
1369
+ const body = await response.text().catch(() => "");
1370
+ throw new Error(`Failed to revoke api key: ${response.status} ${body}`);
1371
+ }
1372
+ }
@@ -24,6 +24,10 @@ export declare const fmt: {
24
24
  * Dim label with value
25
25
  */
26
26
  label(key: string, val: string): string;
27
+ /**
28
+ * Dim text (for inline annotations and footnotes).
29
+ */
30
+ dim(text: string): string;
27
31
  /**
28
32
  * Syntax-highlighted JSON output
29
33
  */
@@ -38,6 +38,12 @@ export const fmt = {
38
38
  label(key, val) {
39
39
  return `${chalk.dim(key)} ${val}`;
40
40
  },
41
+ /**
42
+ * Dim text (for inline annotations and footnotes).
43
+ */
44
+ dim(text) {
45
+ return chalk.dim(text);
46
+ },
41
47
  /**
42
48
  * Syntax-highlighted JSON output
43
49
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {