joonecli 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 (279) hide show
  1. package/AGENTS.md +56 -0
  2. package/Handover.md +115 -0
  3. package/LICENSE +201 -0
  4. package/PROGRESS.md +160 -0
  5. package/README.md +114 -0
  6. package/dist/__tests__/bootstrap.test.d.ts +1 -0
  7. package/dist/__tests__/bootstrap.test.js +76 -0
  8. package/dist/__tests__/bootstrap.test.js.map +1 -0
  9. package/dist/__tests__/config.test.d.ts +1 -0
  10. package/dist/__tests__/config.test.js +84 -0
  11. package/dist/__tests__/config.test.js.map +1 -0
  12. package/dist/__tests__/m55.test.d.ts +1 -0
  13. package/dist/__tests__/m55.test.js +160 -0
  14. package/dist/__tests__/m55.test.js.map +1 -0
  15. package/dist/__tests__/middleware.test.d.ts +1 -0
  16. package/dist/__tests__/middleware.test.js +169 -0
  17. package/dist/__tests__/middleware.test.js.map +1 -0
  18. package/dist/__tests__/modelFactory.test.d.ts +1 -0
  19. package/dist/__tests__/modelFactory.test.js +50 -0
  20. package/dist/__tests__/modelFactory.test.js.map +1 -0
  21. package/dist/__tests__/optimizations.test.d.ts +1 -0
  22. package/dist/__tests__/optimizations.test.js +136 -0
  23. package/dist/__tests__/optimizations.test.js.map +1 -0
  24. package/dist/__tests__/promptBuilder.test.d.ts +1 -0
  25. package/dist/__tests__/promptBuilder.test.js +108 -0
  26. package/dist/__tests__/promptBuilder.test.js.map +1 -0
  27. package/dist/__tests__/sandbox.test.d.ts +1 -0
  28. package/dist/__tests__/sandbox.test.js +78 -0
  29. package/dist/__tests__/sandbox.test.js.map +1 -0
  30. package/dist/__tests__/security.test.d.ts +1 -0
  31. package/dist/__tests__/security.test.js +86 -0
  32. package/dist/__tests__/security.test.js.map +1 -0
  33. package/dist/__tests__/streaming.test.d.ts +1 -0
  34. package/dist/__tests__/streaming.test.js +71 -0
  35. package/dist/__tests__/streaming.test.js.map +1 -0
  36. package/dist/__tests__/toolRouter.test.d.ts +1 -0
  37. package/dist/__tests__/toolRouter.test.js +37 -0
  38. package/dist/__tests__/toolRouter.test.js.map +1 -0
  39. package/dist/__tests__/tools.test.d.ts +1 -0
  40. package/dist/__tests__/tools.test.js +112 -0
  41. package/dist/__tests__/tools.test.js.map +1 -0
  42. package/dist/__tests__/tracing.test.d.ts +1 -0
  43. package/dist/__tests__/tracing.test.js +147 -0
  44. package/dist/__tests__/tracing.test.js.map +1 -0
  45. package/dist/cli/config.d.ts +49 -0
  46. package/dist/cli/config.js +86 -0
  47. package/dist/cli/config.js.map +1 -0
  48. package/dist/cli/index.d.ts +2 -0
  49. package/dist/cli/index.js +625 -0
  50. package/dist/cli/index.js.map +1 -0
  51. package/dist/cli/modelFactory.d.ts +9 -0
  52. package/dist/cli/modelFactory.js +154 -0
  53. package/dist/cli/modelFactory.js.map +1 -0
  54. package/dist/cli/providers.d.ts +18 -0
  55. package/dist/cli/providers.js +94 -0
  56. package/dist/cli/providers.js.map +1 -0
  57. package/dist/core/agentLoop.d.ts +43 -0
  58. package/dist/core/agentLoop.js +245 -0
  59. package/dist/core/agentLoop.js.map +1 -0
  60. package/dist/core/errors.d.ts +62 -0
  61. package/dist/core/errors.js +139 -0
  62. package/dist/core/errors.js.map +1 -0
  63. package/dist/core/promptBuilder.d.ts +49 -0
  64. package/dist/core/promptBuilder.js +84 -0
  65. package/dist/core/promptBuilder.js.map +1 -0
  66. package/dist/core/reasoningRouter.d.ts +62 -0
  67. package/dist/core/reasoningRouter.js +102 -0
  68. package/dist/core/reasoningRouter.js.map +1 -0
  69. package/dist/core/retry.d.ts +25 -0
  70. package/dist/core/retry.js +49 -0
  71. package/dist/core/retry.js.map +1 -0
  72. package/dist/core/sessionResumer.d.ts +17 -0
  73. package/dist/core/sessionResumer.js +78 -0
  74. package/dist/core/sessionResumer.js.map +1 -0
  75. package/dist/core/sessionStore.d.ts +45 -0
  76. package/dist/core/sessionStore.js +167 -0
  77. package/dist/core/sessionStore.js.map +1 -0
  78. package/dist/core/tokenCounter.d.ts +17 -0
  79. package/dist/core/tokenCounter.js +54 -0
  80. package/dist/core/tokenCounter.js.map +1 -0
  81. package/dist/evals/dataset.d.ts +4 -0
  82. package/dist/evals/dataset.js +61 -0
  83. package/dist/evals/dataset.js.map +1 -0
  84. package/dist/evals/evaluator.d.ts +21 -0
  85. package/dist/evals/evaluator.js +68 -0
  86. package/dist/evals/evaluator.js.map +1 -0
  87. package/dist/hitl/bridge.d.ts +65 -0
  88. package/dist/hitl/bridge.js +120 -0
  89. package/dist/hitl/bridge.js.map +1 -0
  90. package/dist/middleware/commandSanitizer.d.ts +18 -0
  91. package/dist/middleware/commandSanitizer.js +50 -0
  92. package/dist/middleware/commandSanitizer.js.map +1 -0
  93. package/dist/middleware/loopDetection.d.ts +28 -0
  94. package/dist/middleware/loopDetection.js +49 -0
  95. package/dist/middleware/loopDetection.js.map +1 -0
  96. package/dist/middleware/permission.d.ts +17 -0
  97. package/dist/middleware/permission.js +59 -0
  98. package/dist/middleware/permission.js.map +1 -0
  99. package/dist/middleware/pipeline.d.ts +31 -0
  100. package/dist/middleware/pipeline.js +62 -0
  101. package/dist/middleware/pipeline.js.map +1 -0
  102. package/dist/middleware/preCompletion.d.ts +29 -0
  103. package/dist/middleware/preCompletion.js +82 -0
  104. package/dist/middleware/preCompletion.js.map +1 -0
  105. package/dist/middleware/types.d.ts +40 -0
  106. package/dist/middleware/types.js +8 -0
  107. package/dist/middleware/types.js.map +1 -0
  108. package/dist/sandbox/bootstrap.d.ts +38 -0
  109. package/dist/sandbox/bootstrap.js +107 -0
  110. package/dist/sandbox/bootstrap.js.map +1 -0
  111. package/dist/sandbox/manager.d.ts +72 -0
  112. package/dist/sandbox/manager.js +180 -0
  113. package/dist/sandbox/manager.js.map +1 -0
  114. package/dist/sandbox/sync.d.ts +55 -0
  115. package/dist/sandbox/sync.js +135 -0
  116. package/dist/sandbox/sync.js.map +1 -0
  117. package/dist/skills/loader.d.ts +55 -0
  118. package/dist/skills/loader.js +132 -0
  119. package/dist/skills/loader.js.map +1 -0
  120. package/dist/skills/tools.d.ts +5 -0
  121. package/dist/skills/tools.js +78 -0
  122. package/dist/skills/tools.js.map +1 -0
  123. package/dist/skills/types.d.ts +13 -0
  124. package/dist/skills/types.js +2 -0
  125. package/dist/skills/types.js.map +1 -0
  126. package/dist/test_cache.d.ts +1 -0
  127. package/dist/test_cache.js +55 -0
  128. package/dist/test_cache.js.map +1 -0
  129. package/dist/test_google.js +93 -0
  130. package/dist/tools/askUser.d.ts +10 -0
  131. package/dist/tools/askUser.js +42 -0
  132. package/dist/tools/askUser.js.map +1 -0
  133. package/dist/tools/browser.d.ts +19 -0
  134. package/dist/tools/browser.js +111 -0
  135. package/dist/tools/browser.js.map +1 -0
  136. package/dist/tools/index.d.ts +27 -0
  137. package/dist/tools/index.js +184 -0
  138. package/dist/tools/index.js.map +1 -0
  139. package/dist/tools/registry.d.ts +31 -0
  140. package/dist/tools/registry.js +168 -0
  141. package/dist/tools/registry.js.map +1 -0
  142. package/dist/tools/router.d.ts +34 -0
  143. package/dist/tools/router.js +73 -0
  144. package/dist/tools/router.js.map +1 -0
  145. package/dist/tools/security.d.ts +28 -0
  146. package/dist/tools/security.js +183 -0
  147. package/dist/tools/security.js.map +1 -0
  148. package/dist/tools/webSearch.d.ts +6 -0
  149. package/dist/tools/webSearch.js +120 -0
  150. package/dist/tools/webSearch.js.map +1 -0
  151. package/dist/tracing/analyzer.d.ts +58 -0
  152. package/dist/tracing/analyzer.js +190 -0
  153. package/dist/tracing/analyzer.js.map +1 -0
  154. package/dist/tracing/langsmith.d.ts +38 -0
  155. package/dist/tracing/langsmith.js +50 -0
  156. package/dist/tracing/langsmith.js.map +1 -0
  157. package/dist/tracing/sessionTracer.d.ts +73 -0
  158. package/dist/tracing/sessionTracer.js +157 -0
  159. package/dist/tracing/sessionTracer.js.map +1 -0
  160. package/dist/tracing/types.d.ts +46 -0
  161. package/dist/tracing/types.js +5 -0
  162. package/dist/tracing/types.js.map +1 -0
  163. package/dist/ui/App.d.ts +24 -0
  164. package/dist/ui/App.js +172 -0
  165. package/dist/ui/App.js.map +1 -0
  166. package/dist/ui/components/HITLPrompt.d.ts +15 -0
  167. package/dist/ui/components/HITLPrompt.js +35 -0
  168. package/dist/ui/components/HITLPrompt.js.map +1 -0
  169. package/dist/ui/components/Header.d.ts +8 -0
  170. package/dist/ui/components/Header.js +6 -0
  171. package/dist/ui/components/Header.js.map +1 -0
  172. package/dist/ui/components/MessageBubble.d.ts +13 -0
  173. package/dist/ui/components/MessageBubble.js +17 -0
  174. package/dist/ui/components/MessageBubble.js.map +1 -0
  175. package/dist/ui/components/StatusBar.d.ts +21 -0
  176. package/dist/ui/components/StatusBar.js +34 -0
  177. package/dist/ui/components/StatusBar.js.map +1 -0
  178. package/dist/ui/components/StreamingText.d.ts +13 -0
  179. package/dist/ui/components/StreamingText.js +24 -0
  180. package/dist/ui/components/StreamingText.js.map +1 -0
  181. package/dist/ui/components/ToolCallPanel.d.ts +15 -0
  182. package/dist/ui/components/ToolCallPanel.js +18 -0
  183. package/dist/ui/components/ToolCallPanel.js.map +1 -0
  184. package/docs/01_insights_and_patterns.md +27 -0
  185. package/docs/02_edge_cases_and_mitigations.md +143 -0
  186. package/docs/03_initial_implementation_plan.md +66 -0
  187. package/docs/04_tech_stack_proposal.md +20 -0
  188. package/docs/05_prd.md +87 -0
  189. package/docs/06_user_stories.md +72 -0
  190. package/docs/07_system_architecture.md +138 -0
  191. package/docs/08_roadmap.md +200 -0
  192. package/e2b/Dockerfile +26 -0
  193. package/package.json +57 -0
  194. package/src/__tests__/bootstrap.test.ts +111 -0
  195. package/src/__tests__/config.test.ts +97 -0
  196. package/src/__tests__/m55.test.ts +238 -0
  197. package/src/__tests__/middleware.test.ts +219 -0
  198. package/src/__tests__/modelFactory.test.ts +63 -0
  199. package/src/__tests__/optimizations.test.ts +201 -0
  200. package/src/__tests__/promptBuilder.test.ts +141 -0
  201. package/src/__tests__/sandbox.test.ts +102 -0
  202. package/src/__tests__/security.test.ts +122 -0
  203. package/src/__tests__/streaming.test.ts +82 -0
  204. package/src/__tests__/toolRouter.test.ts +52 -0
  205. package/src/__tests__/tools.test.ts +146 -0
  206. package/src/__tests__/tracing.test.ts +196 -0
  207. package/src/agents/agentRegistry.ts +69 -0
  208. package/src/agents/agentSpec.ts +67 -0
  209. package/src/agents/builtinAgents.ts +142 -0
  210. package/src/cli/config.ts +124 -0
  211. package/src/cli/index.ts +730 -0
  212. package/src/cli/modelFactory.ts +174 -0
  213. package/src/cli/providers.ts +107 -0
  214. package/src/commands/builtinCommands.ts +293 -0
  215. package/src/commands/commandRegistry.ts +194 -0
  216. package/src/core/agentLoop.d.ts.map +1 -0
  217. package/src/core/agentLoop.ts +312 -0
  218. package/src/core/autoSave.ts +95 -0
  219. package/src/core/compactor.ts +252 -0
  220. package/src/core/contextGuard.ts +129 -0
  221. package/src/core/errors.ts +202 -0
  222. package/src/core/promptBuilder.d.ts.map +1 -0
  223. package/src/core/promptBuilder.ts +139 -0
  224. package/src/core/reasoningRouter.ts +121 -0
  225. package/src/core/retry.ts +75 -0
  226. package/src/core/sessionResumer.ts +90 -0
  227. package/src/core/sessionStore.ts +215 -0
  228. package/src/core/subAgent.ts +339 -0
  229. package/src/core/tokenCounter.ts +64 -0
  230. package/src/evals/dataset.ts +67 -0
  231. package/src/evals/evaluator.ts +81 -0
  232. package/src/hitl/bridge.ts +160 -0
  233. package/src/middleware/commandSanitizer.ts +60 -0
  234. package/src/middleware/loopDetection.ts +63 -0
  235. package/src/middleware/permission.ts +72 -0
  236. package/src/middleware/pipeline.ts +75 -0
  237. package/src/middleware/preCompletion.ts +94 -0
  238. package/src/middleware/types.ts +45 -0
  239. package/src/sandbox/bootstrap.ts +121 -0
  240. package/src/sandbox/manager.ts +239 -0
  241. package/src/sandbox/sync.ts +157 -0
  242. package/src/skills/loader.ts +143 -0
  243. package/src/skills/tools.ts +99 -0
  244. package/src/skills/types.ts +13 -0
  245. package/src/test_cache.ts +72 -0
  246. package/src/test_google.js +40 -0
  247. package/src/test_google.ts +40 -0
  248. package/src/tools/askUser.ts +47 -0
  249. package/src/tools/browser.ts +137 -0
  250. package/src/tools/index.d.ts.map +1 -0
  251. package/src/tools/index.ts +237 -0
  252. package/src/tools/registry.ts +198 -0
  253. package/src/tools/router.ts +78 -0
  254. package/src/tools/security.ts +220 -0
  255. package/src/tools/spawnAgent.ts +158 -0
  256. package/src/tools/webSearch.ts +142 -0
  257. package/src/tracing/analyzer.ts +265 -0
  258. package/src/tracing/langsmith.ts +63 -0
  259. package/src/tracing/sessionTracer.ts +202 -0
  260. package/src/tracing/types.ts +49 -0
  261. package/src/types/valyu.d.ts +37 -0
  262. package/src/ui/App.tsx +404 -0
  263. package/src/ui/components/HITLPrompt.tsx +119 -0
  264. package/src/ui/components/Header.tsx +51 -0
  265. package/src/ui/components/MessageBubble.tsx +46 -0
  266. package/src/ui/components/StatusBar.tsx +138 -0
  267. package/src/ui/components/StreamingText.tsx +48 -0
  268. package/src/ui/components/ToolCallPanel.tsx +80 -0
  269. package/tests/commands/commands.test.ts +356 -0
  270. package/tests/core/compactor.test.ts +217 -0
  271. package/tests/core/retryAndErrors.test.ts +164 -0
  272. package/tests/core/sessionResumer.test.ts +95 -0
  273. package/tests/core/sessionStore.test.ts +84 -0
  274. package/tests/core/stability.test.ts +165 -0
  275. package/tests/core/subAgent.test.ts +238 -0
  276. package/tests/hitl/hitlBridge.test.ts +115 -0
  277. package/tsconfig.json +16 -0
  278. package/vitest.config.ts +10 -0
  279. package/vitest.out +48 -0
