@mnemom/mnemom 0.9.1 → 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.
- package/dist/commands/advisories.d.ts +32 -0
- package/dist/commands/advisories.js +158 -0
- package/dist/commands/api-key.d.ts +37 -0
- package/dist/commands/api-key.js +181 -0
- package/dist/commands/governance.d.ts +85 -0
- package/dist/commands/governance.js +331 -0
- package/dist/commands/org.d.ts +23 -0
- package/dist/commands/org.js +122 -0
- package/dist/commands/posture.d.ts +71 -0
- package/dist/commands/posture.js +440 -0
- package/dist/commands/team.d.ts +56 -0
- package/dist/commands/team.js +507 -0
- package/dist/commands/validate.d.ts +23 -0
- package/dist/commands/validate.js +150 -0
- package/dist/index.js +713 -14
- package/dist/lib/api.d.ts +576 -0
- package/dist/lib/api.js +884 -5
- package/dist/lib/format.d.ts +4 -0
- package/dist/lib/format.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom team ...` commands — Piece 2 of T1-3.1 (ADR-044 amended);
|
|
3
|
+
* extended in Piece 5 (ADR-046) with `team admin {grant,revoke,list}`.
|
|
4
|
+
*
|
|
5
|
+
* mnemom team list — list all teams across orgs
|
|
6
|
+
* mnemom team show <team_id> [--json] — show one team's detail
|
|
7
|
+
* mnemom team alignment-template <team_id> — read alignment template
|
|
8
|
+
* mnemom team alignment-template <team_id> --set <f> — write from YAML/JSON file
|
|
9
|
+
* mnemom team alignment-template <team_id> --clear — clear template
|
|
10
|
+
* mnemom team protection-template <team_id> [...] — same shape (protection)
|
|
11
|
+
* mnemom team preview-compose <team_id> [--protection] — dry-run from stdin/file
|
|
12
|
+
* mnemom team admin grant <team_id> --user <user_id> — grant team_admin
|
|
13
|
+
* mnemom team admin revoke <team_id> --user <user_id> — revoke team_admin
|
|
14
|
+
* mnemom team admin list <team_id> [--json] — list active grants
|
|
15
|
+
*
|
|
16
|
+
* Per ADR-044 amended + Charter §I11: team membership is OPTIONAL. Teams
|
|
17
|
+
* are an agent grouping primitive within an org; users have no team
|
|
18
|
+
* concept. Backend RBAC for the template endpoints is purely org-level
|
|
19
|
+
* (requireOrgRole on team.org_id). The Team Admin role added in Piece 5
|
|
20
|
+
* (ADR-046) is the FIRST user-team association — a per-(org, team, user)
|
|
21
|
+
* grant managed via `mnemom team admin`.
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { listMyTeams, getTeam, getTeamTemplate, putTeamTemplate, deleteTeamTemplate, previewComposeTeamTemplate, grantTeamAdmin, revokeTeamAdmin, listTeamAdmins, } from "../lib/api.js";
|
|
25
|
+
import { requireAuth } from "../lib/auth.js";
|
|
26
|
+
import { fmt } from "../lib/format.js";
|
|
27
|
+
// ─── mnemom team list ────────────────────────────────────────────────────
|
|
28
|
+
export async function teamListCommand(opts) {
|
|
29
|
+
await requireAuth();
|
|
30
|
+
let teams;
|
|
31
|
+
try {
|
|
32
|
+
teams = await listMyTeams();
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
+
console.log(fmt.error(`Failed to list teams: ${msg}`) + "\n");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
console.log(JSON.stringify(teams, null, 2));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log(fmt.header("Teams"));
|
|
44
|
+
console.log();
|
|
45
|
+
if (teams.length === 0) {
|
|
46
|
+
console.log(" No teams found.\n" +
|
|
47
|
+
" Teams are an optional agent grouping primitive within an org.\n" +
|
|
48
|
+
" Solo agents (zero teams) compose under Platform → Org → Agent.\n");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const nameW = 28;
|
|
52
|
+
const idW = 38;
|
|
53
|
+
const orgW = 22;
|
|
54
|
+
const memberW = 8;
|
|
55
|
+
const header = "Name".padEnd(nameW) +
|
|
56
|
+
"Team ID".padEnd(idW) +
|
|
57
|
+
"Org".padEnd(orgW) +
|
|
58
|
+
"Members".padEnd(memberW);
|
|
59
|
+
console.log(` ${header}`);
|
|
60
|
+
console.log(` ${"─".repeat(nameW + idW + orgW + memberW)}`);
|
|
61
|
+
for (const team of teams) {
|
|
62
|
+
const name = team.name.slice(0, nameW - 2).padEnd(nameW);
|
|
63
|
+
const id = team.team_id.slice(0, idW - 2).padEnd(idW);
|
|
64
|
+
const org = (team.org_name ?? team.org_id).slice(0, orgW - 2).padEnd(orgW);
|
|
65
|
+
const members = String(team.member_count ?? 0).padEnd(memberW);
|
|
66
|
+
console.log(` ${name}${id}${org}${members}`);
|
|
67
|
+
}
|
|
68
|
+
console.log(`\n Total: ${teams.length} team(s)\n`);
|
|
69
|
+
}
|
|
70
|
+
// ─── mnemom team show <team_id> ──────────────────────────────────────────
|
|
71
|
+
export async function teamShowCommand(teamId, opts) {
|
|
72
|
+
await requireAuth();
|
|
73
|
+
if (!teamId) {
|
|
74
|
+
console.log(fmt.error("Usage: mnemom team show <team_id>") + "\n");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
let team;
|
|
79
|
+
try {
|
|
80
|
+
team = await getTeam(teamId);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
84
|
+
console.log(fmt.error(`Failed to fetch team: ${msg}`) + "\n");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (opts.json) {
|
|
89
|
+
console.log(JSON.stringify(team, null, 2));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log(fmt.header(team.name));
|
|
93
|
+
console.log();
|
|
94
|
+
console.log(` Team ID: ${team.team_id}`);
|
|
95
|
+
console.log(` Org ID: ${team.org_id}`);
|
|
96
|
+
if (team.description) {
|
|
97
|
+
console.log(` Description: ${team.description}`);
|
|
98
|
+
}
|
|
99
|
+
console.log(` Status: ${team.status ?? "active"}`);
|
|
100
|
+
if (typeof team.member_count === "number") {
|
|
101
|
+
console.log(` Members: ${team.member_count}`);
|
|
102
|
+
}
|
|
103
|
+
if (team.visibility) {
|
|
104
|
+
console.log(` Visibility: ${team.visibility}`);
|
|
105
|
+
}
|
|
106
|
+
if (team.created_at) {
|
|
107
|
+
console.log(` Created: ${new Date(team.created_at).toLocaleDateString()}`);
|
|
108
|
+
}
|
|
109
|
+
console.log();
|
|
110
|
+
}
|
|
111
|
+
export async function teamTemplateCommand(kind, teamId, opts) {
|
|
112
|
+
await requireAuth();
|
|
113
|
+
if (!teamId) {
|
|
114
|
+
console.log(fmt.error(`Usage: mnemom team ${kind}-template <team_id> [--set <file> | --clear] [--json]`) +
|
|
115
|
+
"\n");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (opts.set && opts.clear) {
|
|
120
|
+
console.log(fmt.error("Specify --set OR --clear, not both.") + "\n");
|
|
121
|
+
process.exit(1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// ── PUT (--set <file>) ─────────────────────────────────────────────
|
|
125
|
+
if (opts.set) {
|
|
126
|
+
let body;
|
|
127
|
+
try {
|
|
128
|
+
body = readFileSync(opts.set, "utf8");
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
132
|
+
console.log(fmt.error(`Failed to read '${opts.set}': ${msg}`) + "\n");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let result;
|
|
137
|
+
try {
|
|
138
|
+
result = await putTeamTemplate(teamId, kind, body);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
142
|
+
console.log(fmt.error(`Failed to write team ${kind} template: ${msg}`) + "\n");
|
|
143
|
+
process.exit(1);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (opts.json) {
|
|
147
|
+
console.log(JSON.stringify(result, null, 2));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
console.log(fmt.header(`Team ${kind} template — saved`));
|
|
151
|
+
console.log();
|
|
152
|
+
console.log(` Team ID: ${result.team_id}`);
|
|
153
|
+
console.log(` Org ID: ${result.org_id}`);
|
|
154
|
+
console.log(` Enabled: ${result.enabled ? "yes" : "no"}`);
|
|
155
|
+
if (typeof result.agents_flagged_for_recompose === "number") {
|
|
156
|
+
console.log(` Agents recompose: ${result.agents_flagged_for_recompose} ` +
|
|
157
|
+
`(active members of this team only — not blanket fan-out)`);
|
|
158
|
+
}
|
|
159
|
+
console.log();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// ── DELETE (--clear) ────────────────────────────────────────────────
|
|
163
|
+
if (opts.clear) {
|
|
164
|
+
let result;
|
|
165
|
+
try {
|
|
166
|
+
result = await deleteTeamTemplate(teamId, kind);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
170
|
+
console.log(fmt.error(`Failed to clear team ${kind} template: ${msg}`) + "\n");
|
|
171
|
+
process.exit(1);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (opts.json) {
|
|
175
|
+
console.log(JSON.stringify(result, null, 2));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log(fmt.header(`Team ${kind} template — cleared`));
|
|
179
|
+
console.log();
|
|
180
|
+
console.log(` Team ID: ${result.team_id}`);
|
|
181
|
+
console.log(` Org ID: ${result.org_id}`);
|
|
182
|
+
console.log(` Deleted: yes`);
|
|
183
|
+
if (typeof result.agents_flagged_for_recompose === "number") {
|
|
184
|
+
console.log(` Agents recompose: ${result.agents_flagged_for_recompose}`);
|
|
185
|
+
}
|
|
186
|
+
console.log();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// ── GET (default) ──────────────────────────────────────────────────
|
|
190
|
+
let result;
|
|
191
|
+
try {
|
|
192
|
+
result = await getTeamTemplate(teamId, kind);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
196
|
+
console.log(fmt.error(`Failed to read team ${kind} template: ${msg}`) + "\n");
|
|
197
|
+
process.exit(1);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (opts.json) {
|
|
201
|
+
console.log(JSON.stringify(result, null, 2));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.log(fmt.header(`Team ${kind} template`));
|
|
205
|
+
console.log();
|
|
206
|
+
console.log(` Team ID: ${result.team_id}`);
|
|
207
|
+
console.log(` Org ID: ${result.org_id}`);
|
|
208
|
+
console.log(` Enabled: ${result.enabled ? "yes" : "no"}`);
|
|
209
|
+
console.log();
|
|
210
|
+
if (result.template) {
|
|
211
|
+
console.log(` Template:`);
|
|
212
|
+
console.log(JSON.stringify(result.template, null, 2));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
console.log(` No template set. Use --set <file> to write a YAML or JSON template.`);
|
|
216
|
+
}
|
|
217
|
+
console.log();
|
|
218
|
+
}
|
|
219
|
+
// ─── mnemom team preview-compose <team_id> ───────────────────────────────
|
|
220
|
+
export async function teamPreviewComposeCommand(teamId, opts) {
|
|
221
|
+
await requireAuth();
|
|
222
|
+
if (!teamId) {
|
|
223
|
+
console.log(fmt.error("Usage: mnemom team preview-compose <team_id> [--protection] [--from <file>] [--json]") + "\n");
|
|
224
|
+
process.exit(1);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const kind = opts.protection ? "protection" : "alignment";
|
|
228
|
+
let body;
|
|
229
|
+
if (opts.from) {
|
|
230
|
+
try {
|
|
231
|
+
body = readFileSync(opts.from, "utf8");
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
235
|
+
console.log(fmt.error(`Failed to read '${opts.from}': ${msg}`) + "\n");
|
|
236
|
+
process.exit(1);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// Read from stdin if no file specified.
|
|
242
|
+
body = await readAllStdin();
|
|
243
|
+
if (!body.trim()) {
|
|
244
|
+
console.log(fmt.error("No template body supplied. Pass --from <file> or pipe YAML/JSON via stdin.") + "\n");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
let result;
|
|
250
|
+
try {
|
|
251
|
+
result = await previewComposeTeamTemplate(teamId, kind, body);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
console.log(fmt.error(`Preview failed: ${msg}`) + "\n");
|
|
256
|
+
process.exit(1);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (opts.json) {
|
|
260
|
+
console.log(JSON.stringify(result, null, 2));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
console.log(fmt.header(`Preview compose — team ${kind}`));
|
|
264
|
+
console.log();
|
|
265
|
+
console.log(` Team ID: ${teamId}`);
|
|
266
|
+
console.log();
|
|
267
|
+
console.log(` Composed (org floor + draft team + platform ceiling):`);
|
|
268
|
+
console.log(JSON.stringify(result.composed, null, 2));
|
|
269
|
+
console.log();
|
|
270
|
+
if (Array.isArray(result.conflicts) && result.conflicts.length > 0) {
|
|
271
|
+
console.log(fmt.warn(` Conflicts (${result.conflicts.length}):`));
|
|
272
|
+
console.log(JSON.stringify(result.conflicts, null, 2));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
console.log(` No conflicts — draft does not weaken any floor.`);
|
|
276
|
+
}
|
|
277
|
+
console.log();
|
|
278
|
+
}
|
|
279
|
+
// ─── mnemom team admin {grant,revoke,list} — Piece 5 of T1-3.1 (ADR-046) ──
|
|
280
|
+
export async function teamAdminGrantCommand(teamId, opts) {
|
|
281
|
+
await requireAuth();
|
|
282
|
+
if (!teamId) {
|
|
283
|
+
console.log(fmt.error("Usage: mnemom team admin grant <team_id> --user <user_id>") + "\n");
|
|
284
|
+
process.exit(1);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!opts.user) {
|
|
288
|
+
console.log(fmt.error("Missing --user <user_id>") + "\n");
|
|
289
|
+
process.exit(1);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
let result;
|
|
293
|
+
try {
|
|
294
|
+
result = await grantTeamAdmin(teamId, opts.user);
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
298
|
+
console.log(fmt.error(`Failed to grant team_admin: ${msg}`) + "\n");
|
|
299
|
+
process.exit(1);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (opts.json) {
|
|
303
|
+
console.log(JSON.stringify(result, null, 2));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (result.idempotent_noop) {
|
|
307
|
+
console.log(fmt.header("Team admin grant — already active"));
|
|
308
|
+
console.log();
|
|
309
|
+
console.log(` Team ID: ${result.team_id}`);
|
|
310
|
+
console.log(` User ID: ${result.user_id}`);
|
|
311
|
+
console.log(` Granted by: ${result.granted_by}`);
|
|
312
|
+
console.log(` Granted at: ${new Date(result.granted_at).toLocaleString()}`);
|
|
313
|
+
console.log(` (no change — grant was already active)`);
|
|
314
|
+
console.log();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
console.log(fmt.header("Team admin granted"));
|
|
318
|
+
console.log();
|
|
319
|
+
console.log(` Team ID: ${result.team_id}`);
|
|
320
|
+
console.log(` User ID: ${result.user_id}`);
|
|
321
|
+
console.log(` Granted by: ${result.granted_by}`);
|
|
322
|
+
console.log(` Granted at: ${new Date(result.granted_at).toLocaleString()}`);
|
|
323
|
+
console.log();
|
|
324
|
+
}
|
|
325
|
+
export async function teamAdminRevokeCommand(teamId, opts) {
|
|
326
|
+
await requireAuth();
|
|
327
|
+
if (!teamId) {
|
|
328
|
+
console.log(fmt.error("Usage: mnemom team admin revoke <team_id> --user <user_id>") + "\n");
|
|
329
|
+
process.exit(1);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (!opts.user) {
|
|
333
|
+
console.log(fmt.error("Missing --user <user_id>") + "\n");
|
|
334
|
+
process.exit(1);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
let result;
|
|
338
|
+
try {
|
|
339
|
+
result = await revokeTeamAdmin(teamId, opts.user);
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
343
|
+
console.log(fmt.error(`Failed to revoke team_admin: ${msg}`) + "\n");
|
|
344
|
+
process.exit(1);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (opts.json) {
|
|
348
|
+
console.log(JSON.stringify(result, null, 2));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!result.revoked) {
|
|
352
|
+
console.log(fmt.header("Team admin revoke — no active grant"));
|
|
353
|
+
console.log();
|
|
354
|
+
console.log(` Team ID: ${result.team_id}`);
|
|
355
|
+
console.log(` User ID: ${result.user_id}`);
|
|
356
|
+
console.log(` (no change — no active grant existed)`);
|
|
357
|
+
console.log();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
console.log(fmt.header("Team admin revoked"));
|
|
361
|
+
console.log();
|
|
362
|
+
console.log(` Team ID: ${result.team_id}`);
|
|
363
|
+
console.log(` User ID: ${result.user_id}`);
|
|
364
|
+
if (result.revoked_at) {
|
|
365
|
+
console.log(` Revoked at: ${new Date(result.revoked_at).toLocaleString()}`);
|
|
366
|
+
}
|
|
367
|
+
if (result.revoked_by) {
|
|
368
|
+
console.log(` Revoked by: ${result.revoked_by}`);
|
|
369
|
+
}
|
|
370
|
+
console.log();
|
|
371
|
+
}
|
|
372
|
+
export async function teamAdminListCommand(teamId, opts) {
|
|
373
|
+
await requireAuth();
|
|
374
|
+
if (!teamId) {
|
|
375
|
+
console.log(fmt.error("Usage: mnemom team admin list <team_id>") + "\n");
|
|
376
|
+
process.exit(1);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
let result;
|
|
380
|
+
try {
|
|
381
|
+
result = await listTeamAdmins(teamId);
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
385
|
+
console.log(fmt.error(`Failed to list team admins: ${msg}`) + "\n");
|
|
386
|
+
process.exit(1);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (opts.json) {
|
|
390
|
+
console.log(JSON.stringify(result, null, 2));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
console.log(fmt.header(`Team admins — ${result.team_id}`));
|
|
394
|
+
console.log();
|
|
395
|
+
console.log(` Org ID: ${result.org_id}`);
|
|
396
|
+
console.log();
|
|
397
|
+
if (result.count === 0) {
|
|
398
|
+
console.log(" No team admins on this team.\n" +
|
|
399
|
+
" Org owners + admins act as implicit team admins for every team in the org\n" +
|
|
400
|
+
" (per ADR-046). Grants are needed only for non-org-admin users.\n");
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const userW = 36;
|
|
404
|
+
const grantedByW = 36;
|
|
405
|
+
const grantedAtW = 22;
|
|
406
|
+
const header = "User ID".padEnd(userW) +
|
|
407
|
+
"Granted by".padEnd(grantedByW) +
|
|
408
|
+
"Granted at".padEnd(grantedAtW);
|
|
409
|
+
console.log(` ${header}`);
|
|
410
|
+
console.log(` ${"─".repeat(userW + grantedByW + grantedAtW)}`);
|
|
411
|
+
for (const grant of result.admins) {
|
|
412
|
+
const u = grant.user_id.slice(0, userW - 2).padEnd(userW);
|
|
413
|
+
const gb = grant.granted_by.slice(0, grantedByW - 2).padEnd(grantedByW);
|
|
414
|
+
const ga = new Date(grant.granted_at).toLocaleDateString().padEnd(grantedAtW);
|
|
415
|
+
console.log(` ${u}${gb}${ga}`);
|
|
416
|
+
}
|
|
417
|
+
console.log(`\n Total: ${result.count} active grant(s)\n`);
|
|
418
|
+
}
|
|
419
|
+
async function readAllStdin() {
|
|
420
|
+
return new Promise((resolve) => {
|
|
421
|
+
const chunks = [];
|
|
422
|
+
process.stdin.on("data", (chunk) => {
|
|
423
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
424
|
+
});
|
|
425
|
+
process.stdin.on("end", () => {
|
|
426
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
427
|
+
});
|
|
428
|
+
process.stdin.on("error", () => {
|
|
429
|
+
resolve("");
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
// ─── mnemom team coverage <team_id> — Piece 6 of T1-3.1 ──────────────────
|
|
434
|
+
//
|
|
435
|
+
// Compliance-grade proof-of-coverage view: per-axis sweep summary for the
|
|
436
|
+
// last 30 days + last-swept heartbeat. Surfaces the data SOC 2 / EU AI Act
|
|
437
|
+
// control mappings need to answer "show evidence the detector ran during
|
|
438
|
+
// the audit window" — answers it from a single endpoint.
|
|
439
|
+
import { getTeamSidebandCoverage, } from "../lib/api.js";
|
|
440
|
+
import chalk from "chalk";
|
|
441
|
+
function freshnessColor(lastSweptAt) {
|
|
442
|
+
if (!lastSweptAt)
|
|
443
|
+
return chalk.dim;
|
|
444
|
+
const ageMs = Date.now() - Date.parse(lastSweptAt);
|
|
445
|
+
if (ageMs < 5 * 60_000)
|
|
446
|
+
return chalk.green; // <5 min — hot
|
|
447
|
+
if (ageMs < 60 * 60_000)
|
|
448
|
+
return chalk.yellow; // <1 hour — warm
|
|
449
|
+
return chalk.red; // ≥1 hour — stale
|
|
450
|
+
}
|
|
451
|
+
function formatSummary(s) {
|
|
452
|
+
const lastTxt = s.last_swept_at
|
|
453
|
+
? freshnessColor(s.last_swept_at)(s.last_swept_at)
|
|
454
|
+
: chalk.red("never");
|
|
455
|
+
const fireRate = s.total_swept > 0 ? `${((s.total_fired / s.total_swept) * 100).toFixed(1)}%` : "—";
|
|
456
|
+
return [
|
|
457
|
+
chalk.bold(s.axis.padEnd(12)),
|
|
458
|
+
`swept=${String(s.total_swept).padStart(5)}`,
|
|
459
|
+
`fired=${String(s.total_fired).padStart(4)}`,
|
|
460
|
+
`skipped=${String(s.total_skipped).padStart(3)}`,
|
|
461
|
+
`fire_rate=${fireRate.padStart(7)}`,
|
|
462
|
+
`last_swept=${lastTxt}`,
|
|
463
|
+
].join(" ");
|
|
464
|
+
}
|
|
465
|
+
export async function teamCoverageCommand(teamId, opts = {}) {
|
|
466
|
+
await requireAuth();
|
|
467
|
+
let result;
|
|
468
|
+
try {
|
|
469
|
+
result = await getTeamSidebandCoverage(teamId);
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
console.error(fmt.error(err instanceof Error ? err.message : String(err)) + "\n");
|
|
473
|
+
process.exit(1);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (opts.json) {
|
|
477
|
+
console.log(fmt.json(result));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
console.log(fmt.header(`Sideband coverage — team ${result.team_id}`));
|
|
481
|
+
console.log(chalk.dim(`(last ${result.window_days} days)\n`));
|
|
482
|
+
for (const s of result.summary) {
|
|
483
|
+
console.log(` ${formatSummary(s)}`);
|
|
484
|
+
}
|
|
485
|
+
if (opts.rows) {
|
|
486
|
+
console.log();
|
|
487
|
+
console.log(fmt.section("Daily rows"));
|
|
488
|
+
if (result.rows.length === 0) {
|
|
489
|
+
console.log(chalk.dim(" (no rows)"));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
console.log(chalk.dim(" DAY AXIS SWEPT FIRED SKIP LAST_SWEPT_AT OUTCOME"));
|
|
493
|
+
for (const r of result.rows) {
|
|
494
|
+
console.log([
|
|
495
|
+
" ",
|
|
496
|
+
r.day.padEnd(12),
|
|
497
|
+
r.axis.padEnd(12),
|
|
498
|
+
String(r.swept_count).padStart(5),
|
|
499
|
+
String(r.fired_count).padStart(6),
|
|
500
|
+
String(r.skipped_count).padStart(5),
|
|
501
|
+
" " + r.last_swept_at.padEnd(22),
|
|
502
|
+
r.last_outcome,
|
|
503
|
+
].join(" "));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom validate ...` commands — Safe House Hardening T1-4 (CI gate
|
|
3
|
+
* integration). Surface for engineers to check whether staging is safe
|
|
4
|
+
* to promote to production based on the most recent harness run state.
|
|
5
|
+
*
|
|
6
|
+
* mnemom validate safe-house [--against=staging] [--max-age=24h]
|
|
7
|
+
* [--lane=full|fast] [--strict] [--json]
|
|
8
|
+
*
|
|
9
|
+
* Wraps `GET /v1/admin/safe-house/harness-state` (mnemom-staff only).
|
|
10
|
+
* Returns the most-recent run per lane and renders a promotion-ready
|
|
11
|
+
* summary. With `--strict`, exits non-zero when a lane is stale OR not
|
|
12
|
+
* green — useful for shell scripts and the `mnemom/deploy` gate-guard
|
|
13
|
+
* workflow which can call the CLI rather than parse the JSON itself.
|
|
14
|
+
*/
|
|
15
|
+
interface ValidateSafeHouseOptions {
|
|
16
|
+
against?: string;
|
|
17
|
+
maxAge?: string;
|
|
18
|
+
lane?: "full" | "fast" | "both";
|
|
19
|
+
strict?: boolean;
|
|
20
|
+
json?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function validateSafeHouseCommand(opts: ValidateSafeHouseOptions): Promise<void>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom validate ...` commands — Safe House Hardening T1-4 (CI gate
|
|
3
|
+
* integration). Surface for engineers to check whether staging is safe
|
|
4
|
+
* to promote to production based on the most recent harness run state.
|
|
5
|
+
*
|
|
6
|
+
* mnemom validate safe-house [--against=staging] [--max-age=24h]
|
|
7
|
+
* [--lane=full|fast] [--strict] [--json]
|
|
8
|
+
*
|
|
9
|
+
* Wraps `GET /v1/admin/safe-house/harness-state` (mnemom-staff only).
|
|
10
|
+
* Returns the most-recent run per lane and renders a promotion-ready
|
|
11
|
+
* summary. With `--strict`, exits non-zero when a lane is stale OR not
|
|
12
|
+
* green — useful for shell scripts and the `mnemom/deploy` gate-guard
|
|
13
|
+
* workflow which can call the CLI rather than parse the JSON itself.
|
|
14
|
+
*/
|
|
15
|
+
import { fmt } from "../lib/format.js";
|
|
16
|
+
import { requireAuth } from "../lib/auth.js";
|
|
17
|
+
import { getSafeHouseHarnessState, } from "../lib/api.js";
|
|
18
|
+
/**
|
|
19
|
+
* Parse `--max-age=...` strings: 30m, 2h, 24h, 1d, etc.
|
|
20
|
+
* Returns milliseconds. Defaults to 24h.
|
|
21
|
+
*/
|
|
22
|
+
function parseMaxAge(input) {
|
|
23
|
+
const fallback = 24 * 60 * 60 * 1000;
|
|
24
|
+
if (!input)
|
|
25
|
+
return fallback;
|
|
26
|
+
const match = input.trim().match(/^(\d+)\s*(s|m|h|d)$/i);
|
|
27
|
+
if (!match) {
|
|
28
|
+
throw new Error(`Invalid --max-age value '${input}' (expected e.g. 30m, 2h, 24h, 1d).`);
|
|
29
|
+
}
|
|
30
|
+
const n = parseInt(match[1], 10);
|
|
31
|
+
const unit = match[2].toLowerCase();
|
|
32
|
+
const ms = n * { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[unit];
|
|
33
|
+
return ms;
|
|
34
|
+
}
|
|
35
|
+
function formatRelativeAge(startedAt) {
|
|
36
|
+
const ageMs = Date.now() - Date.parse(startedAt);
|
|
37
|
+
if (ageMs < 0)
|
|
38
|
+
return "in the future?";
|
|
39
|
+
const m = Math.floor(ageMs / 60_000);
|
|
40
|
+
if (m < 1)
|
|
41
|
+
return "<1m ago";
|
|
42
|
+
if (m < 60)
|
|
43
|
+
return `${m}m ago`;
|
|
44
|
+
const h = Math.floor(m / 60);
|
|
45
|
+
if (h < 24)
|
|
46
|
+
return `${h}h ${m % 60}m ago`;
|
|
47
|
+
const d = Math.floor(h / 24);
|
|
48
|
+
return `${d}d ${h % 24}h ago`;
|
|
49
|
+
}
|
|
50
|
+
function evaluateLane(lane, run, maxAgeMs) {
|
|
51
|
+
if (!run) {
|
|
52
|
+
return {
|
|
53
|
+
lane,
|
|
54
|
+
ok: false,
|
|
55
|
+
reason: "no run on record",
|
|
56
|
+
run: null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const ageMs = Date.now() - Date.parse(run.started_at);
|
|
60
|
+
if (ageMs > maxAgeMs) {
|
|
61
|
+
return { lane, ok: false, reason: "stale (older than --max-age)", run };
|
|
62
|
+
}
|
|
63
|
+
if (!run.is_complete) {
|
|
64
|
+
return { lane, ok: false, reason: "did not complete", run };
|
|
65
|
+
}
|
|
66
|
+
if (run.failed > 0) {
|
|
67
|
+
return { lane, ok: false, reason: `${run.failed} failed pair(s)`, run };
|
|
68
|
+
}
|
|
69
|
+
if (run.cac_violations > 0) {
|
|
70
|
+
return { lane, ok: false, reason: `${run.cac_violations} CAC violation(s)`, run };
|
|
71
|
+
}
|
|
72
|
+
if (run.i10_violations > 0) {
|
|
73
|
+
return { lane, ok: false, reason: `${run.i10_violations} I10 violation(s)`, run };
|
|
74
|
+
}
|
|
75
|
+
return { lane, ok: true, reason: "green within window", run };
|
|
76
|
+
}
|
|
77
|
+
function renderRun(run) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
lines.push(fmt.label("run_id:", run.run_id));
|
|
80
|
+
lines.push(fmt.label("started:", `${run.started_at} (${formatRelativeAge(run.started_at)})`));
|
|
81
|
+
lines.push(fmt.label("status:", run.is_complete ? "complete" : "in-progress / cancelled"));
|
|
82
|
+
lines.push(fmt.label("tally:", `${run.passed}/${run.total_pairs} pass · ${run.failed} fail · ${run.warned} warn · ${run.cac_violations} CAC · ${run.i10_violations} I10`));
|
|
83
|
+
lines.push(fmt.label("trigger:", `${run.trigger}${run.git_sha ? ` (sha=${run.git_sha.slice(0, 8)})` : ""}`));
|
|
84
|
+
if (run.duration_ms !== null) {
|
|
85
|
+
const sec = Math.round(run.duration_ms / 1000);
|
|
86
|
+
lines.push(fmt.label("duration:", `${sec}s`));
|
|
87
|
+
}
|
|
88
|
+
return lines.map((l) => ` ${l}`).join("\n");
|
|
89
|
+
}
|
|
90
|
+
export async function validateSafeHouseCommand(opts) {
|
|
91
|
+
await requireAuth();
|
|
92
|
+
// `--against` is informational only today (validation is always against
|
|
93
|
+
// the api root the user is authenticated to). Keep the flag for forward-
|
|
94
|
+
// compat: production gains a separate harness corpus per T1.
|
|
95
|
+
const against = opts.against ?? "staging";
|
|
96
|
+
if (against !== "staging" && against !== "production") {
|
|
97
|
+
console.log(fmt.error(`--against must be 'staging' or 'production' (got '${against}')`) + "\n");
|
|
98
|
+
process.exit(1);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let maxAgeMs;
|
|
102
|
+
try {
|
|
103
|
+
maxAgeMs = parseMaxAge(opts.maxAge);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.log(fmt.error(err instanceof Error ? err.message : String(err)) + "\n");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let state;
|
|
111
|
+
try {
|
|
112
|
+
state = await getSafeHouseHarnessState();
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
console.log(fmt.error(`Failed to fetch harness state: ${err instanceof Error ? err.message : err}`) + "\n");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (opts.json) {
|
|
120
|
+
console.log(JSON.stringify(state, null, 2));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const fullVerdict = evaluateLane("full", state.full, maxAgeMs);
|
|
124
|
+
const fastVerdict = evaluateLane("fast", state.fast, maxAgeMs);
|
|
125
|
+
const wantedLane = opts.lane ?? "both";
|
|
126
|
+
const verdicts = wantedLane === "full" ? [fullVerdict]
|
|
127
|
+
: wantedLane === "fast" ? [fastVerdict]
|
|
128
|
+
: [fullVerdict, fastVerdict];
|
|
129
|
+
console.log(fmt.header(`Safe House harness — ${against}`));
|
|
130
|
+
console.log("");
|
|
131
|
+
for (const v of verdicts) {
|
|
132
|
+
const heading = v.lane === "full" ? "Full lane (nightly)" : "Fast lane (deploy gate)";
|
|
133
|
+
console.log(` ${heading}`);
|
|
134
|
+
if (v.run) {
|
|
135
|
+
console.log(renderRun(v.run));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log(" (no run on record)");
|
|
139
|
+
}
|
|
140
|
+
const verdict = v.ok ? fmt.success(`gate: ${v.reason}`) : fmt.error(`gate: ${v.reason}`);
|
|
141
|
+
console.log(` ${verdict}\n`);
|
|
142
|
+
}
|
|
143
|
+
if (opts.strict) {
|
|
144
|
+
const allOk = verdicts.every((v) => v.ok);
|
|
145
|
+
if (!allOk) {
|
|
146
|
+
process.exit(1);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|