@swifty.js/swifty 0.0.21 → 0.0.23

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 (42) hide show
  1. package/dist/{agent-ZCMBUWLZ.js → agent-RABW3Z3R.js} +1 -1
  2. package/dist/anthropic-V22T6E6E.js +4 -0
  3. package/dist/{checker-OJFS2MZY.js → checker-3RWAYF2U.js} +1 -1
  4. package/dist/{chunk-Y5WGBAGP.js → chunk-5KXBVL2A.js} +1 -1
  5. package/dist/{chunk-MUPVOATV.js → chunk-CV7OKXYC.js} +1 -1
  6. package/dist/chunk-MJR2C7S7.js +130 -0
  7. package/dist/{chunk-GUXBLUNR.js → chunk-NR62AA5K.js} +1 -1
  8. package/dist/{chunk-S6ZADC7C.js → chunk-OCQ6TYLT.js} +21 -20
  9. package/dist/chunk-QN27QEFS.js +4 -0
  10. package/dist/{chunk-LX2LK3TF.js → chunk-SIWLMWHH.js} +12 -11
  11. package/dist/chunk-Y43POSIW.js +389 -0
  12. package/dist/lib/agent-GHXYOARN.js +9 -0
  13. package/dist/lib/anthropic-7YEN2QTU.js +22 -0
  14. package/dist/lib/bwrap-QRGPUP4J.js +6 -0
  15. package/dist/lib/checker-6BW4222R.js +19 -0
  16. package/dist/lib/chunk-7IZEK7X5.js +545 -0
  17. package/dist/lib/chunk-BN4Q54G2.js +1096 -0
  18. package/dist/lib/chunk-BO76JDFZ.js +617 -0
  19. package/dist/lib/chunk-CYDNMEBC.js +339 -0
  20. package/dist/lib/chunk-EY7HE52Q.js +384 -0
  21. package/dist/lib/chunk-GHF2PSEW.js +8 -0
  22. package/dist/lib/chunk-GNI7YX6F.js +426 -0
  23. package/dist/lib/chunk-HK2Z6WP4.js +223 -0
  24. package/dist/lib/chunk-MF3YLQDS.js +1277 -0
  25. package/dist/lib/chunk-OO2CLOEE.js +88 -0
  26. package/dist/lib/chunk-ORSYNBMM.js +38 -0
  27. package/dist/lib/chunk-PZ42NAFA.js +242 -0
  28. package/dist/lib/chunk-VUD72RTY.js +39 -0
  29. package/dist/lib/glob.wasm +0 -0
  30. package/dist/lib/index.d.ts +5347 -0
  31. package/dist/lib/index.js +11990 -0
  32. package/dist/lib/openai-RDHZFJUT.js +15 -0
  33. package/dist/lib/seatbelt-FT5IY73W.js +6 -0
  34. package/dist/lib/tool-filter-VF7TZRE5.js +19 -0
  35. package/dist/main.js +204 -466
  36. package/dist/{openai-HXD52MZB.js → openai-6NWVJ5LI.js} +15 -15
  37. package/dist/{server-VMHWEO2Y.js → server-KQTLAXC6.js} +17 -17
  38. package/package.json +18 -6
  39. package/dist/anthropic-4ZVG7AMA.js +0 -4
  40. package/dist/chunk-6E6UA7MT.js +0 -126
  41. package/dist/chunk-DDBH5AEN.js +0 -386
  42. package/dist/chunk-ZZQE743W.js +0 -4