@@ -0,0 +1,198 @@
1
+ import { DynamicToolInterface } from "./index.js";
2
+
3
+ /**
4
+ * Lazy Tool Registry
5
+ *
6
+ * Instead of loading all complex tools into the System Prompt (which burns tokens
7
+ * and risks cache invalidation if changed), this registry maintains "stubs" —
8
+ * lightweight descriptors that let the agent discover tools on demand.
9
+ *
10
+ * Tools in DeferredToolsDB are NOT sent to the LLM by default. The agent can
11
+ * search for them via SearchToolsTool, then activate them via ActivateToolTool.
12
+ */
13
+
14
+ // ─── Deferred (Lazy) Tools ─────────────────────────────────────────────────────
15
+
16
+ export const DeferredToolsDB: Record<string, DynamicToolInterface> = {
17
+ git_commit: {
18
+ name: "git_commit",
19
+ description: "Creates a new git commit with staged changes.",
20
+ schema: {
21
+ type: "object",
22
+ properties: { message: { type: "string" } },
23
+ required: ["message"],
24
+ },
25
+ execute: async (args) => ({ content: `Committed with message: ${args.message}` }),
26
+ },
27
+ git_diff: {
28
+ name: "git_diff",
29
+ description:
30
+ "Shows the diff of uncommitted changes or between two branches/commits.",
31
+ schema: {
32
+ type: "object",
33
+ properties: {
34
+ target: {
35
+ type: "string",
36
+ description: "Branch, commit, or file path (optional)",
37
+ },
38
+ },
39
+ },
40
+ execute: async (args) => ({ content: `Diff for: ${args.target || "working directory"}` }),
41
+ },
42
+ git_log: {
43
+ name: "git_log",
44
+ description: "Shows recent commit history with messages and hashes.",
45
+ schema: {
46
+ type: "object",
47
+ properties: {
48
+ count: {
49
+ type: "number",
50
+ description: "Number of recent commits to show (default: 10)",
51
+ },
52
+ },
53
+ },
54
+ execute: async (args) => ({ content: `Showing last ${args.count || 10} commits.` }),
55
+ },
56
+ grep_search: {
57
+ name: "grep_search",
58
+ description:
59
+ "Searches for a text pattern across project files using ripgrep. Returns matching lines with filenames and line numbers.",
60
+ schema: {
61
+ type: "object",
62
+ properties: {
63
+ query: { type: "string", description: "Search pattern (regex supported)" },
64
+ path: { type: "string", description: "Directory or file to search in (default: .)" },
65
+ includes: { type: "string", description: "File glob filter (e.g., '*.ts')" },
66
+ },
67
+ required: ["query"],
68
+ },
69
+ execute: async (args) => ({ content: `Search results for '${args.query}'` }),
70
+ },
71
+ list_dir: {
72
+ name: "list_dir",
73
+ description:
74
+ "Lists the contents of a directory — files and subdirectories with sizes.",
75
+ schema: {
76
+ type: "object",
77
+ properties: {
78
+ path: { type: "string", description: "Directory path to list" },
79
+ },
80
+ required: ["path"],
81
+ },
82
+ execute: async (args) => ({ content: `Directory listing for: ${args.path}` }),
83
+ },
84
+ };
85
+
86
+ // ─── Active Tool Set (starts empty, filled by ActivateToolTool) ─────────────
87
+
88
+ const activatedTools: Map<string, DynamicToolInterface> = new Map();
89
+
90
+ /**
91
+ * Returns a tool that has been dynamically activated.
92
+ */
93
+ export function getActivatedTool(name: string): DynamicToolInterface | undefined {
94
+ return activatedTools.get(name);
95
+ }
96
+
97
+ /**
98
+ * Returns all currently activated tools.
99
+ */
100
+ export function getActivatedTools(): DynamicToolInterface[] {
101
+ return Array.from(activatedTools.values());
102
+ }
103
+
104
+ /**
105
+ * Activates a tool from the deferred registry, making it available for execution.
106
+ * Returns the activated tool, or undefined if not found.
107
+ */
108
+ export function activateTool(name: string): DynamicToolInterface | undefined {
109
+ const tool = DeferredToolsDB[name];
110
+ if (!tool) return undefined;
111
+
112
+ activatedTools.set(name, tool);
113
+ return tool;
114
+ }
115
+
116
+ /**
117
+ * Resets all activated tools. For testing.
118
+ */
119
+ export function resetActivatedTools(): void {
120
+ activatedTools.clear();
121
+ }
122
+
123
+ // ─── SearchToolsTool ────────────────────────────────────────────────────────────
124
+
125
+ /**
126
+ * Fuzzy search: matches on tool name OR any word in the description.
127
+ */
128
+ function fuzzyMatch(query: string, tool: DynamicToolInterface): boolean {
129
+ const q = query.toLowerCase();
130
+ const nameMatch = tool.name.toLowerCase().includes(q);
131
+ const descWords = tool.description.toLowerCase();
132
+ const descMatch = descWords.includes(q);
133
+ return nameMatch || descMatch;
134
+ }
135
+
136
+ export const SearchToolsTool: DynamicToolInterface = {
137
+ name: "search_tools",
138
+ description:
139
+ "Search for advanced tools available in the environment. Matches by tool name or description keywords.",
140
+ schema: {
141
+ type: "object",
142
+ properties: {
143
+ query: { type: "string", description: "Search query" },
144
+ },
145
+ required: ["query"],
146
+ },
147
+ execute: async (args: { query: string }) => {
148
+ const matches = Object.values(DeferredToolsDB).filter((tool) =>
149
+ fuzzyMatch(args.query, tool)
150
+ );
151
+
152
+ if (matches.length === 0) {
153
+ return { content: `No tools found matching '${args.query}'. Available categories: git, file, search.` };
154
+ }
155
+
156
+ const descriptions = matches.map(
157
+ (t) => `- **${t.name}**: ${t.description}`
158
+ );
159
+
160
+ return {
161
+ content:
162
+ `Found ${matches.length} tool(s):\n${descriptions.join("\n")}\n\n` +
163
+ `To use a tool, call activate_tool with its name.`
164
+ };
165
+ },
166
+ };
167
+
168
+ // ─── ActivateToolTool ───────────────────────────────────────────────────────────
169
+
170
+ export const ActivateToolTool: DynamicToolInterface = {
171
+ name: "activate_tool",
172
+ description:
173
+ "Activates a discovered tool for use. Call search_tools first to find available tools.",
174
+ schema: {
175
+ type: "object",
176
+ properties: {
177
+ name: { type: "string", description: "The tool name to activate" },
178
+ },
179
+ required: ["name"],
180
+ },
181
+ execute: async (args: { name: string }) => {
182
+ const tool = activateTool(args.name);
183
+
184
+ if (!tool) {
185
+ return {
186
+ content: `Error: Tool '${args.name}' not found in the registry. Use search_tools to see available tools.`,
187
+ isError: true
188
+ };
189
+ }
190
+
191
+ return {
192
+ content:
193
+ `✓ Tool '${args.name}' activated.\n` +
194
+ `Schema: ${JSON.stringify(tool.schema, null, 2)}\n` +
195
+ `You can now call it directly.`
196
+ };
197
+ },
198
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Where a tool executes — on the host machine or in the sandbox.
3
+ */
4
+ export enum ToolTarget {
5
+ /** Run on the host (Node.js process). User sees changes in IDE. */
6
+ HOST = "host",
7
+ /** Run inside the E2B sandbox. Commands are isolated. */
8
+ SANDBOX = "sandbox",
9
+ }
10
+
11
+ /**
12
+ * Tools that execute on the host machine (no code execution risk).
13
+ */
14
+ const HOST_TOOLS = new Set([
15
+ "write_file",
16
+ "read_file",
17
+ "search_tools",
18
+ "activate_tool",
19
+ "list_files",
20
+ "search_files",
21
+ "web_search",
22
+ "search_skills",
23
+ "load_skill",
24
+ "spawn_agent",
25
+ "check_agent",
26
+ ]);
27
+
28
+ /**
29
+ * Tools that execute inside the sandboxed environment.
30
+ */
31
+ const SANDBOX_TOOLS = new Set([
32
+ "bash",
33
+ "run_tests",
34
+ "install_deps",
35
+ "run_command",
36
+ "python",
37
+ "security_scan",
38
+ "dep_scan",
39
+ "browser",
40
+ ]);
41
+
42
+ /**
43
+ * Routes tool calls to either the host machine or the E2B sandbox.
44
+ *
45
+ * Design principle: File I/O runs on the host so the user sees changes
46
+ * in their IDE in real-time. Code execution runs in the sandbox for safety.
47
+ *
48
+ * Unknown tools default to SANDBOX (safe-by-default).
49
+ */
50
+ export class ToolRouter {
51
+ /**
52
+ * Determines where a tool should execute.
53
+ *
54
+ * @param toolName The name of the tool being invoked.
55
+ * @returns ToolTarget.HOST or ToolTarget.SANDBOX
56
+ */
57
+ getTarget(toolName: string): ToolTarget {
58
+ if (HOST_TOOLS.has(toolName)) {
59
+ return ToolTarget.HOST;
60
+ }
61
+ // Default: sandbox (safe-by-default — never execute unknown tools on host)
62
+ return ToolTarget.SANDBOX;
63
+ }
64
+
65
+ /**
66
+ * Returns true if the tool should run on the host.
67
+ */
68
+ isHostTool(toolName: string): boolean {
69
+ return this.getTarget(toolName) === ToolTarget.HOST;
70
+ }
71
+
72
+ /**
73
+ * Returns true if the tool should run in the sandbox.
74
+ */
75
+ isSandboxTool(toolName: string): boolean {
76
+ return this.getTarget(toolName) === ToolTarget.SANDBOX;
77
+ }
78
+ }
@@ -0,0 +1,220 @@
1
+ import { SandboxManager } from "../sandbox/manager.js";
2
+ import { LazyInstaller } from "../sandbox/bootstrap.js";
3
+ import { DynamicToolInterface, ToolResult } from "./index.js";
4
+
5
+ // ─── Sandbox + Installer references (set at session start) ──────────────────
6
+
7
+ let _sandboxManager: SandboxManager | null = null;
8
+ let _installer: LazyInstaller | null = null;
9
+
10
+ /**
11
+ * Binds the security tools to the sandbox and installer.
12
+ * Must be called at session start.
13
+ */
14
+ export function bindSecuritySandbox(
15
+ sandbox: SandboxManager,
16
+ installer: LazyInstaller
17
+ ): void {
18
+ _sandboxManager = sandbox;
19
+ _installer = installer;
20
+ }
21
+
22
+ // ─── Security Helpers ───────────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Escapes a string so it can be safely used as an argument in a Bash shell command.
26
+ */
27
+ function escapeBashArg(arg: string): string {
28
+ return `'${arg.replace(/'/g, "'\\''")}'`;
29
+ }
30
+
31
+ /**
32
+ * Validates a file path to prevent directory traversal out of the workspace.
33
+ */
34
+ function isSafePath(pathStr: string): boolean {
35
+ if (!pathStr || pathStr.trim() === "") return false;
36
+ if (pathStr.includes("..") || pathStr.startsWith("/")) return false;
37
+ return true;
38
+ }
39
+
40
+ // ─── SecurityScanTool ───────────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Scans code for security vulnerabilities using the Gemini CLI Security Extension.
44
+ *
45
+ * Execution flow:
46
+ * 1. LazyInstaller ensures Gemini CLI + security extension are in the sandbox.
47
+ * 2. Runs `gemini -x security:analyze` in the sandbox.
48
+ * 3. Returns the generated security report.
49
+ *
50
+ * If Gemini CLI installation fails, returns a descriptive fallback message
51
+ * suggesting manual review or alternative tools.
52
+ */
53
+ export const SecurityScanTool: DynamicToolInterface = {
54
+ name: "security_scan",
55
+ description:
56
+ "Scans code changes for security vulnerabilities using the Gemini CLI Security Extension. " +
57
+ "Analyzes the current branch diff for common vulnerabilities and generates a security report.",
58
+ schema: {
59
+ type: "object",
60
+ properties: {
61
+ target: {
62
+ type: "string",
63
+ enum: ["changes", "file", "deps"],
64
+ description:
65
+ 'What to scan: "changes" (branch diff), "file" (specific file), "deps" (dependencies only)',
66
+ },
67
+ path: {
68
+ type: "string",
69
+ description:
70
+ "File path for single-file scan (required when target is 'file')",
71
+ },
72
+ },
73
+ required: ["target"],
74
+ },
75
+ execute: async (args: { target: string; path?: string }): Promise<ToolResult> => {
76
+ if (!_sandboxManager || !_sandboxManager.isActive()) {
77
+ return { content: "Sandbox is not active. Cannot run security scan.", isError: true };
78
+ }
79
+ if (!_installer) {
80
+ return { content: "LazyInstaller not initialized. Call bindSecuritySandbox() first.", isError: true };
81
+ }
82
+
83
+ // Ensure Gemini CLI is available
84
+ const cliReady = await _installer.ensureGeminiCli(_sandboxManager);
85
+
86
+ if (!cliReady) {
87
+ return {
88
+ content: (
89
+ "⚠ Gemini CLI could not be installed in the sandbox.\n" +
90
+ "Suggestions:\n" +
91
+ ' - Use `dep_scan` tool for dependency vulnerability scanning (uses npm audit)\n' +
92
+ " - Manually review code for OWASP Top 10 vulnerabilities\n" +
93
+ " - Set sandboxTemplate to a pre-baked template with Gemini CLI installed"
94
+ ),
95
+ isError: true
96
+ };
97
+ }
98
+
99
+ // Build the command based on target
100
+ let command: string;
101
+ switch (args.target) {
102
+ case "changes":
103
+ command = "cd /workspace && gemini -x security:analyze 2>&1";
104
+ break;
105
+ case "file":
106
+ if (!args.path) {
107
+ return { content: "Error: 'path' is required when target is 'file'.", isError: true };
108
+ }
109
+ if (!isSafePath(args.path)) {
110
+ return { content: "Error: Invalid file path. Path must be relative and cannot contain traversal characters ('..').", isError: true };
111
+ }
112
+ command = `cd /workspace && gemini -x security:analyze --file ${escapeBashArg(args.path)} 2>&1`;
113
+ break;
114
+ case "deps":
115
+ command = "cd /workspace && gemini -x security:analyze --deps-only 2>&1";
116
+ break;
117
+ default:
118
+ return { content: `Error: Unknown target "${args.target}". Use "changes", "file", or "deps".`, isError: true };
119
+ }
120
+
121
+ const result = await _sandboxManager.exec(command);
122
+
123
+ if (result.exitCode !== 0) {
124
+ return {
125
+ content: `Security scan failed (exit code ${result.exitCode}):\n${result.stdout}\n${result.stderr}`,
126
+ metadata: { exitCode: result.exitCode },
127
+ isError: true
128
+ };
129
+ }
130
+
131
+ return {
132
+ content: result.stdout || "Security scan completed — no issues found.",
133
+ metadata: { exitCode: result.exitCode },
134
+ isError: false
135
+ };
136
+ },
137
+ };
138
+
139
+ // ─── DepScanTool ────────────────────────────────────────────────────────────────
140
+
141
+ /**
142
+ * Scans project dependencies for known vulnerabilities.
143
+ *
144
+ * Execution flow:
145
+ * 1. Try OSV-Scanner (more comprehensive, covers multiple ecosystems).
146
+ * 2. Fall back to `npm audit --json` (always available in Node sandboxes).
147
+ */
148
+ export const DepScanTool: DynamicToolInterface = {
149
+ name: "dep_scan",
150
+ description:
151
+ "Scans project dependencies for known vulnerabilities (CVEs). " +
152
+ "Uses OSV-Scanner when available, falls back to npm audit.",
153
+ schema: {
154
+ type: "object",
155
+ properties: {
156
+ format: {
157
+ type: "string",
158
+ enum: ["summary", "json"],
159
+ description: 'Output format: "summary" (human readable) or "json" (raw)',
160
+ },
161
+ },
162
+ },
163
+ execute: async (args?: { format?: string }): Promise<ToolResult> => {
164
+ if (!_sandboxManager || !_sandboxManager.isActive()) {
165
+ return { content: "Sandbox is not active. Cannot run dependency scan.", isError: true };
166
+ }
167
+ if (!_installer) {
168
+ return { content: "LazyInstaller not initialized.", isError: true };
169
+ }
170
+
171
+ const format = args?.format ?? "summary";
172
+
173
+ // Try OSV-Scanner first
174
+ const osvReady = await _installer.ensureOsvScanner(_sandboxManager);
175
+
176
+ if (osvReady) {
177
+ const osvCmd =
178
+ format === "json"
179
+ ? "cd /workspace && osv-scanner --json . 2>&1"
180
+ : "cd /workspace && osv-scanner . 2>&1";
181
+
182
+ const result = await _sandboxManager.exec(osvCmd);
183
+
184
+ if (result.exitCode === 0) {
185
+ return {
186
+ content: result.stdout || "No known vulnerabilities found in dependencies.",
187
+ metadata: { exitCode: result.exitCode },
188
+ isError: false
189
+ };
190
+ }
191
+
192
+ // Exit code 1 from OSV-Scanner means vulnerabilities found — still valid output
193
+ if (result.exitCode === 1 && result.stdout) {
194
+ return {
195
+ content: result.stdout,
196
+ metadata: { exitCode: result.exitCode },
197
+ isError: false
198
+ };
199
+ }
200
+
201
+ // If we reach here, OSV-Scanner failed for another reason (e.g. exit > 1)
202
+ console.warn(`⚠ OSV-Scanner failed (exit code ${result.exitCode}). Falling back to npm audit.\nDetails: ${result.stdout}\n${result.stderr}`);
203
+ }
204
+
205
+ // Fallback: npm audit
206
+ const auditCmd =
207
+ format === "json"
208
+ ? "cd /workspace && npm audit --json 2>&1"
209
+ : "cd /workspace && npm audit 2>&1";
210
+
211
+ const auditResult = await _sandboxManager.exec(auditCmd);
212
+
213
+ // npm audit returns 1 when vulnerabilities are found — that's valid output
214
+ return {
215
+ content: auditResult.stdout || "No known vulnerabilities found in dependencies.",
216
+ metadata: { exitCode: auditResult.exitCode },
217
+ isError: false
218
+ };
219
+ },
220
+ };
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Spawn Agent Tools
3
+ *
4
+ * DynamicToolInterface implementations for spawning and checking sub-agents.
5
+ * These tools are registered with the main agent's tool set, allowing the
6
+ * LLM to delegate scoped tasks to isolated sub-agents.
7
+ *
8
+ * Safety: spawn_agent and check_agent are excluded from sub-agent tool sets
9
+ * to enforce the depth-1 nesting limit.
10
+ */
11
+
12
+ import { DynamicToolInterface, ToolResult } from "./index.js";
13
+ import { SubAgentManager } from "../core/subAgent.js";
14
+ import { SubAgentResult } from "../agents/agentSpec.js";
15
+ import { AgentRegistry } from "../agents/agentRegistry.js";
16
+
17
+ // ─── Factory ────────────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Creates the spawn_agent and check_agent tools bound to a SubAgentManager.
21
+ * The agent registry summary is injected into the spawn_agent description
22
+ * so the LLM knows which agents are available.
23
+ */
24
+ export function createSpawnAgentTools(
25
+ manager: SubAgentManager,
26
+ registry: AgentRegistry
27
+ ): DynamicToolInterface[] {
28
+ const agentNames = registry.getNames();
29
+ const agentList = agentNames.join(", ");
30
+
31
+ // ─── spawn_agent ───────────────────────────────────────────────────────────
32
+
33
+ const SpawnAgentTool: DynamicToolInterface = {
34
+ name: "spawn_agent",
35
+ description:
36
+ `Spawn an isolated sub-agent to handle a scoped task. Available agents: ${agentList}. ` +
37
+ `The sub-agent runs independently with its own conversation and returns a structured result. ` +
38
+ `Use mode "async" for non-blocking execution — you can continue working and check results later with check_agent.`,
39
+ schema: {
40
+ type: "object",
41
+ properties: {
42
+ agent: {
43
+ type: "string",
44
+ description: `The agent name from the registry. Available: ${agentList}`,
45
+ enum: agentNames,
46
+ },
47
+ task: {
48
+ type: "string",
49
+ description: "Detailed description of the task for the sub-agent to complete",
50
+ },
51
+ mode: {
52
+ type: "string",
53
+ enum: ["sync", "async"],
54
+ description: "Execution mode: 'sync' (default, blocks until done) or 'async' (non-blocking, returns taskId)",
55
+ },
56
+ },
57
+ required: ["agent", "task"],
58
+ },
59
+ execute: async (args: {
60
+ agent: string;
61
+ task: string;
62
+ mode?: "sync" | "async";
63
+ }): Promise<ToolResult> => {
64
+ const mode = args.mode ?? "sync";
65
+
66
+ if (mode === "async") {
67
+ try {
68
+ const taskId = await manager.spawnAsync(args.agent, args.task);
69
+ return {
70
+ content: `Async sub-agent "${args.agent}" started. Task ID: ${taskId}\n` +
71
+ `Use check_agent with this taskId to get the result when ready.`,
72
+ metadata: { taskId, agentName: args.agent, mode: "async" },
73
+ };
74
+ } catch (err: any) {
75
+ return { content: `Spawn error: ${err.message}`, isError: true };
76
+ }
77
+ }
78
+
79
+ // Sync mode (default)
80
+ const result = await manager.spawn(args.agent, args.task);
81
+ return {
82
+ content: formatSubAgentResult(result),
83
+ metadata: {
84
+ agentName: result.agentName,
85
+ outcome: result.outcome,
86
+ toolCalls: result.toolCallCount,
87
+ turnsUsed: result.turnsUsed,
88
+ duration: result.duration,
89
+ },
90
+ isError: result.outcome === "failure",
91
+ };
92
+ },
93
+ };
94
+
95
+ // ─── check_agent ───────────────────────────────────────────────────────────
96
+
97
+ const CheckAgentTool: DynamicToolInterface = {
98
+ name: "check_agent",
99
+ description:
100
+ "Check the status or retrieve the result of an async sub-agent task. " +
101
+ "If the task is still running, returns a status update. If completed, returns the result.",
102
+ schema: {
103
+ type: "object",
104
+ properties: {
105
+ taskId: {
106
+ type: "string",
107
+ description: "The task ID returned by spawn_agent in async mode",
108
+ },
109
+ },
110
+ required: ["taskId"],
111
+ },
112
+ execute: async (args: { taskId: string }): Promise<ToolResult> => {
113
+ const result = await manager.getResult(args.taskId);
114
+
115
+ if (typeof result === "string") {
116
+ // Still running or unknown
117
+ return { content: result };
118
+ }
119
+
120
+ // Completed — return formatted result
121
+ return {
122
+ content: formatSubAgentResult(result),
123
+ metadata: {
124
+ agentName: result.agentName,
125
+ outcome: result.outcome,
126
+ toolCalls: result.toolCallCount,
127
+ turnsUsed: result.turnsUsed,
128
+ duration: result.duration,
129
+ },
130
+ isError: result.outcome === "failure",
131
+ };
132
+ },
133
+ };
134
+
135
+ return [SpawnAgentTool, CheckAgentTool];
136
+ }
137
+
138
+ // ─── Formatter ──────────────────────────────────────────────────────────────────
139
+
140
+ /**
141
+ * Formats a SubAgentResult into a readable string for the main agent.
142
+ */
143
+ function formatSubAgentResult(result: SubAgentResult): string {
144
+ const lines = [
145
+ `--- Sub-Agent Result: ${result.agentName} ---`,
146
+ `Outcome: ${result.outcome}`,
147
+ `Turns: ${result.turnsUsed} | Tool Calls: ${result.toolCallCount} | Duration: ${Math.round(result.duration / 1000)}s`,
148
+ `Tokens: ~${result.tokenUsage.prompt} prompt + ~${result.tokenUsage.completion} completion`,
149
+ ];
150
+
151
+ if (result.filesModified.length > 0) {
152
+ lines.push(`Files Modified: ${result.filesModified.join(", ")}`);
153
+ }
154
+
155
+ lines.push(`\nResult:\n${result.result}`);
156
+
157
+ return lines.join("\n");
158
+ }