@switchboard-mcp/core 0.1.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.
Files changed (74) hide show
  1. package/README.md +9 -0
  2. package/dist/approvals/approval-requests.d.ts +151 -0
  3. package/dist/approvals/approval-requests.d.ts.map +1 -0
  4. package/dist/approvals/approval-requests.js +393 -0
  5. package/dist/approvals/approval-requests.js.map +1 -0
  6. package/dist/audit/audit-log.d.ts +45 -0
  7. package/dist/audit/audit-log.d.ts.map +1 -0
  8. package/dist/audit/audit-log.js +88 -0
  9. package/dist/audit/audit-log.js.map +1 -0
  10. package/dist/config/gitignore.d.ts +9 -0
  11. package/dist/config/gitignore.d.ts.map +1 -0
  12. package/dist/config/gitignore.js +90 -0
  13. package/dist/config/gitignore.js.map +1 -0
  14. package/dist/config/load-config.d.ts +28 -0
  15. package/dist/config/load-config.d.ts.map +1 -0
  16. package/dist/config/load-config.js +138 -0
  17. package/dist/config/load-config.js.map +1 -0
  18. package/dist/config/paths.d.ts +13 -0
  19. package/dist/config/paths.d.ts.map +1 -0
  20. package/dist/config/paths.js +38 -0
  21. package/dist/config/paths.js.map +1 -0
  22. package/dist/daemon/daemon-state.d.ts +44 -0
  23. package/dist/daemon/daemon-state.d.ts.map +1 -0
  24. package/dist/daemon/daemon-state.js +110 -0
  25. package/dist/daemon/daemon-state.js.map +1 -0
  26. package/dist/import/import-plan.d.ts +92 -0
  27. package/dist/import/import-plan.d.ts.map +1 -0
  28. package/dist/import/import-plan.js +677 -0
  29. package/dist/import/import-plan.js.map +1 -0
  30. package/dist/index.d.ts +18 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +18 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/install/client-config.d.ts +73 -0
  35. package/dist/install/client-config.d.ts.map +1 -0
  36. package/dist/install/client-config.js +514 -0
  37. package/dist/install/client-config.js.map +1 -0
  38. package/dist/mandates/mandates.d.ts +244 -0
  39. package/dist/mandates/mandates.d.ts.map +1 -0
  40. package/dist/mandates/mandates.js +690 -0
  41. package/dist/mandates/mandates.js.map +1 -0
  42. package/dist/namespaces/namespaces.d.ts +16 -0
  43. package/dist/namespaces/namespaces.d.ts.map +1 -0
  44. package/dist/namespaces/namespaces.js +49 -0
  45. package/dist/namespaces/namespaces.js.map +1 -0
  46. package/dist/onboarding/init-config.d.ts +19 -0
  47. package/dist/onboarding/init-config.d.ts.map +1 -0
  48. package/dist/onboarding/init-config.js +85 -0
  49. package/dist/onboarding/init-config.js.map +1 -0
  50. package/dist/providers/provider-add.d.ts +25 -0
  51. package/dist/providers/provider-add.d.ts.map +1 -0
  52. package/dist/providers/provider-add.js +179 -0
  53. package/dist/providers/provider-add.js.map +1 -0
  54. package/dist/providers/provider-templates.d.ts +96 -0
  55. package/dist/providers/provider-templates.d.ts.map +1 -0
  56. package/dist/providers/provider-templates.js +556 -0
  57. package/dist/providers/provider-templates.js.map +1 -0
  58. package/dist/scan/scan.d.ts +67 -0
  59. package/dist/scan/scan.d.ts.map +1 -0
  60. package/dist/scan/scan.js +389 -0
  61. package/dist/scan/scan.js.map +1 -0
  62. package/dist/schemas/config.d.ts +197 -0
  63. package/dist/schemas/config.d.ts.map +1 -0
  64. package/dist/schemas/config.js +133 -0
  65. package/dist/schemas/config.js.map +1 -0
  66. package/dist/secrets/secret-refs.d.ts +10 -0
  67. package/dist/secrets/secret-refs.d.ts.map +1 -0
  68. package/dist/secrets/secret-refs.js +32 -0
  69. package/dist/secrets/secret-refs.js.map +1 -0
  70. package/dist/secrets/secrets.d.ts +63 -0
  71. package/dist/secrets/secrets.d.ts.map +1 -0
  72. package/dist/secrets/secrets.js +301 -0
  73. package/dist/secrets/secrets.js.map +1 -0
  74. package/package.json +42 -0