@@ -0,0 +1,8 @@
1
+ // src/tools/tool-names.ts
2
+ var TOOL_SEARCH_TOOL_NAME = "ToolSearch";
3
+ var MCP_CALL_TOOL_NAME = "McpCall";
4
+
5
+ export {
6
+ TOOL_SEARCH_TOOL_NAME,
7
+ MCP_CALL_TOOL_NAME
8
+ };
@@ -0,0 +1,426 @@
1
+ import {
2
+ mcpCallPermissionContent
3
+ } from "./chunk-PZ42NAFA.js";
4
+ import {
5
+ createChildLogger,
6
+ strArg
7
+ } from "./chunk-EY7HE52Q.js";
8
+ import {
9
+ MCP_CALL_TOOL_NAME
10
+ } from "./chunk-GHF2PSEW.js";
11
+
12
+ // src/permissions/checker.ts
13
+ import { readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
14
+ import { homedir, tmpdir } from "os";
15
+ import { join, resolve, dirname } from "path";
16
+ import yaml from "js-yaml";
17
+ import z, { parse } from "zod";
18
+ var log = createChildLogger({ module: "permissions" });
19
+ var DANGEROUS_PATTERNS = [];
20
+ var SAFE_PREFIXES = [
21
+ "ls",
22
+ "pwd",
23
+ "echo",
24
+ "cat",
25
+ "head",
26
+ "tail",
27
+ "wc",
28
+ "date",
29
+ "whoami",
30
+ "uname",
31
+ "hostname",
32
+ "which",
33
+ "type",
34
+ "file",
35
+ "git status",
36
+ "git log",
37
+ "git diff",
38
+ "git branch",
39
+ "git show",
40
+ "git rev-parse",
41
+ "git remote",
42
+ "bun test",
43
+ "bun run",
44
+ "npm test",
45
+ "npm run",
46
+ "go test",
47
+ "go build",
48
+ "go vet",
49
+ "python -c",
50
+ "node -e"
51
+ ];
52
+ var CONTENT_FIELDS = {
53
+ Bash: "command",
54
+ PowerShell: "command",
55
+ ReadFile: "file_path",
56
+ WriteFile: "file_path",
57
+ EditFile: "file_path",
58
+ Glob: "pattern",
59
+ Grep: "pattern"
60
+ };
61
+ var DEFAULT_DENY_WRITE = [
62
+ ".swifty/config.yaml",
63
+ ".swifty/permissions.local.yaml",
64
+ ".swifty/skills/"
65
+ ];
66
+ function extractContent(toolName, args) {
67
+ if (toolName === MCP_CALL_TOOL_NAME) {
68
+ return mcpCallPermissionContent(strArg(args, "server", ""), strArg(args, "tool", ""));
69
+ }
70
+ const field = CONTENT_FIELDS[toolName];
71
+ if (!field) {
72
+ return "";
73
+ }
74
+ const v = args[field];
75
+ return typeof v === "string" ? v : "";
76
+ }
77
+ var PathSandbox = class {
78
+ allowedRoots;
79
+ denyWritePaths;
80
+ projectDir;
81
+ constructor(projectDir) {
82
+ this.projectDir = resolve(projectDir);
83
+ this.allowedRoots = [this.projectDir, tmpdir()];
84
+ this.denyWritePaths = DEFAULT_DENY_WRITE.map((p) => join(this.projectDir, p));
85
+ }
86
+ addRoot(root) {
87
+ this.allowedRoots.push(resolve(root));
88
+ }
89
+ // Add custom deny-write paths
90
+ addDenyWrite(path) {
91
+ this.denyWritePaths.push(resolve(path));
92
+ }
93
+ /**
94
+ * Check whether a path is in the deny-write list.
95
+ * denyWrite has the highest priority — even if the path is within an allowed root, writes are still denied.
96
+ */
97
+ checkDenyWrite(filePath) {
98
+ const absolute = resolve(filePath);
99
+ for (const denied of this.denyWritePaths) {
100
+ if (absolute.startsWith(denied)) {
101
+ return {
102
+ effect: "deny",
103
+ reason: `Path ${filePath} is in deny-write list`
104
+ };
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ check(filePath) {
110
+ const absolute = resolve(filePath);
111
+ for (const root of this.allowedRoots) {
112
+ if (absolute.startsWith(root)) {
113
+ return null;
114
+ }
115
+ }
116
+ return {
117
+ effect: "deny",
118
+ reason: `Path ${filePath} is outside allowed directories`
119
+ };
120
+ }
121
+ };
122
+ function globMatch(pattern, content) {
123
+ const re = "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
124
+ try {
125
+ return new RegExp(re).test(content);
126
+ } catch (err) {
127
+ log.error({ err }, "permissions operation failed");
128
+ return false;
129
+ }
130
+ }
131
+ var RULE_RE = /^(\w+)\((.+)\)$/;
132
+ function loadRulesFile(path) {
133
+ let data;
134
+ try {
135
+ data = readFileSync(path, "utf-8");
136
+ } catch (err) {
137
+ if (err.code !== "ENOENT") {
138
+ log.error({ err }, "permissions operation failed");
139
+ }
140
+ return [];
141
+ }
142
+ const YamlEntrySchema = z.object({
143
+ rule: z.string().optional(),
144
+ effect: z.string().optional()
145
+ });
146
+ let yamlData;
147
+ try {
148
+ const parsed = yaml.load(data);
149
+ yamlData = parse(z.array(YamlEntrySchema), parsed);
150
+ } catch (err) {
151
+ log.error({ err }, "permissions operation failed");
152
+ return [];
153
+ }
154
+ const rules = [];
155
+ for (const entry of yamlData) {
156
+ if (entry.effect !== "allow" && entry.effect !== "deny" && entry.effect !== "ask") {
157
+ continue;
158
+ }
159
+ const m = RULE_RE.exec((entry.rule ?? "").trim());
160
+ if (!m) {
161
+ continue;
162
+ }
163
+ rules.push({ tool: m[1], pattern: m[2], effect: entry.effect });
164
+ }
165
+ return rules;
166
+ }
167
+ function evaluateRules(rules, toolName, content) {
168
+ let hit = null;
169
+ for (const r of rules) {
170
+ if (r.tool !== toolName && r.tool !== "*") {
171
+ continue;
172
+ }
173
+ if (!globMatch(r.pattern, content)) {
174
+ continue;
175
+ }
176
+ if (r.effect === "deny") {
177
+ return "deny";
178
+ }
179
+ if (r.effect === "ask") {
180
+ hit = "ask";
181
+ } else {
182
+ hit ??= "allow";
183
+ }
184
+ }
185
+ return hit;
186
+ }
187
+ var RuleEngine = class {
188
+ userPath;
189
+ projectPath;
190
+ localPath;
191
+ cache = /* @__PURE__ */ new Map();
192
+ constructor(workDir) {
193
+ this.userPath = join(homedir(), ".swifty", "permissions.yaml");
194
+ this.projectPath = join(workDir, ".swifty", "permissions.yaml");
195
+ this.localPath = join(workDir, ".swifty", "permissions.local.yaml");
196
+ }
197
+ // Read a single rules file; skips disk I/O and parsing on cache hit.
198
+ rulesFor(path) {
199
+ let st;
200
+ try {
201
+ st = statSync(path, { bigint: true });
202
+ } catch {
203
+ this.cache.delete(path);
204
+ return [];
205
+ }
206
+ const cached = this.cache.get(path);
207
+ if (cached?.mtimeNs === st.mtimeNs && cached.size === st.size) {
208
+ return cached.rules;
209
+ }
210
+ const rules = loadRulesFile(path);
211
+ this.cache.set(path, { mtimeNs: st.mtimeNs, size: st.size, rules });
212
+ return rules;
213
+ }
214
+ // Return the merged snapshot of all three rules files. Reuses the previous
215
+ // parse result when files are unchanged; re-reads only on change, so edits
216
+ // take effect on the next evaluation without redundant parsing. One snapshot
217
+ // is taken per tool call and shared across sub-command checks.
218
+ snapshot() {
219
+ return [this.userPath, this.projectPath, this.localPath].flatMap((p) => this.rulesFor(p));
220
+ }
221
+ // Take a snapshot then adjudicate: reuses the previous parse result when
222
+ // files are unchanged; a freshly written "allow always" rule takes effect
223
+ // immediately. Priority is deny > ask > allow regardless of which layer or
224
+ // line a rule resides on, so a deny cannot be overridden by an allow from
225
+ // another layer. Returns null when no rule matches.
226
+ evaluate(toolName, content) {
227
+ return evaluateRules(this.snapshot(), toolName, content);
228
+ }
229
+ // Persists a rule to the project-local YAML file in the `Tool(pattern)`
230
+ // format so "allow always" survives a restart.
231
+ appendLocalRule(rule) {
232
+ mkdirSync(dirname(this.localPath), { recursive: true });
233
+ const rules = loadRulesFile(this.localPath);
234
+ const exists = rules.some(
235
+ (r) => r.tool === rule.tool && r.pattern === rule.pattern && r.effect === rule.effect
236
+ );
237
+ if (exists) {
238
+ return;
239
+ }
240
+ rules.push(rule);
241
+ const entries = rules.map((r) => ({
242
+ rule: `${r.tool}(${r.pattern})`,
243
+ effect: r.effect
244
+ }));
245
+ writeFileSync(this.localPath, yaml.dump(entries), "utf-8");
246
+ }
247
+ };
248
+ function detectDangerous(command) {
249
+ for (const p of DANGEROUS_PATTERNS) {
250
+ if (p.re.test(command)) {
251
+ return p.reason;
252
+ }
253
+ }
254
+ return "";
255
+ }
256
+ function isSafeCommand(command) {
257
+ const trimmed = command.trim();
258
+ if (trimmed.includes(">") || trimmed.includes("|") || trimmed.includes(";") || trimmed.includes("&&") || trimmed.includes("$(") || trimmed.includes("`")) {
259
+ return false;
260
+ }
261
+ return SAFE_PREFIXES.some(
262
+ (prefix) => trimmed === prefix || trimmed.startsWith(prefix + " ") || trimmed.startsWith(prefix + " ")
263
+ );
264
+ }
265
+ function modeDecide(mode, category) {
266
+ switch (mode) {
267
+ case "bypassPermissions":
268
+ return "allow";
269
+ case "plan":
270
+ return category === "read" ? "allow" : "ask";
271
+ case "acceptEdits":
272
+ return category === "command" ? "ask" : "allow";
273
+ case "default":
274
+ default:
275
+ return category === "read" ? "allow" : "ask";
276
+ }
277
+ }
278
+ var PermissionChecker = class {
279
+ mode;
280
+ planFilePath = "";
281
+ // Sandbox mode: when enabled, command-category tools run through OS sandbox isolation, with optional auto-allow
282
+ sandboxEnabled = false;
283
+ sandboxAutoAllow = false;
284
+ sandbox;
285
+ ruleEngine;
286
+ constructor(workDir, mode = "default") {
287
+ this.mode = mode;
288
+ this.sandbox = new PathSandbox(workDir);
289
+ this.ruleEngine = new RuleEngine(workDir);
290
+ }
291
+ check(toolName, category, args) {
292
+ const content = extractContent(toolName, args);
293
+ let snapshot = null;
294
+ const rules = () => snapshot ??= this.ruleEngine.snapshot();
295
+ if (this.mode === "plan" && (toolName === "WriteFile" || toolName === "EditFile")) {
296
+ const path = strArg(args, "file_path", "");
297
+ if (path.includes(".swifty/plans/")) {
298
+ return {
299
+ effect: "allow",
300
+ reason: "Plan file write allowed in plan mode"
301
+ };
302
+ }
303
+ }
304
+ if (category === "command" && isSafeCommand(content)) {
305
+ return { effect: "allow", reason: "Safe read-only command" };
306
+ }
307
+ const dangerReason = category === "command" ? detectDangerous(content) : "";
308
+ if (dangerReason) {
309
+ return {
310
+ effect: "deny",
311
+ reason: `Dangerous command blocked: ${dangerReason}`
312
+ };
313
+ }
314
+ if (this.sandboxEnabled && this.sandboxAutoAllow && toolName === "Bash") {
315
+ const subcommands = strArg(args, "command").split(/\s*(?:&&|\|\||[;|])\s*/).map((s) => s.trim()).filter(Boolean);
316
+ let hasAsk = false;
317
+ for (const sub of subcommands) {
318
+ const ruleResult = evaluateRules(rules(), toolName, sub);
319
+ if (ruleResult === "deny") {
320
+ return { effect: "deny", reason: "Permission rule: deny" };
321
+ }
322
+ if (ruleResult === "ask") {
323
+ hasAsk = true;
324
+ }
325
+ }
326
+ if (hasAsk) {
327
+ return {
328
+ effect: "ask",
329
+ reason: "Permission rule: ask (sandbox does not override)"
330
+ };
331
+ }
332
+ return {
333
+ effect: "allow",
334
+ reason: "Sandbox auto-allow: OS sandbox active"
335
+ };
336
+ }
337
+ const filePath = strArg(args, "file_path", strArg(args, "path", ""));
338
+ if ((category === "read" || category === "write") && filePath) {
339
+ if (category === "write") {
340
+ const denyDecision = this.sandbox.checkDenyWrite(filePath);
341
+ if (denyDecision) {
342
+ return denyDecision;
343
+ }
344
+ }
345
+ const sandboxDecision = this.sandbox.check(filePath);
346
+ if (sandboxDecision && this.mode !== "bypassPermissions") {
347
+ const ruleEffect2 = this.ruleEngine.evaluate(toolName, content);
348
+ if (ruleEffect2) {
349
+ return {
350
+ effect: ruleEffect2,
351
+ reason: `Permission rule: ${ruleEffect2}`
352
+ };
353
+ }
354
+ return { effect: "ask", reason: sandboxDecision.reason };
355
+ }
356
+ }
357
+ const ruleEffect = evaluateRules(rules(), toolName, content);
358
+ if (ruleEffect) {
359
+ return { effect: ruleEffect, reason: `Permission rule: ${ruleEffect}` };
360
+ }
361
+ return {
362
+ effect: modeDecide(this.mode, category),
363
+ reason: `Mode: ${this.mode}`
364
+ };
365
+ }
366
+ // Allow an extra directory outside the project root. When a background agent
367
+ // needs read/write access to user-level data (e.g., user-level memory dir),
368
+ // the caller declares it explicitly; the sandbox baseline stays at the project root.
369
+ allowExtraRoot(path) {
370
+ this.sandbox.addRoot(path);
371
+ }
372
+ // Persist a scoped "allow always" rule.
373
+ // - File path: parent directory + `/*` (a directory path uses itself + `/*`).
374
+ // - Command: first 1-2 words + `*` so it allows that command family.
375
+ allowAlways(toolName, args) {
376
+ const content = extractContent(toolName, args);
377
+ const isFilePath = toolName === "ReadFile" || toolName === "WriteFile" || toolName === "EditFile";
378
+ let pattern;
379
+ if (isFilePath && content) {
380
+ const abs = resolve(content);
381
+ let isDir = false;
382
+ try {
383
+ isDir = statSync(abs).isDirectory();
384
+ } catch {
385
+ }
386
+ pattern = join(isDir ? abs : dirname(abs), "*");
387
+ } else {
388
+ const words = content.trim().split(/\s+/).slice(0, 2);
389
+ pattern = words.join(" ") + "*";
390
+ }
391
+ this.ruleEngine.appendLocalRule({
392
+ tool: toolName,
393
+ pattern,
394
+ effect: "allow"
395
+ });
396
+ }
397
+ /**
398
+ * Generate a human-readable description of the tool action for display in HITL confirmation dialogs.
399
+ * Prioritizes extracting fields defined in contentFields (e.g., command, file_path);
400
+ * falls back to a key:value summary of parameters if no match is found.
401
+ */
402
+ describeToolAction(toolName, args) {
403
+ const content = extractContent(toolName, args);
404
+ if (content) {
405
+ return content;
406
+ }
407
+ const parts = [];
408
+ for (const [k, v] of Object.entries(args)) {
409
+ let s = String(v);
410
+ if (s.length > 80) {
411
+ s = s.slice(0, 80) + "...";
412
+ }
413
+ parts.push(`${k}: ${s}`);
414
+ }
415
+ return parts.join(", ");
416
+ }
417
+ };
418
+
419
+ export {
420
+ extractContent,
421
+ PathSandbox,
422
+ evaluateRules,
423
+ RuleEngine,
424
+ isSafeCommand,
425
+ PermissionChecker
426
+ };
@@ -0,0 +1,223 @@
1
+ import {
2
+ MCP_CALL_TOOL_NAME,
3
+ TOOL_SEARCH_TOOL_NAME
4
+ } from "./chunk-GHF2PSEW.js";
5
+
6
+ // src/tools/registry.ts
7
+ var ToolRegistry = class {
8
+ tools = /* @__PURE__ */ new Map();
9
+ discovered = /* @__PURE__ */ new Set();
10
+ /**
11
+ * How MCP tools are loaded, written by mcp/strategy after connecting to the
12
+ * server. ToolSearch relies on it to decide what to return, and the client
13
+ * relies on it to decide whether to send defer_loading. It stays eager when
14
+ * there is no MCP, which behaves the same as no deferral.
15
+ */
16
+ mcpLoadingMode = "eager";
17
+ /**
18
+ * Whether to expose the search and dispatch tools to the model is computed once
19
+ * by applyMode in mcp/strategy at session start. It is not recomputed each turn
20
+ * based on "are there still deferred tools": tools may be disabled at runtime,
21
+ * and recomputing would drop one from tools[] mid-session — that's an array
22
+ * change, which breaks the cache prefix just the same.
23
+ */
24
+ exposeToolSearch = false;
25
+ exposeMcpCall = false;
26
+ register(tool) {
27
+ this.tools.set(tool.name, tool);
28
+ }
29
+ get(name) {
30
+ return this.tools.get(name);
31
+ }
32
+ listTools() {
33
+ return [...this.tools.values()];
34
+ }
35
+ getAllSchemas(protocol = "anthropic") {
36
+ const isOpenAI = protocol === "openai" || protocol === "openai-compat";
37
+ const native = this.mcpLoadingMode === "native" && !isOpenAI;
38
+ const schemas = [];
39
+ for (const tool of this.tools.values()) {
40
+ if (tool.name === TOOL_SEARCH_TOOL_NAME && !this.exposeToolSearch || tool.name === MCP_CALL_TOOL_NAME && !this.exposeMcpCall) {
41
+ continue;
42
+ }
43
+ const deferred = Boolean(tool.deferred) && !this.discovered.has(tool.name);
44
+ if (deferred && !native) {
45
+ continue;
46
+ }
47
+ const s = tool.schema();
48
+ if (isOpenAI) {
49
+ schemas.push({
50
+ strict: false,
51
+ // Whether to enforce strict parameter validation. Default true.
52
+ type: "function",
53
+ name: s.name,
54
+ description: s.description,
55
+ parameters: s.input_schema,
56
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
57
+ // @ts-ignore
58
+ function: {
59
+ name: s.name,
60
+ description: s.description,
61
+ parameters: s.input_schema
62
+ }
63
+ });
64
+ } else {
65
+ schemas.push(
66
+ deferred ? {
67
+ ...s,
68
+ type: "custom",
69
+ defer_loading: true
70
+ } : {
71
+ ...s,
72
+ type: "custom"
73
+ }
74
+ );
75
+ }
76
+ }
77
+ return schemas;
78
+ }
79
+ /**
80
+ * Names of deferred tools not yet discovered, in lexicographic order.
81
+ *
82
+ * Sorting is not cosmetic: callers compare this list to detect pool changes;
83
+ * unstable ordering would produce different text for the same set of tools
84
+ * and break the comparison.
85
+ */
86
+ getDeferredToolNames() {
87
+ const names = [];
88
+ for (const tool of this.tools.values()) {
89
+ if (tool.deferred && !this.discovered.has(tool.name)) {
90
+ names.push(tool.name);
91
+ }
92
+ }
93
+ return names.sort();
94
+ }
95
+ getDeferredTools() {
96
+ return [...this.tools.values()].filter((t) => t.deferred && !this.discovered.has(t.name));
97
+ }
98
+ searchDeferred(query, maxResults = 5) {
99
+ const lower = query.toLowerCase();
100
+ const matches = [];
101
+ for (const tool of this.tools.values()) {
102
+ if (!tool.deferred || this.discovered.has(tool.name)) {
103
+ continue;
104
+ }
105
+ if (tool.name.toLowerCase().includes(lower) || tool.description.toLowerCase().includes(lower)) {
106
+ matches.push(tool);
107
+ if (matches.length >= maxResults) {
108
+ break;
109
+ }
110
+ }
111
+ }
112
+ return matches;
113
+ }
114
+ findDeferredByNames(names) {
115
+ const lowerMap = /* @__PURE__ */ new Map();
116
+ for (const [name, tool] of this.tools) {
117
+ lowerMap.set(name.toLowerCase(), tool);
118
+ }
119
+ return names.map((n) => lowerMap.get(n.toLowerCase())).filter((t) => t?.deferred ?? false);
120
+ }
121
+ markDiscovered(name) {
122
+ this.discovered.add(name);
123
+ }
124
+ isDiscovered(name) {
125
+ return this.discovered.has(name);
126
+ }
127
+ };
128
+
129
+ // src/subagent/tool-filter.ts
130
+ var SUBAGENT_DISALLOWED_TOOLS = /* @__PURE__ */ new Set([
131
+ "ExitPlanMode",
132
+ "Agent",
133
+ // Prevents recursive spawning of subagents
134
+ "AskUserQuestion",
135
+ "TaskStop"
136
+ ]);
137
+ var TEAMMATE_DISALLOWED_TOOLS = /* @__PURE__ */ new Set(["TeamCreate", "TeamDelete"]);
138
+ var CUSTOM_AGENT_DISALLOWED_TOOLS = /* @__PURE__ */ new Set([
139
+ "ExitPlanMode",
140
+ "Agent",
141
+ "AskUserQuestion",
142
+ "TaskStop"
143
+ ]);
144
+ var ASYNC_AGENT_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
145
+ "ReadFile",
146
+ // "WebSearch",
147
+ "Grep",
148
+ "Glob",
149
+ "Bash",
150
+ "PowerShell",
151
+ "EditFile",
152
+ "WriteFile",
153
+ "LoadSkill",
154
+ "SyntheticOutput",
155
+ "ToolSearch",
156
+ "EnterWorktree",
157
+ "ExitWorktree",
158
+ // ToolSearch only reads out schemas; actual invocation relies on McpCall.
159
+ // The two must be allowed together, or the subagent sees tools but cannot call them
160
+ "McpCall"
161
+ ]);
162
+ function isMCPTool(name) {
163
+ return name.startsWith("mcp__");
164
+ }
165
+ function filterToolsForAgent(registry, allowedTools, disallowedTools, isAsync, isCustom = false) {
166
+ const disallowed = new Set(disallowedTools ?? []);
167
+ const allowed = new Set(allowedTools ?? []);
168
+ const hasWhitelist = allowed.size > 0 && !(allowed.size === 1 && allowed.has("*"));
169
+ const filtered = new ToolRegistry();
170
+ for (const tool of registry.listTools()) {
171
+ const name = tool.name;
172
+ if (isMCPTool(name)) {
173
+ filtered.register(tool);
174
+ continue;
175
+ }
176
+ if (SUBAGENT_DISALLOWED_TOOLS.has(name)) {
177
+ continue;
178
+ }
179
+ if (isCustom && CUSTOM_AGENT_DISALLOWED_TOOLS.has(name)) {
180
+ continue;
181
+ }
182
+ if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(name)) {
183
+ continue;
184
+ }
185
+ if (disallowed.has(name)) {
186
+ continue;
187
+ }
188
+ if (hasWhitelist && !allowed.has(name)) {
189
+ continue;
190
+ }
191
+ filtered.register(tool);
192
+ }
193
+ return filtered;
194
+ }
195
+ var FORK_QUERY_SOURCE = "agent:builtin:fork";
196
+ function cloneRegistryForFork(registry) {
197
+ const forked = new ToolRegistry();
198
+ for (const tool of registry.listTools()) {
199
+ if (tool.name === "Agent" && "querySource" in tool) {
200
+ const clone = Object.create(
201
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
202
+ Object.getPrototypeOf(tool),
203
+ Object.getOwnPropertyDescriptors(tool)
204
+ );
205
+ clone.querySource = FORK_QUERY_SOURCE;
206
+ forked.register(clone);
207
+ } else {
208
+ forked.register(tool);
209
+ }
210
+ }
211
+ return forked;
212
+ }
213
+
214
+ export {
215
+ ToolRegistry,
216
+ SUBAGENT_DISALLOWED_TOOLS,
217
+ TEAMMATE_DISALLOWED_TOOLS,
218
+ CUSTOM_AGENT_DISALLOWED_TOOLS,
219
+ ASYNC_AGENT_ALLOWED_TOOLS,
220
+ filterToolsForAgent,
221
+ FORK_QUERY_SOURCE,
222
+ cloneRegistryForFork
223
+ };