macro-agent 0.1.11 → 0.2.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 (108) hide show
  1. package/dist/agent/agent-manager-v2.d.ts.map +1 -1
  2. package/dist/agent/agent-manager-v2.js +240 -7
  3. package/dist/agent/agent-manager-v2.js.map +1 -1
  4. package/dist/agent/types.d.ts +47 -0
  5. package/dist/agent/types.d.ts.map +1 -1
  6. package/dist/agent/types.js.map +1 -1
  7. package/dist/boot-v2.d.ts +33 -0
  8. package/dist/boot-v2.d.ts.map +1 -1
  9. package/dist/boot-v2.js +142 -11
  10. package/dist/boot-v2.js.map +1 -1
  11. package/dist/cli/inbox-mcp-proxy.d.ts +36 -0
  12. package/dist/cli/inbox-mcp-proxy.d.ts.map +1 -0
  13. package/dist/cli/inbox-mcp-proxy.js +51 -0
  14. package/dist/cli/inbox-mcp-proxy.js.map +1 -0
  15. package/dist/dispatch/loadout-translation.d.ts +100 -0
  16. package/dist/dispatch/loadout-translation.d.ts.map +1 -0
  17. package/dist/dispatch/loadout-translation.js +90 -0
  18. package/dist/dispatch/loadout-translation.js.map +1 -0
  19. package/dist/dispatch/mail-inbound-consumer.d.ts +89 -0
  20. package/dist/dispatch/mail-inbound-consumer.d.ts.map +1 -0
  21. package/dist/dispatch/mail-inbound-consumer.js +261 -0
  22. package/dist/dispatch/mail-inbound-consumer.js.map +1 -0
  23. package/dist/dispatch/mail-inbound-reuse-consumer.d.ts +75 -0
  24. package/dist/dispatch/mail-inbound-reuse-consumer.d.ts.map +1 -0
  25. package/dist/dispatch/mail-inbound-reuse-consumer.js +325 -0
  26. package/dist/dispatch/mail-inbound-reuse-consumer.js.map +1 -0
  27. package/dist/dispatch/permission-evaluator.d.ts +68 -0
  28. package/dist/dispatch/permission-evaluator.d.ts.map +1 -0
  29. package/dist/dispatch/permission-evaluator.js +159 -0
  30. package/dist/dispatch/permission-evaluator.js.map +1 -0
  31. package/dist/dispatch/permission-overlay.d.ts +64 -0
  32. package/dist/dispatch/permission-overlay.d.ts.map +1 -0
  33. package/dist/dispatch/permission-overlay.js +72 -0
  34. package/dist/dispatch/permission-overlay.js.map +1 -0
  35. package/dist/dispatch/permissions-handler.d.ts +71 -0
  36. package/dist/dispatch/permissions-handler.d.ts.map +1 -0
  37. package/dist/dispatch/permissions-handler.js +83 -0
  38. package/dist/dispatch/permissions-handler.js.map +1 -0
  39. package/dist/dispatch/spawn-agent-handler.d.ts +84 -0
  40. package/dist/dispatch/spawn-agent-handler.d.ts.map +1 -0
  41. package/dist/dispatch/spawn-agent-handler.js +85 -0
  42. package/dist/dispatch/spawn-agent-handler.js.map +1 -0
  43. package/dist/lifecycle/handlers-v2.d.ts +7 -0
  44. package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
  45. package/dist/lifecycle/handlers-v2.js +27 -0
  46. package/dist/lifecycle/handlers-v2.js.map +1 -1
  47. package/dist/map/lifecycle-bridge.d.ts +18 -0
  48. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  49. package/dist/map/lifecycle-bridge.js +23 -1
  50. package/dist/map/lifecycle-bridge.js.map +1 -1
  51. package/dist/map/mail-bridge.d.ts +55 -0
  52. package/dist/map/mail-bridge.d.ts.map +1 -0
  53. package/dist/map/mail-bridge.js +115 -0
  54. package/dist/map/mail-bridge.js.map +1 -0
  55. package/dist/map/sidecar.d.ts.map +1 -1
  56. package/dist/map/sidecar.js +245 -1
  57. package/dist/map/sidecar.js.map +1 -1
  58. package/dist/map/types.d.ts +15 -0
  59. package/dist/map/types.d.ts.map +1 -1
  60. package/dist/mcp/tools/done-v2.d.ts.map +1 -1
  61. package/dist/mcp/tools/done-v2.js +1 -0
  62. package/dist/mcp/tools/done-v2.js.map +1 -1
  63. package/dist/teams/seed-defaults.d.ts.map +1 -1
  64. package/dist/teams/seed-defaults.js +6 -2
  65. package/dist/teams/seed-defaults.js.map +1 -1
  66. package/dist/teams/team-loader.d.ts.map +1 -1
  67. package/dist/teams/team-loader.js +17 -1
  68. package/dist/teams/team-loader.js.map +1 -1
  69. package/dist/teams/team-runtime-v2.d.ts.map +1 -1
  70. package/dist/teams/team-runtime-v2.js +2 -0
  71. package/dist/teams/team-runtime-v2.js.map +1 -1
  72. package/package.json +6 -6
  73. package/src/agent/__tests__/agent-manager-v2.permission-interception.test.ts +296 -0
  74. package/src/agent/__tests__/agent-manager-v2.permissions.test.ts +233 -0
  75. package/src/agent/agent-manager-v2.ts +268 -8
  76. package/src/agent/types.ts +51 -0
  77. package/src/boot-v2.ts +190 -12
  78. package/src/cli/inbox-mcp-proxy.ts +56 -0
  79. package/src/dispatch/CLAUDE.md +129 -0
  80. package/src/dispatch/__tests__/loadout-translation.test.ts +141 -0
  81. package/src/dispatch/__tests__/mail-inbound-consumer.integration.test.ts +519 -0
  82. package/src/dispatch/__tests__/mail-inbound-consumer.test.ts +589 -0
  83. package/src/dispatch/__tests__/mail-inbound-reuse-consumer.test.ts +575 -0
  84. package/src/dispatch/__tests__/permission-evaluator.test.ts +196 -0
  85. package/src/dispatch/__tests__/permission-overlay.test.ts +56 -0
  86. package/src/dispatch/__tests__/permissions-handler.test.ts +168 -0
  87. package/src/dispatch/__tests__/spawn-agent-handler.test.ts +282 -0
  88. package/src/dispatch/loadout-translation.ts +138 -0
  89. package/src/dispatch/mail-inbound-consumer.ts +397 -0
  90. package/src/dispatch/mail-inbound-reuse-consumer.ts +479 -0
  91. package/src/dispatch/permission-evaluator.ts +191 -0
  92. package/src/dispatch/permission-overlay.ts +89 -0
  93. package/src/dispatch/permissions-handler.ts +112 -0
  94. package/src/dispatch/spawn-agent-handler.ts +160 -0
  95. package/src/lifecycle/handlers-v2.ts +34 -0
  96. package/src/map/__tests__/lifecycle-bridge.test.ts +64 -0
  97. package/src/map/__tests__/mail-bridge.test.ts +196 -0
  98. package/src/map/lifecycle-bridge.ts +48 -2
  99. package/src/map/mail-bridge.ts +203 -0
  100. package/src/map/sidecar.ts +346 -1
  101. package/src/map/types.ts +21 -0
  102. package/src/mcp/tools/done-v2.ts +1 -0
  103. package/src/teams/seed-defaults.ts +6 -2
  104. package/src/teams/team-loader.ts +21 -2
  105. package/src/teams/team-runtime-v2.ts +2 -0
  106. package/src/workspace/__tests__/self-driving-yaml.test.ts +10 -2
  107. package/templates/teams/self-driving/team.yaml +142 -0
  108. package/tsconfig.json +2 -1
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Mail-Inbound Reuse Consumer
3
+ *
4
+ * Receives hub-driven `x-dispatch/work` envelopes addressed to **non-sidecar**
5
+ * agents — long-lived team workers, coordinators, etc. — and drives them
6
+ * through the dispatch turn using their existing session, then posts the
7
+ * summary back as a mail turn.
8
+ *
9
+ * Mirrors `mail-inbound-consumer.ts` but with three semantic differences:
10
+ *
11
+ * 1. Filters envelopes addressed to ANY non-sidecar agent (the existing
12
+ * consumer filters for the dispatcher recipient).
13
+ * 2. Does **not** spawn — it drives the existing agent's session via
14
+ * `agentManager.prompt(agentId, prompt)` and watches for `done()` in
15
+ * the update stream.
16
+ * 3. Tracks `inflightDispatches` per agentId. A second envelope arriving
17
+ * while the same agent is already processing a dispatch is rejected
18
+ * with `recipient_busy` so the orchestrator can retry against another
19
+ * agent (or fall back to fresh-spawn). Reject is **dispatch-scoped**
20
+ * — non-dispatch work on the agent (peer messages, user chat) does
21
+ * NOT trigger the busy reject; that work stacks naturally.
22
+ *
23
+ * Reply path: captures `args.summary` from the done() tool call's rawInput
24
+ * directly off the update stream, so it works for both parented and
25
+ * parentless target agents (the parented branch in `handlers-v2` does NOT
26
+ * stash `_lastSummary` — only parentless agents do — but we don't need
27
+ * that path because we observe done() in-stream).
28
+ *
29
+ * @module dispatch/mail-inbound-reuse-consumer
30
+ */
31
+ import { collapsePermissionsForAutonomous, } from "./loadout-translation.js";
32
+ import { setPermissionOverlay, clearPermissionOverlay, } from "./permission-overlay.js";
33
+ const SEEN_TASK_TTL_MS = 60 * 60 * 1000;
34
+ /**
35
+ * Wire the mail-inbound reuse consumer.
36
+ *
37
+ * Returns a `stop()` handle that detaches the inbox listener.
38
+ */
39
+ export function createMailInboundReuseConsumer(opts) {
40
+ const { dispatcherAgentId, inboxEvents, agentManager, agentStore, getSidecar, log = (msg) => console.log(msg), } = opts;
41
+ // agentId → inflight dispatch state. Used both to gate concurrent
42
+ // dispatches against the same agent and to look up the conversation
43
+ // when posting the reply.
44
+ const inflightDispatches = new Map();
45
+ // taskId → expiresAt: idempotency guard mirroring mail-inbound-consumer.
46
+ const seenTaskIds = new Map();
47
+ function pruneSeenTaskIds() {
48
+ const now = Date.now();
49
+ for (const [id, expiresAt] of seenTaskIds) {
50
+ if (expiresAt <= now)
51
+ seenTaskIds.delete(id);
52
+ }
53
+ }
54
+ let droppedMalformedCount = 0;
55
+ let busyRejectCount = 0;
56
+ log(`[mail-inbound-reuse] Consumer ready — listening for x-dispatch/work envelopes ` +
57
+ `addressed to non-sidecar agents (sidecar=${dispatcherAgentId})`);
58
+ const onMessage = (event) => {
59
+ // Only handle envelopes addressed to NON-sidecar agents. Sidecar
60
+ // envelopes are owned by mail-inbound-consumer (fresh-spawn).
61
+ if (event.agentId === dispatcherAgentId)
62
+ return;
63
+ const content = event.message?.content;
64
+ if (content?.schema !== "x-dispatch/work")
65
+ return;
66
+ const data = content.data;
67
+ if (!data?.taskId) {
68
+ droppedMalformedCount++;
69
+ log(`[mail-inbound-reuse] Dropping malformed envelope (no taskId, total=${droppedMalformedCount})`);
70
+ return;
71
+ }
72
+ const taskId = data.taskId;
73
+ pruneSeenTaskIds();
74
+ const seenExpiresAt = seenTaskIds.get(taskId);
75
+ if (seenExpiresAt !== undefined && seenExpiresAt > Date.now()) {
76
+ // Re-delivery within dedup window — silently drop.
77
+ return;
78
+ }
79
+ seenTaskIds.set(taskId, Date.now() + SEEN_TASK_TTL_MS);
80
+ const targetAgentId = event.agentId;
81
+ const conversationId = content._conversationId ?? null;
82
+ const prompt = data.prompt ?? data.content ?? "";
83
+ // Resolve target — must be a known, non-stopped agent.
84
+ const targetRecord = agentStore.getAgent(targetAgentId);
85
+ if (!targetRecord) {
86
+ log(`[mail-inbound-reuse] Unknown target agent ${targetAgentId} for taskId=${taskId} — dropping`);
87
+ void postReplyTurn(conversationId, targetAgentId, {
88
+ status: "agent_unavailable",
89
+ reason: `Agent ${targetAgentId} not registered on this swarm`,
90
+ });
91
+ return;
92
+ }
93
+ if (targetRecord.state === "stopped" || targetRecord.state === "failed") {
94
+ log(`[mail-inbound-reuse] Target agent ${targetAgentId} state=${targetRecord.state} — dropping taskId=${taskId}`);
95
+ void postReplyTurn(conversationId, targetAgentId, {
96
+ status: "agent_unavailable",
97
+ reason: `Agent ${targetAgentId} state=${targetRecord.state}`,
98
+ });
99
+ return;
100
+ }
101
+ // In-flight check — only reject when this same agent is already
102
+ // processing another tracked dispatch. Non-dispatch work (peer chat,
103
+ // user prompts) does not block; promptUntilDone-style serial stacking
104
+ // handles that.
105
+ const existing = inflightDispatches.get(targetAgentId);
106
+ if (existing) {
107
+ busyRejectCount++;
108
+ log(`[mail-inbound-reuse] recipient_busy — agent=${targetAgentId} already processing ` +
109
+ `dispatch=${existing.dispatchId}; rejecting taskId=${taskId}`);
110
+ void postReplyTurn(conversationId, targetAgentId, {
111
+ status: "recipient_busy",
112
+ reason: `Agent ${targetAgentId} is processing dispatch ${existing.dispatchId}`,
113
+ });
114
+ return;
115
+ }
116
+ log(`[mail-inbound-reuse] Driving dispatch taskId=${taskId} on existing agent=${targetAgentId} ` +
117
+ `conv=${conversationId ?? "(none)"}`);
118
+ inflightDispatches.set(targetAgentId, {
119
+ dispatchId: taskId,
120
+ conversationId,
121
+ startedAt: Date.now(),
122
+ });
123
+ // Drive the agent's existing session via raw `prompt()` rather than
124
+ // `promptUntilDone` because the latter auto-terminates the agent on
125
+ // done() — fatal for long-lived workers we want to reuse. We watch
126
+ // the update stream ourselves for the done() tool call and capture
127
+ // the summary inline.
128
+ void driveDispatch(targetAgentId, taskId, prompt, conversationId, data.loadout).finally(() => {
129
+ inflightDispatches.delete(targetAgentId);
130
+ // Always clear the permission overlay, even if driveDispatch
131
+ // didn't set one — keeps the registry tidy and defends against
132
+ // a future code path that sets one but skips its own cleanup.
133
+ clearPermissionOverlay(targetAgentId);
134
+ });
135
+ };
136
+ async function driveDispatch(targetAgentId, taskId, prompt, conversationId, loadout) {
137
+ // Apply the dispatch's loadout permissions as a runtime overlay
138
+ // for the duration of this prompt drive. The PreToolUse hook
139
+ // installed at spawn-time consults the overlay registry per tool
140
+ // call and denies calls that match the loadout's deny rules.
141
+ // `fullAutonomous: true` because mail-inbound workers have no
142
+ // human in the loop — `ask` rules collapse to `allow`. Cleared
143
+ // unconditionally in `finally` so a crash mid-prompt doesn't
144
+ // leave a stale overlay on the agent.
145
+ const overlay = collapsePermissionsForAutonomous(loadout?.permissions,
146
+ /* fullAutonomous */ true);
147
+ if (overlay) {
148
+ setPermissionOverlay(targetAgentId, overlay);
149
+ log(`[mail-inbound-reuse] Applied permission overlay for agent=${targetAgentId} ` +
150
+ `taskId=${taskId} (deny=${overlay.deny.length} allow=${overlay.allow.length})`);
151
+ }
152
+ let summary;
153
+ let status;
154
+ let doneSeen = false;
155
+ let promptError;
156
+ try {
157
+ for await (const update of agentManager.prompt(targetAgentId, prompt)) {
158
+ const captured = captureDoneCall(update);
159
+ if (captured) {
160
+ doneSeen = true;
161
+ if (captured.summary)
162
+ summary = captured.summary;
163
+ if (captured.status)
164
+ status = captured.status;
165
+ }
166
+ }
167
+ }
168
+ catch (err) {
169
+ promptError = err;
170
+ log(`[mail-inbound-reuse] prompt() threw for agent=${targetAgentId} taskId=${taskId}: ` +
171
+ `${promptError.message ?? String(promptError)}`);
172
+ }
173
+ // Fallback: read `_lastSummary` from agentStore. The done() handler
174
+ // persists this for in-flight agents (Phase 2C) so the reply path
175
+ // is reliable even when the prompt iterator's update stream raced
176
+ // the ACP connection close. Covers:
177
+ // - prompt() threw before yielding the done() update (catch above)
178
+ // - inline capture saw done() but `args.summary` was empty
179
+ // - iterator yielded but our captureDoneCall missed (shape drift)
180
+ if (!summary) {
181
+ try {
182
+ const record = agentStore.getAgent(targetAgentId);
183
+ const fallback = record?.metadata?._lastSummary;
184
+ if (typeof fallback === "string" && fallback.length > 0) {
185
+ summary = fallback;
186
+ doneSeen = true;
187
+ log(`[mail-inbound-reuse] Recovered summary from _lastSummary fallback for agent=${targetAgentId} taskId=${taskId}`);
188
+ }
189
+ }
190
+ catch {
191
+ /* best effort — store may be closing during shutdown */
192
+ }
193
+ }
194
+ // Post reply: prefer real summary, fall back to status notes when
195
+ // we genuinely have nothing.
196
+ if (summary) {
197
+ void postReplyTurn(conversationId, targetAgentId, summary).then(() => {
198
+ // Clear the persisted summary so it doesn't replay if the same
199
+ // agentId is dispatched again. Best-effort.
200
+ try {
201
+ const existing = agentStore.getAgent(targetAgentId)?.metadata ?? {};
202
+ const { _lastSummary: _drop, ...rest } = existing;
203
+ void _drop;
204
+ agentStore.updateAgent(targetAgentId, { metadata: rest });
205
+ }
206
+ catch {
207
+ /* best effort */
208
+ }
209
+ });
210
+ return;
211
+ }
212
+ if (promptError) {
213
+ void postReplyTurn(conversationId, targetAgentId, {
214
+ status: "failed",
215
+ reason: `Prompt failed: ${promptError.message ?? String(promptError)}`,
216
+ });
217
+ return;
218
+ }
219
+ if (!doneSeen) {
220
+ log(`[mail-inbound-reuse] Agent ${targetAgentId} finished prompt without calling done() ` +
221
+ `for taskId=${taskId} — posting "incomplete" reply`);
222
+ void postReplyTurn(conversationId, targetAgentId, {
223
+ status: "incomplete",
224
+ reason: "Agent did not call done() within the prompt cycle",
225
+ });
226
+ return;
227
+ }
228
+ void postReplyTurn(conversationId, targetAgentId, `Dispatch ${taskId} ${status ?? "completed"} (no summary)`);
229
+ }
230
+ /**
231
+ * Detect a `done()` tool-call update and extract `{ status, summary }`
232
+ * from rawInput. Mirrors `promptUntilDone`'s detection logic but also
233
+ * captures `summary` (which the AgentManager's loop discards).
234
+ */
235
+ function captureDoneCall(update) {
236
+ const u = update;
237
+ const sessionUpdate = u.sessionUpdate;
238
+ const title = u.title;
239
+ const isDoneToolCall = (sessionUpdate === "tool_call" || sessionUpdate === "tool_call_update") &&
240
+ typeof title === "string" &&
241
+ title.endsWith("__done");
242
+ if (!isDoneToolCall) {
243
+ // Older fallback shape.
244
+ if (u.type === "result" &&
245
+ u.subtype === "tool_result" &&
246
+ u.toolName === "done") {
247
+ const result = u.result;
248
+ if (result) {
249
+ return { status: result.status, summary: result.summary };
250
+ }
251
+ }
252
+ return null;
253
+ }
254
+ let input;
255
+ try {
256
+ const raw = u.rawInput;
257
+ if (typeof raw === "string") {
258
+ input = JSON.parse(raw);
259
+ }
260
+ else if (raw && typeof raw === "object") {
261
+ input = raw;
262
+ }
263
+ else if (u.input && typeof u.input === "object") {
264
+ input = u.input;
265
+ }
266
+ }
267
+ catch {
268
+ // rawInput not yet parseable (multi-update tool call); ignore.
269
+ }
270
+ if (!input)
271
+ return null;
272
+ return { status: input.status, summary: input.summary };
273
+ }
274
+ async function postReplyTurn(conversationId, fromAgentId, content) {
275
+ if (!conversationId) {
276
+ log(`[mail-inbound-reuse] No conversationId — reply for ${fromAgentId} dropped: ` +
277
+ `${typeof content === "string" ? content.slice(0, 80) : content.status}`);
278
+ return;
279
+ }
280
+ const sidecar = getSidecar();
281
+ if (!sidecar?.postMailTurn) {
282
+ log(`[mail-inbound-reuse] No sidecar/postMailTurn — reply turn dropped`);
283
+ return;
284
+ }
285
+ const body = typeof content === "string" ? content : JSON.stringify(content);
286
+ try {
287
+ await sidecar.postMailTurn(conversationId, fromAgentId, body);
288
+ }
289
+ catch (err) {
290
+ log(`[mail-inbound-reuse] postMailTurn failed for ${fromAgentId}: ` +
291
+ `${err.message ?? String(err)}`);
292
+ }
293
+ }
294
+ inboxEvents.on("inbox.message", onMessage);
295
+ let stopped = false;
296
+ return {
297
+ stop() {
298
+ if (stopped)
299
+ return;
300
+ stopped = true;
301
+ try {
302
+ if (inboxEvents.off) {
303
+ inboxEvents.off("inbox.message", onMessage);
304
+ }
305
+ else if (inboxEvents.removeListener) {
306
+ inboxEvents.removeListener("inbox.message", onMessage);
307
+ }
308
+ }
309
+ catch {
310
+ // best effort
311
+ }
312
+ log(`[mail-inbound-reuse] Consumer stopped`);
313
+ },
314
+ stats() {
315
+ pruneSeenTaskIds();
316
+ return {
317
+ droppedMalformed: droppedMalformedCount,
318
+ seenTaskIds: seenTaskIds.size,
319
+ busyRejects: busyRejectCount,
320
+ inflightCount: inflightDispatches.size,
321
+ };
322
+ },
323
+ };
324
+ }
325
+ //# sourceMappingURL=mail-inbound-reuse-consumer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mail-inbound-reuse-consumer.js","sourceRoot":"","sources":["../../src/dispatch/mail-inbound-reuse-consumer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Permission Evaluator
3
+ *
4
+ * Pure function: given a tool call (name + input) and an overlay's
5
+ * permission rules, decide whether to deny, allow, or pass-through.
6
+ * Used by the `PreToolUse` hook installed at spawn time to enforce
7
+ * dispatch-supplied loadout permissions on a running session.
8
+ *
9
+ * Rule format (matches Claude Agent SDK convention):
10
+ *
11
+ * <ToolName> — match any call to this tool
12
+ * <ToolName>(<glob-pattern>) — match calls whose primary input
13
+ * field matches the glob pattern
14
+ *
15
+ * The "primary input field" is tool-specific:
16
+ *
17
+ * Bash → input.command
18
+ * Read → input.file_path
19
+ * Write → input.file_path
20
+ * Edit → input.file_path
21
+ * Grep → input.pattern
22
+ * <other> → no field-level match; rule must be bare `<ToolName>`
23
+ *
24
+ * Glob: `*` matches any sequence of characters (no path-segment
25
+ * distinction). Other regex specials are escaped.
26
+ *
27
+ * Decision precedence:
28
+ *
29
+ * 1. If any rule in `deny` matches → 'deny'
30
+ * 2. Else if any rule in `allow` matches → 'allow'
31
+ * 3. Else → 'pass-through' (let the session's static rules decide)
32
+ *
33
+ * `ask` rules are not evaluated here — the consumer is expected to
34
+ * collapse `ask` to either `allow` or `deny` before setting the
35
+ * overlay (via `collapsePermissionsForAutonomous` based on the
36
+ * spawn's `fullAutonomous` flag). The evaluator sees only collapsed
37
+ * `allow` and `deny` lists.
38
+ *
39
+ * @module dispatch/permission-evaluator
40
+ */
41
+ import type { OverlayPermissions } from "./permission-overlay.js";
42
+ export interface PermissionDecision {
43
+ decision: "allow" | "deny" | "pass-through";
44
+ matchedRule?: string;
45
+ matchedField?: string;
46
+ }
47
+ /**
48
+ * Evaluate a single tool call against an overlay's permission rules.
49
+ *
50
+ * Returns `'pass-through'` (the default) when no rule matches — the
51
+ * caller should fall back to the session's static permission rules
52
+ * for the final decision.
53
+ */
54
+ export declare function evaluatePermission(toolName: string, toolInput: unknown, overlay: OverlayPermissions): PermissionDecision;
55
+ interface RuleMatch {
56
+ matched: boolean;
57
+ field?: string;
58
+ }
59
+ /**
60
+ * Test a single rule against a tool call. Returns whether the rule
61
+ * matched and which input field (if any) was tested.
62
+ *
63
+ * Exported only for testability; production callers should use
64
+ * `evaluatePermission`.
65
+ */
66
+ export declare function matchRule(rule: string, toolName: string, toolInput: unknown): RuleMatch;
67
+ export {};
68
+ //# sourceMappingURL=permission-evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-evaluator.d.ts","sourceRoot":"","sources":["../../src/dispatch/permission-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAElE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,cAAc,CAAC;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAmBD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,kBAAkB,GAC1B,kBAAkB,CAyBpB;AAED,UAAU,SAAS;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,OAAO,GACjB,SAAS,CA4BX"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Permission Evaluator
3
+ *
4
+ * Pure function: given a tool call (name + input) and an overlay's
5
+ * permission rules, decide whether to deny, allow, or pass-through.
6
+ * Used by the `PreToolUse` hook installed at spawn time to enforce
7
+ * dispatch-supplied loadout permissions on a running session.
8
+ *
9
+ * Rule format (matches Claude Agent SDK convention):
10
+ *
11
+ * <ToolName> — match any call to this tool
12
+ * <ToolName>(<glob-pattern>) — match calls whose primary input
13
+ * field matches the glob pattern
14
+ *
15
+ * The "primary input field" is tool-specific:
16
+ *
17
+ * Bash → input.command
18
+ * Read → input.file_path
19
+ * Write → input.file_path
20
+ * Edit → input.file_path
21
+ * Grep → input.pattern
22
+ * <other> → no field-level match; rule must be bare `<ToolName>`
23
+ *
24
+ * Glob: `*` matches any sequence of characters (no path-segment
25
+ * distinction). Other regex specials are escaped.
26
+ *
27
+ * Decision precedence:
28
+ *
29
+ * 1. If any rule in `deny` matches → 'deny'
30
+ * 2. Else if any rule in `allow` matches → 'allow'
31
+ * 3. Else → 'pass-through' (let the session's static rules decide)
32
+ *
33
+ * `ask` rules are not evaluated here — the consumer is expected to
34
+ * collapse `ask` to either `allow` or `deny` before setting the
35
+ * overlay (via `collapsePermissionsForAutonomous` based on the
36
+ * spawn's `fullAutonomous` flag). The evaluator sees only collapsed
37
+ * `allow` and `deny` lists.
38
+ *
39
+ * @module dispatch/permission-evaluator
40
+ */
41
+ /**
42
+ * Tool-name → primary input field name. Add entries as needed for
43
+ * additional tools. Tools not listed have no field-level matching;
44
+ * only bare `<ToolName>` rules apply.
45
+ */
46
+ const PRIMARY_INPUT_FIELD = {
47
+ Bash: "command",
48
+ Read: "file_path",
49
+ Write: "file_path",
50
+ Edit: "file_path",
51
+ MultiEdit: "file_path",
52
+ Grep: "pattern",
53
+ Glob: "pattern",
54
+ NotebookRead: "notebook_path",
55
+ NotebookEdit: "notebook_path",
56
+ };
57
+ /**
58
+ * Evaluate a single tool call against an overlay's permission rules.
59
+ *
60
+ * Returns `'pass-through'` (the default) when no rule matches — the
61
+ * caller should fall back to the session's static permission rules
62
+ * for the final decision.
63
+ */
64
+ export function evaluatePermission(toolName, toolInput, overlay) {
65
+ // Deny rules win over allow.
66
+ for (const rule of overlay.deny ?? []) {
67
+ const match = matchRule(rule, toolName, toolInput);
68
+ if (match.matched) {
69
+ return {
70
+ decision: "deny",
71
+ matchedRule: rule,
72
+ ...(match.field ? { matchedField: match.field } : {}),
73
+ };
74
+ }
75
+ }
76
+ for (const rule of overlay.allow ?? []) {
77
+ const match = matchRule(rule, toolName, toolInput);
78
+ if (match.matched) {
79
+ return {
80
+ decision: "allow",
81
+ matchedRule: rule,
82
+ ...(match.field ? { matchedField: match.field } : {}),
83
+ };
84
+ }
85
+ }
86
+ return { decision: "pass-through" };
87
+ }
88
+ /**
89
+ * Test a single rule against a tool call. Returns whether the rule
90
+ * matched and which input field (if any) was tested.
91
+ *
92
+ * Exported only for testability; production callers should use
93
+ * `evaluatePermission`.
94
+ */
95
+ export function matchRule(rule, toolName, toolInput) {
96
+ const parsed = parseRule(rule);
97
+ if (!parsed)
98
+ return { matched: false };
99
+ if (parsed.toolName !== toolName)
100
+ return { matched: false };
101
+ // No pattern → any call to this tool matches.
102
+ if (parsed.pattern === undefined)
103
+ return { matched: true };
104
+ // Empty pattern (`Bash()`) — also any call to this tool. Conservative.
105
+ if (parsed.pattern === "")
106
+ return { matched: true };
107
+ const fieldName = PRIMARY_INPUT_FIELD[toolName];
108
+ if (!fieldName) {
109
+ // No primary field defined for this tool → can't match a pattern.
110
+ // Pattern-bearing rules for unknown tools never match (skip).
111
+ return { matched: false };
112
+ }
113
+ const fieldValue = readField(toolInput, fieldName);
114
+ if (typeof fieldValue !== "string") {
115
+ return { matched: false };
116
+ }
117
+ const re = globToRegex(parsed.pattern);
118
+ return {
119
+ matched: re.test(fieldValue),
120
+ field: fieldName,
121
+ };
122
+ }
123
+ function parseRule(rule) {
124
+ // Tool names allow letters/digits/underscores AND hyphens — MCP tools use
125
+ // hyphens in their server prefix (e.g., `mcp__agent-inbox__list_agents`).
126
+ const m = rule.match(/^([A-Za-z_][A-Za-z0-9_-]*)(?:\((.*)\))?$/);
127
+ if (!m)
128
+ return null;
129
+ const [, toolName, pattern] = m;
130
+ if (pattern === undefined) {
131
+ return { toolName: toolName };
132
+ }
133
+ return { toolName: toolName, pattern };
134
+ }
135
+ function readField(input, field) {
136
+ if (!input || typeof input !== "object")
137
+ return undefined;
138
+ return input[field];
139
+ }
140
+ /**
141
+ * Convert a Claude permission glob into a regex. Only `*` is special;
142
+ * everything else is treated as a literal. The result is anchored
143
+ * (`^...$`) for whole-string matching.
144
+ */
145
+ function globToRegex(pattern) {
146
+ let out = "^";
147
+ for (const ch of pattern) {
148
+ if (ch === "*") {
149
+ out += ".*";
150
+ }
151
+ else {
152
+ // Escape regex specials.
153
+ out += ch.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
154
+ }
155
+ }
156
+ out += "$";
157
+ return new RegExp(out);
158
+ }
159
+ //# sourceMappingURL=permission-evaluator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-evaluator.js","sourceRoot":"","sources":["../../src/dispatch/permission-evaluator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Permission Overlay Registry
3
+ *
4
+ * Process-singleton map of `agentId → Permissions` for in-flight
5
+ * dispatches. The `PreToolUse` hook installed at spawn time consults
6
+ * this registry on every tool call. Dispatch consumers (e.g., the
7
+ * mail-inbound-reuse-consumer) set the overlay when claiming an
8
+ * in-flight dispatch and clear it on resolution.
9
+ *
10
+ * Why a registry rather than a per-spawn argument:
11
+ * - The session is created with permissive rules at boot. The
12
+ * dispatch's narrower deny rules need to apply at *call time*, not
13
+ * at spawn time, because the agent already exists when the dispatch
14
+ * arrives. The hook closure captures `agentId` and reads the
15
+ * registry per-call so updates propagate without recreating the
16
+ * session.
17
+ *
18
+ * Semantics:
19
+ * - Intersection-only: the overlay can ADD denies to a session but
20
+ * cannot grant new allows. If the session was spawned with broad
21
+ * permissions and the overlay says `allow: [Read]`, the session's
22
+ * other tools still work — the overlay only tightens.
23
+ * - Single overlay per agent: `mail-inbound-reuse-consumer` already
24
+ * enforces one in-flight dispatch per agent (`recipient_busy`
25
+ * reject), so overwriting is safe.
26
+ *
27
+ * @module dispatch/permission-overlay
28
+ */
29
+ export interface OverlayPermissions {
30
+ allow?: string[];
31
+ deny?: string[];
32
+ ask?: string[];
33
+ }
34
+ /**
35
+ * Apply a permission overlay for an agent. Subsequent tool calls by
36
+ * that agent flow through the `PreToolUse` hook, which consults this
37
+ * registry. Overwrites any prior overlay for the same agent.
38
+ */
39
+ export declare function setPermissionOverlay(agentId: string, perms: OverlayPermissions): void;
40
+ /**
41
+ * Remove the active overlay for an agent. Subsequent tool calls fall
42
+ * back to the session's static permission rules.
43
+ */
44
+ export declare function clearPermissionOverlay(agentId: string): void;
45
+ /**
46
+ * Read the current overlay for an agent. Returns `undefined` when no
47
+ * overlay is in effect — the hook treats that as pass-through.
48
+ */
49
+ export declare function getPermissionOverlay(agentId: string): OverlayPermissions | undefined;
50
+ /**
51
+ * Clear all overlays. Used as defense-in-depth when a fresh consumer
52
+ * starts (process startup); also exposed for test isolation.
53
+ */
54
+ export declare function clearAllPermissionOverlays(): void;
55
+ /**
56
+ * Test helper — alias for `clearAllPermissionOverlays`. Kept distinct
57
+ * so it's grep-able for "test-only" cleanup paths.
58
+ */
59
+ export declare function _resetForTest(): void;
60
+ /**
61
+ * Test helper — number of overlays currently active.
62
+ */
63
+ export declare function _sizeForTest(): number;
64
+ //# sourceMappingURL=permission-overlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-overlay.d.ts","sourceRoot":"","sources":["../../src/dispatch/permission-overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,kBAAkB,GACxB,IAAI,CAEN;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE5D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,GACd,kBAAkB,GAAG,SAAS,CAEhC;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC"}