@@ -0,0 +1,690 @@
1
+ import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import * as z from "zod";
5
+ const mandateStoreLockTimeoutMs = 5_000;
6
+ const mandateStoreStaleLockMs = 30_000;
7
+ export const mandateApprovalGateSchema = z.object({
8
+ id: z.string().min(1),
9
+ toolPattern: z.string().min(1),
10
+ reason: z.string().min(1).optional(),
11
+ risk: z.enum(["low", "medium", "high", "critical"]).optional(),
12
+ labels: z.array(z.string().min(1)).optional()
13
+ });
14
+ export const mandateHandoffStateSchema = z.enum([
15
+ "open",
16
+ "completed",
17
+ "blocked",
18
+ "cancelled"
19
+ ]);
20
+ export const mandateSchema = z.object({
21
+ version: z.literal(1),
22
+ id: z.string().min(1),
23
+ mandateUid: z.string().min(1).optional(),
24
+ task: z.string().min(1),
25
+ parentMandateId: z.string().min(1).optional(),
26
+ parentMandateUid: z.string().min(1).optional(),
27
+ delegatedBy: z.string().min(1).optional(),
28
+ delegationPath: z.array(z.string().min(1)).optional(),
29
+ delegationUids: z.array(z.string().min(1)).optional(),
30
+ maxLeaseExpiresAt: z.string().min(1).optional(),
31
+ repoPath: z.string().min(1),
32
+ worktreePath: z.string().min(1),
33
+ branch: z.string().min(1),
34
+ agentRole: z.string().min(1),
35
+ profiles: z.array(z.string().min(1)),
36
+ lease: z.string().min(1),
37
+ createdAt: z.string().min(1),
38
+ expiresAt: z.string().min(1),
39
+ allowedTools: z.array(z.string()),
40
+ deniedTools: z.array(z.string()),
41
+ approvalGates: z
42
+ .array(z.union([mandateApprovalGateSchema, z.string().min(1)]))
43
+ .transform((gates) => normalizeApprovalGates(gates)),
44
+ handoffState: mandateHandoffStateSchema,
45
+ handoffSummary: z.string().min(1).optional(),
46
+ handoffNextSteps: z.array(z.string().min(1)).optional(),
47
+ handoffArtifacts: z.array(z.string().min(1)).optional(),
48
+ handoffBy: z.string().min(1).optional(),
49
+ handoffAt: z.string().min(1).optional()
50
+ });
51
+ export const mandateStoreSchema = z.object({
52
+ version: z.literal(1),
53
+ mandates: z.array(mandateSchema)
54
+ });
55
+ export function resolveMandateStorePath(options = {}) {
56
+ const env = options.env ?? process.env;
57
+ const home = options.homeDir ?? homedir();
58
+ const stateRoot = env.XDG_STATE_HOME
59
+ ? resolve(env.XDG_STATE_HOME)
60
+ : join(home, ".local", "state");
61
+ return join(stateRoot, "switchboard", "mandates", "mandates.json");
62
+ }
63
+ export function normalizeMandateId(value) {
64
+ return value
65
+ .trim()
66
+ .toLowerCase()
67
+ .replace(/[^a-z0-9_-]+/g, "-")
68
+ .replace(/^-+|-+$/g, "")
69
+ .replace(/-{2,}/g, "-");
70
+ }
71
+ export function parseMandateLease(value) {
72
+ const match = /^([1-9]\d*)(m|h|d)$/.exec(value.trim());
73
+ if (!match) {
74
+ throw new Error("lease must use a positive duration like 30m, 2h, or 1d");
75
+ }
76
+ const amountText = match[1];
77
+ const unit = match[2];
78
+ if (!amountText || !unit) {
79
+ throw new Error("lease must use a positive duration like 30m, 2h, or 1d");
80
+ }
81
+ const amount = Number(amountText);
82
+ const unitMs = unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : 86_400_000;
83
+ return amount * unitMs;
84
+ }
85
+ export function mandateRuntimeStatus(mandate, now = new Date()) {
86
+ if (mandate.handoffState && mandate.handoffState !== "open") {
87
+ return "closed";
88
+ }
89
+ return new Date(mandate.expiresAt).getTime() > now.getTime()
90
+ ? "active"
91
+ : "expired";
92
+ }
93
+ export function evaluateMandateToolPolicy(toolName, policy) {
94
+ const allowedTools = uniqueTrimmed(policy.allowedTools ?? []);
95
+ const deniedTools = uniqueTrimmed(policy.deniedTools ?? []);
96
+ const approvalGates = normalizeApprovalGates(policy.approvalGates ?? []);
97
+ const approvedApprovalRequests = policy.approvedApprovalRequests ?? [];
98
+ if (matchesAnyToolPattern(toolName, deniedTools)) {
99
+ return {
100
+ allowed: false,
101
+ reason: `tool "${toolName}" is denied by mandate policy`
102
+ };
103
+ }
104
+ if (allowedTools.length > 0 &&
105
+ !matchesAnyToolPattern(toolName, allowedTools)) {
106
+ return {
107
+ allowed: false,
108
+ reason: `tool "${toolName}" is not allowed by mandate policy`
109
+ };
110
+ }
111
+ const approvalGate = lastMatchingApprovalGate(approvalGates, toolName);
112
+ if (approvalGate) {
113
+ const approvedRequest = approvedApprovalRequests.find((request) => request.approvalGateId === approvalGate.id && request.toolName === toolName);
114
+ if (approvedRequest) {
115
+ return (approvedRequest.id
116
+ ? { allowed: true, approvalRequestId: approvedRequest.id }
117
+ : { allowed: true });
118
+ }
119
+ return {
120
+ allowed: false,
121
+ approvalRequired: true,
122
+ approvalGate,
123
+ reason: `tool "${toolName}" requires approval by mandate gate "${approvalGate.id}"`
124
+ };
125
+ }
126
+ return { allowed: true };
127
+ }
128
+ export async function createMandate(options) {
129
+ const now = options.now ?? (() => new Date());
130
+ const createdAt = now();
131
+ const normalized = normalizeCreateMandateOptions(options, createdAt);
132
+ const path = options.path ?? resolveMandateStorePath();
133
+ return withMandateStoreLock(path, async () => {
134
+ const store = await readMandateStore({ path });
135
+ assertNoActiveDuplicate(store, normalized, createdAt);
136
+ const mandate = buildMandate(normalized, createdAt);
137
+ store.mandates.push(mandate);
138
+ await writeMandateStore(store, { path });
139
+ return withRuntimeStatus(mandate, createdAt);
140
+ });
141
+ }
142
+ export async function createChildMandate(options) {
143
+ const now = options.now ?? (() => new Date());
144
+ const createdAt = now();
145
+ const child = normalizeCreateMandateOptions(options, createdAt);
146
+ const parentId = normalizeMandateId(options.parentId);
147
+ if (!parentId) {
148
+ throw new Error("parent mandate id is required");
149
+ }
150
+ const path = options.path ?? resolveMandateStorePath();
151
+ return withMandateStoreLock(path, async () => {
152
+ const store = await readMandateStore({ path });
153
+ const parent = store.mandates.find((mandate) => mandate.id === parentId &&
154
+ mandate.repoPath === child.repoPath &&
155
+ mandateRuntimeStatus(mandate, createdAt) === "active");
156
+ if (!parent) {
157
+ throw new Error(`active parent mandate "${parentId}" was not found for ${child.repoPath}`);
158
+ }
159
+ const normalizedParent = withRuntimeStatus(parent, createdAt);
160
+ validateChildMandateScope(child, normalizedParent, createdAt);
161
+ assertNoActiveDuplicate(store, child, createdAt);
162
+ const inheritedDeniedTools = uniqueTrimmed([
163
+ ...normalizedParent.deniedTools,
164
+ ...(options.deniedTools ?? [])
165
+ ]);
166
+ const childApprovalGates = normalizeApprovalGates(options.approvalRequiredTools ?? [], normalizedParent.approvalGates.length);
167
+ const inheritedApprovalPatterns = new Set(normalizedParent.approvalGates.map((gate) => gate.toolPattern));
168
+ const duplicateApprovalGate = childApprovalGates.find((gate) => inheritedApprovalPatterns.has(gate.toolPattern));
169
+ if (duplicateApprovalGate) {
170
+ throw new Error(`child approval gate "${duplicateApprovalGate.toolPattern}" is already inherited from parent mandate "${normalizedParent.id}"; omit the duplicate gate or choose a narrower tool pattern`);
171
+ }
172
+ const approvalGates = [
173
+ ...normalizedParent.approvalGates,
174
+ ...childApprovalGates
175
+ ];
176
+ const parentDelegationPath = normalizedParent.delegationPath ?? [normalizedParent.id];
177
+ const parentUid = mandateUidFor(normalizedParent);
178
+ const parentDelegationUids = normalizedParent.delegationUids ?? [parentUid];
179
+ const mandate = buildMandate({
180
+ ...child,
181
+ allowedTools: child.allowedTools.length > 0
182
+ ? child.allowedTools
183
+ : normalizedParent.allowedTools,
184
+ deniedTools: inheritedDeniedTools,
185
+ approvalGates,
186
+ parentMandateId: normalizedParent.id,
187
+ parentMandateUid: parentUid,
188
+ delegatedBy: options.delegatedBy?.trim() || normalizedParent.id,
189
+ delegationPath: [...parentDelegationPath, child.id],
190
+ delegationUids: [...parentDelegationUids, child.mandateUid],
191
+ maxLeaseExpiresAt: normalizedParent.maxLeaseExpiresAt ?? normalizedParent.expiresAt
192
+ }, createdAt);
193
+ store.mandates.push(mandate);
194
+ await writeMandateStore(store, { path });
195
+ return withRuntimeStatus(mandate, createdAt);
196
+ });
197
+ }
198
+ export async function updateMandateHandoff(options) {
199
+ const id = normalizeMandateId(options.id);
200
+ if (!id) {
201
+ throw new Error("mandate id is required");
202
+ }
203
+ const repoPath = resolve(options.repoPath);
204
+ const now = options.now ?? (() => new Date());
205
+ const handedOffAt = now();
206
+ if (!Number.isFinite(handedOffAt.getTime())) {
207
+ throw new Error("mandate handoff time is invalid");
208
+ }
209
+ const path = options.path ?? resolveMandateStorePath();
210
+ return withMandateStoreLock(path, async () => {
211
+ const store = await readMandateStore({ path });
212
+ const mandateIndex = findLatestMandateIndex(store.mandates, id, repoPath);
213
+ if (mandateIndex === -1) {
214
+ throw new Error(`mandate "${id}" was not found for ${repoPath}`);
215
+ }
216
+ const mandate = store.mandates[mandateIndex];
217
+ if (!mandate) {
218
+ throw new Error(`mandate "${id}" was not found for ${repoPath}`);
219
+ }
220
+ const openDescendants = descendantMandates(store.mandates, mandate).filter((descendant) => descendant.handoffState === "open");
221
+ if (openDescendants.length > 0) {
222
+ throw new Error(`cannot hand off mandate "${id}" while child mandates remain open: ${openDescendants
223
+ .map((descendant) => descendant.id)
224
+ .join(", ")}`);
225
+ }
226
+ const handoffSummary = normalizeOptionalText(options.summary, "handoff summary");
227
+ const handoffNextSteps = normalizeOptionalList(options.nextSteps ?? [], "handoff next step");
228
+ const handoffArtifacts = normalizeOptionalList(options.artifacts ?? [], "handoff artifact");
229
+ const handoffBy = normalizeOptionalText(options.handoffBy, "handoff by");
230
+ const updatedMandate = {
231
+ ...mandate,
232
+ handoffState: options.state,
233
+ ...(handoffSummary ? { handoffSummary } : {}),
234
+ ...(handoffNextSteps.length > 0 ? { handoffNextSteps } : {}),
235
+ ...(handoffArtifacts.length > 0 ? { handoffArtifacts } : {}),
236
+ ...(handoffBy ? { handoffBy } : {}),
237
+ handoffAt: handedOffAt.toISOString()
238
+ };
239
+ store.mandates[mandateIndex] = updatedMandate;
240
+ await writeMandateStore(store, { path });
241
+ return withRuntimeStatus(updatedMandate, handedOffAt);
242
+ });
243
+ }
244
+ export async function renewMandate(options) {
245
+ const id = normalizeMandateId(options.id);
246
+ if (!id) {
247
+ throw new Error("mandate id is required");
248
+ }
249
+ const repoPath = resolve(options.repoPath);
250
+ const leaseMs = parseMandateLease(options.lease);
251
+ const now = options.now ?? (() => new Date());
252
+ const renewedAt = now();
253
+ if (!Number.isFinite(renewedAt.getTime())) {
254
+ throw new Error("mandate renewal time is invalid");
255
+ }
256
+ const path = options.path ?? resolveMandateStorePath();
257
+ return withMandateStoreLock(path, async () => {
258
+ const store = await readMandateStore({ path });
259
+ const mandateIndex = findLatestMandateIndex(store.mandates, id, repoPath);
260
+ if (mandateIndex === -1) {
261
+ throw new Error(`mandate "${id}" was not found for ${repoPath}`);
262
+ }
263
+ const mandate = store.mandates[mandateIndex];
264
+ if (!mandate) {
265
+ throw new Error(`mandate "${id}" was not found for ${repoPath}`);
266
+ }
267
+ if (mandate.handoffState !== "open") {
268
+ throw new Error(`mandate "${id}" is closed with handoff state "${mandate.handoffState}"`);
269
+ }
270
+ const expiresAt = new Date(renewedAt.getTime() + leaseMs).toISOString();
271
+ if (mandate.maxLeaseExpiresAt && Date.parse(expiresAt) > Date.parse(mandate.maxLeaseExpiresAt)) {
272
+ throw new Error("mandate renewal cannot outlive parent mandate lease");
273
+ }
274
+ const renewedMandate = {
275
+ ...mandate,
276
+ lease: options.lease.trim(),
277
+ expiresAt
278
+ };
279
+ store.mandates[mandateIndex] = renewedMandate;
280
+ await writeMandateStore(store, { path });
281
+ return withRuntimeStatus(renewedMandate, renewedAt);
282
+ });
283
+ }
284
+ export async function listMandates(options = {}) {
285
+ const now = options.now?.() ?? new Date();
286
+ const store = await readMandateStore(options.path ? { path: options.path } : {});
287
+ const repoPath = options.repoPath ? resolve(options.repoPath) : undefined;
288
+ const id = options.id ? normalizeMandateId(options.id) : undefined;
289
+ return store.mandates
290
+ .filter((mandate) => (repoPath ? mandate.repoPath === repoPath : true))
291
+ .filter((mandate) => (id ? mandate.id === id : true))
292
+ .map((mandate) => withRuntimeStatus(mandate, now));
293
+ }
294
+ export async function resolveActiveMandate(options) {
295
+ const id = normalizeMandateId(options.id);
296
+ if (!id) {
297
+ throw new Error("mandate id is required");
298
+ }
299
+ const mandates = await listMandates({
300
+ ...(options.path ? { path: options.path } : {}),
301
+ repoPath: options.repoPath,
302
+ id,
303
+ ...(options.now ? { now: options.now } : {})
304
+ });
305
+ if (mandates.length === 0) {
306
+ throw new Error(`mandate "${id}" was not found for ${resolve(options.repoPath)}`);
307
+ }
308
+ const mandate = mandates.find((item) => item.runtimeStatus === "active");
309
+ if (!mandate) {
310
+ const closedMandate = mandates.find((item) => item.runtimeStatus === "closed");
311
+ if (closedMandate) {
312
+ throw new Error(`mandate "${id}" is closed with handoff state "${closedMandate.handoffState}"`);
313
+ }
314
+ throw new Error(`mandate "${id}" is expired`);
315
+ }
316
+ return mandate;
317
+ }
318
+ export async function readMandateStore(options = {}) {
319
+ const path = options.path ?? resolveMandateStorePath();
320
+ const raw = await readFile(path, "utf8").catch((error) => {
321
+ if (typeof error === "object" &&
322
+ error !== null &&
323
+ "code" in error &&
324
+ error.code === "ENOENT") {
325
+ return "";
326
+ }
327
+ throw error;
328
+ });
329
+ if (!raw.trim()) {
330
+ return { version: 1, mandates: [] };
331
+ }
332
+ const parsed = JSON.parse(raw);
333
+ const result = mandateStoreSchema.safeParse(parsed);
334
+ if (!result.success) {
335
+ throw new Error(`invalid Switchboard mandate store at ${path}: ${z.prettifyError(result.error)}`);
336
+ }
337
+ return result.data;
338
+ }
339
+ async function writeMandateStore(store, options = {}) {
340
+ const path = options.path ?? resolveMandateStorePath();
341
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
342
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
343
+ await writeFile(tempPath, `${JSON.stringify(store, null, 2)}\n`, {
344
+ mode: 0o600
345
+ });
346
+ await chmod(tempPath, 0o600);
347
+ await rename(tempPath, path);
348
+ await chmod(path, 0o600);
349
+ }
350
+ async function withMandateStoreLock(path, fn) {
351
+ const lockPath = `${path}.lock`;
352
+ const startedAt = Date.now();
353
+ while (true) {
354
+ try {
355
+ await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
356
+ const handle = await open(lockPath, "wx", 0o600);
357
+ try {
358
+ await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
359
+ }
360
+ finally {
361
+ await handle.close();
362
+ }
363
+ break;
364
+ }
365
+ catch (error) {
366
+ if (!isNodeErrorCode(error, "EEXIST")) {
367
+ throw error;
368
+ }
369
+ await removeStaleLock(lockPath);
370
+ if (Date.now() - startedAt > mandateStoreLockTimeoutMs) {
371
+ throw new Error(`timed out waiting for mandate store lock at ${lockPath}`);
372
+ }
373
+ await sleep(25);
374
+ }
375
+ }
376
+ try {
377
+ return await fn();
378
+ }
379
+ finally {
380
+ await rm(lockPath, { force: true });
381
+ }
382
+ }
383
+ async function removeStaleLock(lockPath) {
384
+ const lockStat = await stat(lockPath).catch((error) => {
385
+ if (isNodeErrorCode(error, "ENOENT")) {
386
+ return undefined;
387
+ }
388
+ throw error;
389
+ });
390
+ if (lockStat && Date.now() - lockStat.mtimeMs > mandateStoreStaleLockMs) {
391
+ await rm(lockPath, { force: true });
392
+ }
393
+ }
394
+ function sleep(ms) {
395
+ return new Promise((resolveSleep) => {
396
+ setTimeout(resolveSleep, ms);
397
+ });
398
+ }
399
+ function isNodeErrorCode(error, code) {
400
+ return (typeof error === "object" &&
401
+ error !== null &&
402
+ "code" in error &&
403
+ error.code === code);
404
+ }
405
+ function withRuntimeStatus(mandate, now) {
406
+ return {
407
+ ...mandate,
408
+ runtimeStatus: mandateRuntimeStatus(mandate, now)
409
+ };
410
+ }
411
+ function createMandateUid(id, createdAt) {
412
+ return `${id}:${createdAt.toISOString()}`;
413
+ }
414
+ function mandateUidFor(mandate) {
415
+ return (mandate.mandateUid ?? createMandateUid(mandate.id, new Date(mandate.createdAt)));
416
+ }
417
+ function findLatestMandateIndex(mandates, id, repoPath) {
418
+ for (let index = mandates.length - 1; index >= 0; index -= 1) {
419
+ const mandate = mandates[index];
420
+ if (mandate?.id === id && mandate.repoPath === repoPath) {
421
+ return index;
422
+ }
423
+ }
424
+ return -1;
425
+ }
426
+ function descendantMandates(mandates, parent) {
427
+ const descendants = [];
428
+ const parentIds = new Set([parent.id]);
429
+ const parentUid = parent.mandateUid;
430
+ const parentUids = new Set(parentUid ? [parentUid] : []);
431
+ for (const mandate of mandates) {
432
+ if (mandate.repoPath !== parent.repoPath ||
433
+ (parentUid ? mandate.mandateUid === parentUid : mandate.id === parent.id)) {
434
+ continue;
435
+ }
436
+ if (parentUid) {
437
+ const delegationUids = mandate.delegationUids ?? [];
438
+ if (mandate.parentMandateUid === parentUid ||
439
+ delegationUids.some((uid) => parentUids.has(uid))) {
440
+ descendants.push(mandate);
441
+ if (mandate.mandateUid) {
442
+ parentUids.add(mandate.mandateUid);
443
+ }
444
+ }
445
+ continue;
446
+ }
447
+ const delegationPath = mandate.delegationPath ?? [];
448
+ if (mandate.parentMandateId === parent.id ||
449
+ delegationPath.some((id) => parentIds.has(id))) {
450
+ descendants.push(mandate);
451
+ parentIds.add(mandate.id);
452
+ }
453
+ }
454
+ return descendants;
455
+ }
456
+ function normalizeCreateMandateOptions(options, createdAt) {
457
+ const id = normalizeMandateId(options.task);
458
+ if (!id) {
459
+ throw new Error("mandate task must produce a non-empty id");
460
+ }
461
+ const agentRole = options.agentRole.trim();
462
+ if (!agentRole) {
463
+ throw new Error("mandate agent role is required");
464
+ }
465
+ const branch = options.branch.trim();
466
+ if (!branch) {
467
+ throw new Error("mandate branch is required");
468
+ }
469
+ const profiles = uniqueTrimmed(options.profiles);
470
+ if (profiles.length === 0) {
471
+ throw new Error("mandate requires at least one profile");
472
+ }
473
+ const leaseMs = parseMandateLease(options.lease);
474
+ if (!Number.isFinite(createdAt.getTime())) {
475
+ throw new Error("mandate creation time is invalid");
476
+ }
477
+ return {
478
+ id,
479
+ mandateUid: createMandateUid(id, createdAt),
480
+ task: options.task.trim(),
481
+ repoPath: resolve(options.repoPath),
482
+ worktreePath: resolve(options.worktreePath),
483
+ branch,
484
+ agentRole,
485
+ profiles,
486
+ lease: options.lease.trim(),
487
+ leaseMs,
488
+ allowedTools: uniqueTrimmed(options.allowedTools ?? []),
489
+ deniedTools: uniqueTrimmed(options.deniedTools ?? []),
490
+ approvalGates: normalizeApprovalGates(options.approvalRequiredTools ?? [])
491
+ };
492
+ }
493
+ function assertNoActiveDuplicate(store, mandate, now) {
494
+ const activeDuplicate = store.mandates.find((existing) => existing.id === mandate.id &&
495
+ existing.repoPath === mandate.repoPath &&
496
+ mandateRuntimeStatus(existing, now) === "active");
497
+ if (activeDuplicate) {
498
+ throw new Error(`active mandate "${mandate.id}" already exists for ${mandate.repoPath}; choose a different task name or wait for it to expire`);
499
+ }
500
+ }
501
+ function buildMandate(options, createdAt) {
502
+ return {
503
+ version: 1,
504
+ id: options.id,
505
+ mandateUid: options.mandateUid,
506
+ task: options.task,
507
+ ...(options.parentMandateId
508
+ ? { parentMandateId: options.parentMandateId }
509
+ : {}),
510
+ ...(options.parentMandateUid
511
+ ? { parentMandateUid: options.parentMandateUid }
512
+ : {}),
513
+ ...(options.delegatedBy ? { delegatedBy: options.delegatedBy } : {}),
514
+ ...(options.delegationPath ? { delegationPath: options.delegationPath } : {}),
515
+ ...(options.delegationUids ? { delegationUids: options.delegationUids } : {}),
516
+ ...(options.maxLeaseExpiresAt
517
+ ? { maxLeaseExpiresAt: options.maxLeaseExpiresAt }
518
+ : {}),
519
+ repoPath: options.repoPath,
520
+ worktreePath: options.worktreePath,
521
+ branch: options.branch,
522
+ agentRole: options.agentRole,
523
+ profiles: options.profiles,
524
+ lease: options.lease,
525
+ createdAt: createdAt.toISOString(),
526
+ expiresAt: new Date(createdAt.getTime() + options.leaseMs).toISOString(),
527
+ allowedTools: options.allowedTools,
528
+ deniedTools: options.deniedTools,
529
+ approvalGates: options.approvalGates,
530
+ handoffState: "open"
531
+ };
532
+ }
533
+ function validateChildMandateScope(child, parent, createdAt) {
534
+ if (child.repoPath !== parent.repoPath) {
535
+ throw new Error("child mandate repo must match parent mandate repo");
536
+ }
537
+ if (child.worktreePath !== parent.worktreePath) {
538
+ throw new Error("child mandate worktree must match parent mandate worktree");
539
+ }
540
+ if (child.branch !== parent.branch) {
541
+ throw new Error("child mandate branch must match parent mandate branch");
542
+ }
543
+ const parentProfiles = new Set(parent.profiles);
544
+ const missingProfiles = child.profiles.filter((profile) => !parentProfiles.has(profile));
545
+ if (missingProfiles.length > 0) {
546
+ throw new Error(`child mandate profiles exceed parent scope: ${missingProfiles.join(", ")}`);
547
+ }
548
+ const maxLeaseExpiresAt = parent.maxLeaseExpiresAt ?? parent.expiresAt;
549
+ const childExpiresAt = new Date(createdAt.getTime() + child.leaseMs);
550
+ if (childExpiresAt.getTime() > Date.parse(maxLeaseExpiresAt)) {
551
+ throw new Error("child mandate lease cannot outlive parent mandate lease");
552
+ }
553
+ if (child.allowedTools.length > 0 &&
554
+ !toolPatternsWithinParent(child.allowedTools, parent.allowedTools)) {
555
+ throw new Error("child mandate allowed tools exceed parent tool scope");
556
+ }
557
+ }
558
+ function toolPatternsWithinParent(childPatterns, parentPatterns) {
559
+ if (parentPatterns.length === 0) {
560
+ return true;
561
+ }
562
+ return childPatterns.every((childPattern) => parentPatterns.some((parentPattern) => childToolPatternWithinParent(childPattern, parentPattern)));
563
+ }
564
+ function childToolPatternWithinParent(childPattern, parentPattern) {
565
+ if (parentPattern === "*" || childPattern === parentPattern) {
566
+ return true;
567
+ }
568
+ if (parentPattern.endsWith("*")) {
569
+ const parentPrefix = parentPattern.slice(0, -1);
570
+ return childPattern.startsWith(parentPrefix);
571
+ }
572
+ return false;
573
+ }
574
+ function uniqueTrimmed(values) {
575
+ const seen = new Set();
576
+ const result = [];
577
+ for (const value of values) {
578
+ const trimmed = value.trim();
579
+ if (!trimmed || seen.has(trimmed)) {
580
+ continue;
581
+ }
582
+ seen.add(trimmed);
583
+ result.push(trimmed);
584
+ }
585
+ return result;
586
+ }
587
+ function normalizeOptionalText(value, label) {
588
+ if (value === undefined) {
589
+ return undefined;
590
+ }
591
+ const trimmed = value.trim();
592
+ if (!trimmed) {
593
+ return undefined;
594
+ }
595
+ if (hasControlCharacters(trimmed)) {
596
+ throw new Error(`${label} must not contain control characters`);
597
+ }
598
+ return trimmed;
599
+ }
600
+ function normalizeOptionalList(values, label) {
601
+ const normalized = uniqueTrimmed(values);
602
+ const invalid = normalized.find((value) => hasControlCharacters(value));
603
+ if (invalid) {
604
+ throw new Error(`${label} must not contain control characters`);
605
+ }
606
+ return normalized;
607
+ }
608
+ function normalizeApprovalGates(gates, idOffset = 0) {
609
+ const seen = new Set();
610
+ const result = [];
611
+ for (const gate of gates) {
612
+ const toolPattern = typeof gate === "string" ? gate.trim() : gate.toolPattern.trim();
613
+ if (!toolPattern || seen.has(toolPattern)) {
614
+ continue;
615
+ }
616
+ seen.add(toolPattern);
617
+ const id = typeof gate === "string" || !gate.id?.trim()
618
+ ? `gate-${idOffset + result.length + 1}`
619
+ : gate.id.trim();
620
+ const reason = typeof gate === "string" ? undefined : gate.reason?.trim() || undefined;
621
+ if (reason && hasControlCharacters(reason)) {
622
+ throw new Error("approval gate reason must not contain control characters");
623
+ }
624
+ const risk = typeof gate === "string" ? undefined : normalizeApprovalRisk(gate.risk);
625
+ const labels = typeof gate === "string" ? [] : normalizeApprovalLabels(gate.labels ?? []);
626
+ result.push({
627
+ id,
628
+ toolPattern,
629
+ ...(reason ? { reason } : {}),
630
+ ...(risk ? { risk } : {}),
631
+ ...(labels.length > 0 ? { labels } : {})
632
+ });
633
+ }
634
+ return result;
635
+ }
636
+ function lastMatchingApprovalGate(gates, toolName) {
637
+ for (let index = gates.length - 1; index >= 0; index -= 1) {
638
+ const gate = gates[index];
639
+ if (gate && toolPatternToRegExp(gate.toolPattern).test(toolName)) {
640
+ return gate;
641
+ }
642
+ }
643
+ return undefined;
644
+ }
645
+ function normalizeApprovalRisk(value) {
646
+ const risk = value?.trim().toLowerCase();
647
+ if (!risk) {
648
+ return undefined;
649
+ }
650
+ if (risk !== "low" &&
651
+ risk !== "medium" &&
652
+ risk !== "high" &&
653
+ risk !== "critical") {
654
+ throw new Error("approval gate risk must be one of: low, medium, high, critical");
655
+ }
656
+ return risk;
657
+ }
658
+ function normalizeApprovalLabels(labels) {
659
+ const seen = new Set();
660
+ const result = [];
661
+ for (const rawLabel of labels) {
662
+ const label = rawLabel.trim().toLowerCase();
663
+ if (!label || seen.has(label)) {
664
+ continue;
665
+ }
666
+ if (hasControlCharacters(label)) {
667
+ throw new Error("approval gate labels must not contain control characters");
668
+ }
669
+ if (!/^[a-z0-9][a-z0-9_.:-]*$/.test(label)) {
670
+ throw new Error("approval gate labels must use lowercase letters, digits, dots, colons, underscores, or hyphens");
671
+ }
672
+ seen.add(label);
673
+ result.push(label);
674
+ }
675
+ return result;
676
+ }
677
+ function hasControlCharacters(value) {
678
+ return [...value].some((char) => {
679
+ const codePoint = char.codePointAt(0);
680
+ return codePoint !== undefined && (codePoint < 32 || codePoint === 127);
681
+ });
682
+ }
683
+ function matchesAnyToolPattern(toolName, patterns) {
684
+ return patterns.some((pattern) => toolPatternToRegExp(pattern).test(toolName));
685
+ }
686
+ function toolPatternToRegExp(pattern) {
687
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
688
+ return new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
689
+ }
690
+ //# sourceMappingURL=mandates.js.map