macro-agent 0.1.12 → 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.
- package/dist/agent/agent-manager-v2.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.js +240 -7
- package/dist/agent/agent-manager-v2.js.map +1 -1
- package/dist/agent/types.d.ts +47 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/boot-v2.d.ts +33 -0
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +142 -11
- package/dist/boot-v2.js.map +1 -1
- package/dist/cli/inbox-mcp-proxy.d.ts +36 -0
- package/dist/cli/inbox-mcp-proxy.d.ts.map +1 -0
- package/dist/cli/inbox-mcp-proxy.js +51 -0
- package/dist/cli/inbox-mcp-proxy.js.map +1 -0
- package/dist/dispatch/loadout-translation.d.ts +100 -0
- package/dist/dispatch/loadout-translation.d.ts.map +1 -0
- package/dist/dispatch/loadout-translation.js +90 -0
- package/dist/dispatch/loadout-translation.js.map +1 -0
- package/dist/dispatch/mail-inbound-consumer.d.ts +89 -0
- package/dist/dispatch/mail-inbound-consumer.d.ts.map +1 -0
- package/dist/dispatch/mail-inbound-consumer.js +261 -0
- package/dist/dispatch/mail-inbound-consumer.js.map +1 -0
- package/dist/dispatch/mail-inbound-reuse-consumer.d.ts +75 -0
- package/dist/dispatch/mail-inbound-reuse-consumer.d.ts.map +1 -0
- package/dist/dispatch/mail-inbound-reuse-consumer.js +325 -0
- package/dist/dispatch/mail-inbound-reuse-consumer.js.map +1 -0
- package/dist/dispatch/permission-evaluator.d.ts +68 -0
- package/dist/dispatch/permission-evaluator.d.ts.map +1 -0
- package/dist/dispatch/permission-evaluator.js +159 -0
- package/dist/dispatch/permission-evaluator.js.map +1 -0
- package/dist/dispatch/permission-overlay.d.ts +64 -0
- package/dist/dispatch/permission-overlay.d.ts.map +1 -0
- package/dist/dispatch/permission-overlay.js +72 -0
- package/dist/dispatch/permission-overlay.js.map +1 -0
- package/dist/dispatch/permissions-handler.d.ts +71 -0
- package/dist/dispatch/permissions-handler.d.ts.map +1 -0
- package/dist/dispatch/permissions-handler.js +83 -0
- package/dist/dispatch/permissions-handler.js.map +1 -0
- package/dist/dispatch/spawn-agent-handler.d.ts +84 -0
- package/dist/dispatch/spawn-agent-handler.d.ts.map +1 -0
- package/dist/dispatch/spawn-agent-handler.js +85 -0
- package/dist/dispatch/spawn-agent-handler.js.map +1 -0
- package/dist/lifecycle/handlers-v2.d.ts +7 -0
- package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
- package/dist/lifecycle/handlers-v2.js +27 -0
- package/dist/lifecycle/handlers-v2.js.map +1 -1
- package/dist/map/lifecycle-bridge.d.ts +18 -0
- package/dist/map/lifecycle-bridge.d.ts.map +1 -1
- package/dist/map/lifecycle-bridge.js +23 -1
- package/dist/map/lifecycle-bridge.js.map +1 -1
- package/dist/map/mail-bridge.d.ts +55 -0
- package/dist/map/mail-bridge.d.ts.map +1 -0
- package/dist/map/mail-bridge.js +115 -0
- package/dist/map/mail-bridge.js.map +1 -0
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +245 -1
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/types.d.ts +15 -0
- package/dist/map/types.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.js +1 -0
- package/dist/mcp/tools/done-v2.js.map +1 -1
- package/dist/teams/team-loader.d.ts.map +1 -1
- package/dist/teams/team-loader.js.map +1 -1
- package/dist/teams/team-runtime-v2.d.ts.map +1 -1
- package/dist/teams/team-runtime-v2.js +2 -0
- package/dist/teams/team-runtime-v2.js.map +1 -1
- package/package.json +6 -5
- package/src/agent/__tests__/agent-manager-v2.permission-interception.test.ts +296 -0
- package/src/agent/__tests__/agent-manager-v2.permissions.test.ts +233 -0
- package/src/agent/agent-manager-v2.ts +268 -8
- package/src/agent/types.ts +51 -0
- package/src/boot-v2.ts +190 -12
- package/src/cli/inbox-mcp-proxy.ts +56 -0
- package/src/dispatch/CLAUDE.md +129 -0
- package/src/dispatch/__tests__/loadout-translation.test.ts +141 -0
- package/src/dispatch/__tests__/mail-inbound-consumer.integration.test.ts +519 -0
- package/src/dispatch/__tests__/mail-inbound-consumer.test.ts +589 -0
- package/src/dispatch/__tests__/mail-inbound-reuse-consumer.test.ts +575 -0
- package/src/dispatch/__tests__/permission-evaluator.test.ts +196 -0
- package/src/dispatch/__tests__/permission-overlay.test.ts +56 -0
- package/src/dispatch/__tests__/permissions-handler.test.ts +168 -0
- package/src/dispatch/__tests__/spawn-agent-handler.test.ts +282 -0
- package/src/dispatch/loadout-translation.ts +138 -0
- package/src/dispatch/mail-inbound-consumer.ts +397 -0
- package/src/dispatch/mail-inbound-reuse-consumer.ts +479 -0
- package/src/dispatch/permission-evaluator.ts +191 -0
- package/src/dispatch/permission-overlay.ts +89 -0
- package/src/dispatch/permissions-handler.ts +112 -0
- package/src/dispatch/spawn-agent-handler.ts +160 -0
- package/src/lifecycle/handlers-v2.ts +34 -0
- package/src/map/__tests__/lifecycle-bridge.test.ts +64 -0
- package/src/map/__tests__/mail-bridge.test.ts +196 -0
- package/src/map/lifecycle-bridge.ts +48 -2
- package/src/map/mail-bridge.ts +203 -0
- package/src/map/sidecar.ts +346 -1
- package/src/map/types.ts +21 -0
- package/src/mcp/tools/done-v2.ts +1 -0
- package/src/teams/team-loader.ts +3 -1
- package/src/teams/team-runtime-v2.ts +2 -0
- package/dist/workspace/dataplane-adapter.d.ts +0 -260
- package/dist/workspace/dataplane-adapter.d.ts.map +0 -1
- package/dist/workspace/dataplane-adapter.js +0 -416
- package/dist/workspace/dataplane-adapter.js.map +0 -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"}
|