@hachej/boring-agent 0.1.17 → 0.1.20
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/{sandbox-handle-store-hK76cTjn.d.ts → agentPluginEvents-zyIvVjsA.d.ts} +28 -32
- package/dist/{chunk-F3CE5CNW.js → chunk-B5JECXMG.js} +5 -7
- package/dist/front/index.d.ts +23 -7
- package/dist/front/index.js +764 -421
- package/dist/front/styles.css +61 -0
- package/dist/{tool-ui-DSmWuqGe.d.ts → harness-DRrTn_5T.d.ts} +43 -24
- package/dist/server/index.d.ts +79 -10
- package/dist/server/index.js +251 -60
- package/dist/shared/index.d.ts +5 -3
- package/dist/shared/index.js +3 -1
- package/dist/tool-ui-DIFNGwYd.d.ts +20 -0
- package/docs/ACCESSIBILITY.md +55 -0
- package/docs/API.md +64 -0
- package/docs/CSP.md +40 -0
- package/docs/ERROR_CODES.md +54 -0
- package/docs/KNOWN_LIMITATIONS.md +100 -0
- package/docs/MIGRATION.md +50 -0
- package/docs/PERFORMANCE.md +68 -0
- package/docs/PLUGINS.md +108 -0
- package/docs/README.md +17 -0
- package/docs/RISKS-MULTI-TAB.md +46 -0
- package/docs/STYLING.md +71 -0
- package/docs/UI-SHADCN.md +56 -0
- package/docs/VERCEL_COSTS.md +103 -0
- package/docs/plans/AGENT_EVAL_FRAMEWORK.md +466 -0
- package/docs/plans/agent-package-spec.md +1777 -0
- package/docs/plans/harness-followup-capabilities.md +440 -0
- package/docs/plans/harness-tool-ui-capabilities.md +144 -0
- package/docs/plans/pi-followup-history-projection.md +229 -0
- package/docs/plans/pi-tools-migration.md +1061 -0
- package/docs/plans/reviews/pi-followup-history-codex-review.md +11 -0
- package/docs/plans/reviews/pi-followup-history-gpt-review.md +64 -0
- package/docs/plans/reviews/pi-followup-history-opus-review.md +43 -0
- package/docs/plans/reviews/pi-followup-history-xai-review.md +43 -0
- package/docs/plans/vercel-base-snapshot-template-plan.md +527 -0
- package/docs/plans/vercel-persistent-sandbox-adapter.md +553 -0
- package/docs/runtime.md +56 -0
- package/docs/tools.md +75 -0
- package/package.json +4 -3
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
# Plan — Lean on pi's tools; plug our backends per-mode via Operations
|
|
2
|
+
|
|
3
|
+
**Status:** draft v4 — codex r5 catch-up (2026-04-29). NOTE: the live code already moved past this plan's Phase 0/1 milestones — `standardCatalog.ts` no longer exists at `packages/agent/src/server/catalog/`; `createAgentApp.ts:91` already uses `buildHarnessAgentTools(bundle) + buildFilesystemAgentTools(bundle)` directly. Sections below describing migration FROM `standardCatalog` are historical record; READ as "this is how we got here." Phase 1.5 of PLUGIN_MODEL.md (workspace) is the consumer-side counterpart now in flight.
|
|
4
|
+
**Path:** `boring-ui-v2/packages/agent/`
|
|
5
|
+
**Sequencing:** ships first; PLUGIN_MODEL.md (workspace) lands after.
|
|
6
|
+
|
|
7
|
+
## TL;DR
|
|
8
|
+
|
|
9
|
+
We currently hand-roll six tool implementations under pi-adjacent names
|
|
10
|
+
(`bash`, `read`, `write`, `edit`, `find`, `grep`) and shadow pi's
|
|
11
|
+
defaults via `customTools`. **Pi was designed to support exactly our use
|
|
12
|
+
case** — issue [#564](https://github.com/badlogic/pi-mono/issues/564)
|
|
13
|
+
explicitly added the `XxxOperations` seam "for remote delegation" with an
|
|
14
|
+
SSH example as its acceptance criteria. We should adopt pi's tools and plug
|
|
15
|
+
our backends through Operations, mirroring pi's `examples/extensions/ssh.ts`
|
|
16
|
+
canonical pattern.
|
|
17
|
+
|
|
18
|
+
Two phases:
|
|
19
|
+
|
|
20
|
+
- **Phase 0 — Clean up the existing tool files.** Pure refactor: extract
|
|
21
|
+
shared primitives, drop dead code, fix inconsistencies. ~150 LOC removed.
|
|
22
|
+
Stands alone even if Phase 1 is deferred.
|
|
23
|
+
|
|
24
|
+
- **Phase 1 — Per-mode tool wiring against pi's factories.** Each
|
|
25
|
+
`RuntimeModeAdapter` (`direct`/`local`/`vercel-sandbox`) declares its own
|
|
26
|
+
tool list. Most tools are pi defaults verbatim; modes only override
|
|
27
|
+
Operations where the underlying platform actually differs.
|
|
28
|
+
|
|
29
|
+
After both phases: **~570 LOC of code we own** vs ~1830 today — a **75%
|
|
30
|
+
reduction** on this surface. Plus we gain pi's polish for free (process-tree
|
|
31
|
+
kill on abort, macOS path-fuzzy-match, auto-fd/rg-download, smarter
|
|
32
|
+
truncation, .gitignore-aware globber).
|
|
33
|
+
|
|
34
|
+
## Design principles (locked)
|
|
35
|
+
|
|
36
|
+
Three principles govern every decision in this plan. When in doubt, default
|
|
37
|
+
to the lower-numbered principle:
|
|
38
|
+
|
|
39
|
+
### Principle 1 — Leverage pi's model-facing surface verbatim
|
|
40
|
+
|
|
41
|
+
Tool **names**, **schemas**, **prompt snippets**, and **guideline phrasing**
|
|
42
|
+
all come from pi unchanged. The model sees a stock pi agent. This means:
|
|
43
|
+
|
|
44
|
+
- Use pi's tool names (`find`, `grep`, `read`, `write`, `edit`, `bash`,
|
|
45
|
+
`ls`) — never our own variants.
|
|
46
|
+
- Use pi's TypeBox schemas as authored — never re-declare them in our
|
|
47
|
+
code.
|
|
48
|
+
- Use pi's `promptSnippet` wording — never wrap it with our own bullets
|
|
49
|
+
or guideline bullets.
|
|
50
|
+
- Future pi tool releases (e.g. a hypothetical `tree`) flow into our
|
|
51
|
+
catalog with zero shape work on our side.
|
|
52
|
+
|
|
53
|
+
The motivation: **cross-agent conventions**. Models trained on
|
|
54
|
+
pi/Claude Code/Cursor naming and schemas perform better with consistent
|
|
55
|
+
surface area across agents. Diverging is a tax we pay every turn.
|
|
56
|
+
|
|
57
|
+
### Principle 2 — Replace internal behavior via Operations / spawnHooks
|
|
58
|
+
|
|
59
|
+
When we need different *behavior* (sandboxed exec, remote VM, audit
|
|
60
|
+
logging), we plug it in through pi's `XxxOperations` seam — never by
|
|
61
|
+
shadowing pi's tool with our own definition. This is the entire point of
|
|
62
|
+
pi-mono#564 and the canonical SSH/sandbox extension patterns.
|
|
63
|
+
|
|
64
|
+
Consequence: every line of code we own is in **`Operations` adapters,
|
|
65
|
+
`spawnHook`s, or shared resource bundles** — not in tool definitions
|
|
66
|
+
themselves. Adding a new sandbox provider (Modal, Docker, Apple
|
|
67
|
+
container) is "implement six Operations interfaces" — not "rewrite six
|
|
68
|
+
tools."
|
|
69
|
+
|
|
70
|
+
### Principle 3 — Add custom AgentTools only when pi cannot be made to work
|
|
71
|
+
|
|
72
|
+
Custom AgentTools (registered via `customTools` through
|
|
73
|
+
`tool-adapter.ts`) exist for **two cases only**:
|
|
74
|
+
|
|
75
|
+
1. **Pi has no equivalent.** Example: `executeIsolatedCodeTool` — pi
|
|
76
|
+
doesn't ship per-call ephemeral sandboxes.
|
|
77
|
+
2. **Pi's tool architecture cannot accommodate our backend.** Example:
|
|
78
|
+
`vercelGrepTool` — pi's `GrepOperations` cannot redirect the
|
|
79
|
+
ripgrep spawn (only `isDirectory` + `readFile` for context lines),
|
|
80
|
+
so in vercel-sandbox mode we *must* roll our own that runs `rg`
|
|
81
|
+
inside the VM via `sandbox.exec`. **Even then we mirror pi's grep
|
|
82
|
+
schema 1:1** so the model's grep behavior is identical across modes
|
|
83
|
+
— Principle 1 still applies to the schema.
|
|
84
|
+
|
|
85
|
+
Any new addition to `customTools` must justify itself against these two
|
|
86
|
+
cases in its PR description. If neither applies, it's an Operations
|
|
87
|
+
adapter waiting to happen.
|
|
88
|
+
|
|
89
|
+
### Anti-patterns these principles forbid
|
|
90
|
+
|
|
91
|
+
- Shadowing pi's tool names with re-implementations under the same name.
|
|
92
|
+
- Re-declaring pi's schemas (even "for clarity"). The schema lives in pi.
|
|
93
|
+
- Adding per-tool guideline bullets (`Prefer the X tool when...`) — pi's
|
|
94
|
+
catalog is sufficient signal; one line per tool is too many.
|
|
95
|
+
- Wrapping a pi tool's `promptSnippet` with our own prefix. Pi's wording
|
|
96
|
+
IS the wording.
|
|
97
|
+
- Adding a custom tool because "we have a slightly different parameter
|
|
98
|
+
shape we want." Reshape via Operations input/output translation
|
|
99
|
+
instead, or just adopt pi's shape (it's almost always richer).
|
|
100
|
+
|
|
101
|
+
## Why
|
|
102
|
+
|
|
103
|
+
### Original spec rationale was thin
|
|
104
|
+
|
|
105
|
+
`agent-package-spec.md` decision #4 originally said Boring would supply its
|
|
106
|
+
own `tools: [...]` instead of pi's defaults, with no further justification.
|
|
107
|
+
Decision #6 budgeted ~200 LOC for tool factories and deferred dedicated
|
|
108
|
+
search/list tools to shell usage. We've drifted: 1100+ LOC across 6 tools, 5×
|
|
109
|
+
budget, with search tool names shipped off-spec before the rename to `find`/`grep`.
|
|
110
|
+
|
|
111
|
+
### Pi has the Operations seam expressly for this
|
|
112
|
+
|
|
113
|
+
Issue [#564 (closed)](https://github.com/badlogic/pi-mono/issues/564) — the
|
|
114
|
+
RFC that introduced `XxxOperations` interfaces — opens with: *"Make built-in
|
|
115
|
+
tool core operations pluggable so extensions can delegate to remote systems
|
|
116
|
+
(e.g., SSH)."* Acceptance criteria included shipping an SSH example. That
|
|
117
|
+
example (`examples/extensions/ssh.ts`) is the canonical reference for our
|
|
118
|
+
vercel-sandbox mode.
|
|
119
|
+
|
|
120
|
+
### Pi's defaults are strictly better than ours where they apply
|
|
121
|
+
|
|
122
|
+
| Capability | Pi default | Ours |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| Process-tree kill on bash abort | ✅ via `killProcessTree(pid)` | ❌ child only |
|
|
125
|
+
| Auto-download `fd` / `rg` if missing | ✅ via `ensureTool()` | ❌ |
|
|
126
|
+
| macOS path NFD / curly-quote / AM-PM normalization on read | ✅ via `resolveReadPath` | ❌ |
|
|
127
|
+
| Smart output truncation (head/tail with first-line guard) | ✅ via `truncateHead` | ⚠️ basic |
|
|
128
|
+
| `.gitignore`-aware find with `--no-require-git` | ✅ via fd | ✅ via FileSearch |
|
|
129
|
+
|
|
130
|
+
### Two pi extensions are de-facto reference templates
|
|
131
|
+
|
|
132
|
+
- `examples/extensions/ssh.ts` — remote backend per-tool override. Lift this
|
|
133
|
+
pattern verbatim for vercel-sandbox.
|
|
134
|
+
- `examples/extensions/sandbox/index.ts` — OS-level sandboxing via
|
|
135
|
+
`SandboxManager.wrapWithSandbox(command)` from
|
|
136
|
+
`@anthropic-ai/sandbox-runtime`. Lift the `spawnHook`/wrapped-bash pattern
|
|
137
|
+
for bwrap mode.
|
|
138
|
+
|
|
139
|
+
### Community signals from pi-mono issues
|
|
140
|
+
|
|
141
|
+
- **#3782** — *"Default to using custom `grep` tool to prevent context
|
|
142
|
+
pollution and token waste"*: pi made grep a default tool because bare
|
|
143
|
+
`bash grep` over `node_modules` burned ~98k tokens in real sessions.
|
|
144
|
+
Implication: **don't drop dedicated grep — preserve smart-grep behavior
|
|
145
|
+
even in vercel-sandbox**.
|
|
146
|
+
- **#3500** — *"Discover skills on remote machine when routing tools through
|
|
147
|
+
SSH"*: SSH extension authors are actively extending the pattern. Pi is
|
|
148
|
+
iterating to make the remote story stronger.
|
|
149
|
+
- **#3320 / #2965** — remote context-file (AGENTS.md/CLAUDE.md) loading via
|
|
150
|
+
SSH extension. Pi is solving the full agent-runs-against-remote-system
|
|
151
|
+
case, not just bash forwarding.
|
|
152
|
+
- **No open issues** on ssh/remote → the pattern is mature; we're catching
|
|
153
|
+
up to where pi's community already is.
|
|
154
|
+
|
|
155
|
+
## Phase 0 — Clean up the existing tool files (precursor)
|
|
156
|
+
|
|
157
|
+
Pure refactor. No behavior change. Independent of Phase 1 — even if Phase 1
|
|
158
|
+
were deferred indefinitely, this step stands alone.
|
|
159
|
+
|
|
160
|
+
### Audit findings (cross-file duplication, inconsistencies, dead code)
|
|
161
|
+
|
|
162
|
+
#### Cross-file duplication
|
|
163
|
+
|
|
164
|
+
1. `makeError(message)` declared identically in all 6 files.
|
|
165
|
+
2. `FileChangeMetadata` declared 3 times with one divergent field.
|
|
166
|
+
3. `bytesWritten()` identical in `editTool.ts:68-70` and `writeTool.ts:81-83`.
|
|
167
|
+
4. `nowIso()` identical in `editTool.ts:72-74` and `writeTool.ts:85-87`.
|
|
168
|
+
5. `normalizeLimit()` identical in `findFilesTool.ts:22-36` and
|
|
169
|
+
`grepFilesTool.ts:32-46`.
|
|
170
|
+
6. Two separate `TextDecoder('utf-8', { fatal: false })` instances.
|
|
171
|
+
7. `DEFAULT_LIMIT = 200`, `MAX_LIMIT = 5_000`, `MAX_*_LENGTH = 256`
|
|
172
|
+
repeated.
|
|
173
|
+
|
|
174
|
+
#### Inconsistencies
|
|
175
|
+
|
|
176
|
+
8. `bashTool.ts:209-214` inlines its error object instead of using
|
|
177
|
+
`makeError`.
|
|
178
|
+
9. `bashTool.ts` is the only file that doesn't check
|
|
179
|
+
`ctx.abortSignal.aborted` at the start.
|
|
180
|
+
10. `additionalProperties: false` on the JSON schema only on
|
|
181
|
+
`findFilesTool` and `grepFilesTool`.
|
|
182
|
+
11. Param descriptions only on `grepFilesTool`.
|
|
183
|
+
12. `grepFilesTool.ts:64` uses `parseInt(..., 10)`; rest use
|
|
184
|
+
`Number.isInteger`.
|
|
185
|
+
13. Param-typing approach varies (raw `params`, `XxxParams` interface,
|
|
186
|
+
`as Record<string, unknown>`).
|
|
187
|
+
14. Error phrasing varies (`"X aborted"` vs `"X failed: msg"` vs
|
|
188
|
+
`"unknown X failure"`).
|
|
189
|
+
|
|
190
|
+
#### Dead / questionable code
|
|
191
|
+
|
|
192
|
+
15. `readTool.ts:97` — stale TODO referencing a long-shipped milestone.
|
|
193
|
+
16. `bashTool.ts:9-11` — single-use `decode()` helper.
|
|
194
|
+
17. `editTool.ts:46-66` — two near-identical replace functions; can be one.
|
|
195
|
+
18. `writeTool.ts:75-79` — `makeTmpPath` sanitizes `toolCallId` even
|
|
196
|
+
though pi guarantees UUIDs. Either real defense-in-depth (document) or
|
|
197
|
+
dead.
|
|
198
|
+
19. `findFilesTool.ts:54` — description mentions `vercel-sandbox mode`
|
|
199
|
+
even though the tool has no sandbox knowledge.
|
|
200
|
+
20. `bashTool.ts:23-194` — 170 lines of shell-parsing for
|
|
201
|
+
`inferBashFileChanges`. Wrong file. **Defer to a separate PR** —
|
|
202
|
+
independent of this plan.
|
|
203
|
+
21. `bashTool.ts:234-244` — emits the same fields twice (text + details);
|
|
204
|
+
document why.
|
|
205
|
+
|
|
206
|
+
### What `_shared.ts` exports
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// src/server/catalog/tools/_shared.ts
|
|
210
|
+
export function makeError(message: string): ToolResult
|
|
211
|
+
export function bytesWritten(content: string): number
|
|
212
|
+
export function nowIso(): string
|
|
213
|
+
export function normalizeLimit(
|
|
214
|
+
raw: unknown,
|
|
215
|
+
opts: { default: number; max: number },
|
|
216
|
+
): { limit: number; error?: string }
|
|
217
|
+
|
|
218
|
+
export const decoder: TextDecoder
|
|
219
|
+
export function decode(bytes: Uint8Array): string
|
|
220
|
+
|
|
221
|
+
export type FileChangeOp = 'write' | 'edit' | 'unlink' | 'rename' | 'mkdir'
|
|
222
|
+
export interface FileChangeMetadata {
|
|
223
|
+
op: FileChangeOp
|
|
224
|
+
path: string
|
|
225
|
+
oldPath?: string
|
|
226
|
+
timestamp: string
|
|
227
|
+
size?: number
|
|
228
|
+
/** Distinguishes file:created from file:changed. Universal field
|
|
229
|
+
* (set even by edit ops, where it's always true). */
|
|
230
|
+
existsBefore?: boolean
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export const DEFAULT_TOOL_LIMIT = 200
|
|
234
|
+
export const MAX_TOOL_LIMIT = 5_000
|
|
235
|
+
export const MAX_PATTERN_LENGTH = 256
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Phase 0 standardization decisions
|
|
239
|
+
|
|
240
|
+
1. **`existsBefore` on `FileChangeMetadata` becomes universal.** `editTool`
|
|
241
|
+
starts emitting `existsBefore: true` so the workspace bridge sees a
|
|
242
|
+
consistent shape.
|
|
243
|
+
2. **`additionalProperties: false` everywhere.** Tightens the model-facing
|
|
244
|
+
tool contract.
|
|
245
|
+
3. **`toolCallId` sanitization in `writeTool`** stays as defense-in-depth
|
|
246
|
+
with a one-line comment.
|
|
247
|
+
|
|
248
|
+
### Phase 0 verification
|
|
249
|
+
|
|
250
|
+
- `pnpm exec tsc --noEmit` clean.
|
|
251
|
+
- All 6 tool tests + new `_shared.test.ts` green.
|
|
252
|
+
- `bash scripts/check-invariants.sh packages/agent` passes.
|
|
253
|
+
- No tool file imports `makeError`/`bytesWritten`/`nowIso`/`normalizeLimit`
|
|
254
|
+
from anywhere except `_shared.ts`.
|
|
255
|
+
|
|
256
|
+
### Phase 0 estimated scope
|
|
257
|
+
|
|
258
|
+
- New: `_shared.ts` (~80 LOC) + `_shared.test.ts` (~50 LOC).
|
|
259
|
+
- Touched: all 6 tool files (~30-40% smaller each).
|
|
260
|
+
- Net: ~ **-20 LOC** (mostly redistribution).
|
|
261
|
+
- Risk: very low.
|
|
262
|
+
|
|
263
|
+
## Phase 1 — Per-mode tool wiring against pi's factories
|
|
264
|
+
|
|
265
|
+
The cleanest design is **bundle factories that produce mode-appropriate
|
|
266
|
+
tools**, called only from `createAgentApp`'s `standardCatalog` path.
|
|
267
|
+
Per PLUGIN_MODEL.md v7.0+: filesystemPlugin is UI-only (panels +
|
|
268
|
+
catalog) and does NOT carry `agentTools` — file tools are HARNESS
|
|
269
|
+
substrate. Each bundle factory takes a `RuntimeBundle` and returns
|
|
270
|
+
`AgentTool[]` constructed via pi's factories with mode-specific
|
|
271
|
+
Operations injected.
|
|
272
|
+
|
|
273
|
+
`RuntimeModeAdapter` does NOT carry a `tools` field. Modes own *resources*
|
|
274
|
+
(workspace, sandbox, fileSearch); they don't own tool registration. Tool
|
|
275
|
+
registration is split across two bundles, both per-mode-aware:
|
|
276
|
+
|
|
277
|
+
- `buildHarnessAgentTools(bundle)` — produces `[bash,
|
|
278
|
+
executeIsolatedCode]`. Always registered by `createAgentApp` directly
|
|
279
|
+
(per PLUGIN_MODEL.md line 122: bash + isolated-code stay harness-level).
|
|
280
|
+
- `buildFilesystemAgentTools(bundle)` — produces `[read, write, edit,
|
|
281
|
+
find, grep, ls]`. Registered by `createAgentApp`'s
|
|
282
|
+
`standardCatalog` by default; opt out via `disableDefaultFileTools:
|
|
283
|
+
true`. **NOT** registered through any plugin (PLUGIN_MODEL v7.0+:
|
|
284
|
+
filesystemPlugin is UI-only, no `agentTools` field).
|
|
285
|
+
|
|
286
|
+
This honors PLUGIN_MODEL.md v7.0's separation of concerns: file tools
|
|
287
|
+
are HARNESS substrate (controlled by `disableDefaultFileTools`); the
|
|
288
|
+
filesystem plugin contributes UI only (controlled by `excludeDefaults`).
|
|
289
|
+
Two switches at two layers.
|
|
290
|
+
|
|
291
|
+
### Alignment with PLUGIN_MODEL.md
|
|
292
|
+
|
|
293
|
+
PLUGIN_MODEL.md (workspace) defines the plugin pipeline that consumes our
|
|
294
|
+
file-ops tools. Without any change to its contract, this plan slots in
|
|
295
|
+
cleanly via two adjustments:
|
|
296
|
+
|
|
297
|
+
1. **`filesystemPlugin` is UI-only** (PLUGIN_MODEL v7.0+). It does
|
|
298
|
+
NOT carry `agentTools`; it's a plain module-scope const with
|
|
299
|
+
`panels` (FileTree, CodeEditor, MarkdownEditor) + `catalogs`
|
|
300
|
+
(Files, hitting `/api/v1/files/search`). File tools live with
|
|
301
|
+
the harness:
|
|
302
|
+
```ts
|
|
303
|
+
// packages/workspace/src/plugin/defaults/filesystemPlugin.ts (v7.0+)
|
|
304
|
+
export const filesystemPlugin: Plugin = definePlugin({
|
|
305
|
+
id: 'filesystem',
|
|
306
|
+
label: 'Filesystem',
|
|
307
|
+
panels: [/* FileTree, CodeEditor, MarkdownEditor */],
|
|
308
|
+
catalogs: [filesCatalog],
|
|
309
|
+
// No agentTools — file tools are harness substrate.
|
|
310
|
+
})
|
|
311
|
+
```
|
|
312
|
+
`createAgentApp` registers `buildFilesystemAgentTools(bundle)` +
|
|
313
|
+
`buildHarnessAgentTools(bundle)` directly via `standardCatalog`.
|
|
314
|
+
`createWorkspaceAgentApp` plain-wraps `createAgentApp` and runs
|
|
315
|
+
the plugin model bootstrap on top — no dual-registration dance,
|
|
316
|
+
no `disableDefaultFileTools: true` indirection.
|
|
317
|
+
2. **Tool name update.** Search tool references in
|
|
318
|
+
PLUGIN_MODEL.md (lines 1145, 1550-1554) become `find`/`grep`. Doc-only
|
|
319
|
+
change; can co-land or follow up.
|
|
320
|
+
|
|
321
|
+
### Five PLUGIN_MODEL guarantees we preserve
|
|
322
|
+
|
|
323
|
+
1. Plugin contract `Plugin.agentTools: AgentTool[]` unchanged — array is
|
|
324
|
+
constructed at plugin instantiation.
|
|
325
|
+
2. Late-wins-on-id works — operates on the array; doesn't care how
|
|
326
|
+
entries were constructed.
|
|
327
|
+
3. `excludeDefaults: ['filesystem']` (workspace switch) removes the
|
|
328
|
+
filesystem UI (Files left-tab + code/markdown editor auto-routing).
|
|
329
|
+
It does NOT remove LLM file tools — those are harness substrate.
|
|
330
|
+
Use `disableDefaultFileTools: true` (harness switch) on
|
|
331
|
+
`createAgentApp` to remove file tools entirely. Two switches at
|
|
332
|
+
two layers, per PLUGIN_MODEL v7.0's separation of concerns.
|
|
333
|
+
4. Standalone path stays default-on with `disableDefaultFileTools` opt-out
|
|
334
|
+
— `createAgentApp` calls `buildFilesystemAgentTools` directly via
|
|
335
|
+
`standardCatalog`. Plugin pipeline never sees file tools.
|
|
336
|
+
5. Bash + isolated-code stay harness-level — they flow through
|
|
337
|
+
`buildHarnessAgentTools` which `createAgentApp` always registers
|
|
338
|
+
directly, never via plugin.
|
|
339
|
+
|
|
340
|
+
### How pi resolves paths (architectural prerequisite)
|
|
341
|
+
|
|
342
|
+
Pi expects model-supplied paths and resolves them against `cwd` via
|
|
343
|
+
`resolveToCwd(filePath, cwd)` in `dist/core/tools/path-utils.js`. **Pi does
|
|
344
|
+
no bounding** — `../../../etc/passwd` resolves cleanly to its absolute path
|
|
345
|
+
and is passed to `Operations.readFile(absPath)`. Path-bounding is 100% the
|
|
346
|
+
Operations adapter's responsibility.
|
|
347
|
+
|
|
348
|
+
This matches `agent-package-spec.md` decision #7a (Workspace enforces
|
|
349
|
+
path-bounding via `validatePath` / `assertRealPathWithinWorkspace`). We move
|
|
350
|
+
the bounding from inside our hand-rolled tool to inside the Operations
|
|
351
|
+
adapter — same security posture, different layer.
|
|
352
|
+
|
|
353
|
+
### `RuntimeModeAdapter` after this plan
|
|
354
|
+
|
|
355
|
+
Modes own only resources, not tools:
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
// src/server/runtime/mode.ts (sketch)
|
|
359
|
+
export interface RuntimeBundle {
|
|
360
|
+
mode: 'direct' | 'local' | 'vercel-sandbox'
|
|
361
|
+
workspace: Workspace
|
|
362
|
+
sandbox: Sandbox
|
|
363
|
+
fileSearch: FileSearch
|
|
364
|
+
// No `tools` field — bundles produce tools, not modes.
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### `buildHarnessAgentTools(bundle)` — bash + isolated-code
|
|
369
|
+
|
|
370
|
+
Always registered by `createAgentApp` directly. Never goes through the
|
|
371
|
+
plugin pipeline (per PLUGIN_MODEL.md non-goal line 122).
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
// src/server/tools/harness/index.ts (sketch)
|
|
375
|
+
import { createBashTool, createLocalBashOperations } from '@mariozechner/pi-coding-agent'
|
|
376
|
+
import { executeIsolatedCodeTool } from '../executeIsolatedCodeTool'
|
|
377
|
+
|
|
378
|
+
export function buildHarnessAgentTools(bundle: RuntimeBundle): AgentTool[] {
|
|
379
|
+
const tools: AgentTool[] = []
|
|
380
|
+
tools.push(createBashTool(bundleCwd(bundle), bashOptionsForMode(bundle)))
|
|
381
|
+
if (bundle.sandbox.capabilities.includes('isolated-code')) {
|
|
382
|
+
tools.push(executeIsolatedCodeTool(bundle.sandbox))
|
|
383
|
+
}
|
|
384
|
+
return tools
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function bashOptionsForMode(bundle: RuntimeBundle): BashToolOptions {
|
|
388
|
+
switch (bundle.mode) {
|
|
389
|
+
case 'direct':
|
|
390
|
+
return { operations: createLocalBashOperations() } // pi default verbatim
|
|
391
|
+
case 'local':
|
|
392
|
+
return {
|
|
393
|
+
operations: createLocalBashOperations(),
|
|
394
|
+
spawnHook: bwrapSpawnHook(bundle.workspace.root), // bwrap wrap
|
|
395
|
+
}
|
|
396
|
+
case 'vercel-sandbox':
|
|
397
|
+
return { operations: vercelBashOps(bundle.sandbox) }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### `buildFilesystemAgentTools(bundle)` — read/write/edit/find/grep/ls
|
|
403
|
+
|
|
404
|
+
Registered by `createAgentApp`'s `standardCatalog` by default; opt out
|
|
405
|
+
via `disableDefaultFileTools: true`. NOT registered via any plugin
|
|
406
|
+
(PLUGIN_MODEL v7.0+: filesystemPlugin is UI-only).
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
// src/server/tools/filesystem/index.ts (sketch)
|
|
410
|
+
import {
|
|
411
|
+
createReadTool, createWriteTool, createEditTool,
|
|
412
|
+
createFindTool, createGrepTool, createLsTool,
|
|
413
|
+
} from '@mariozechner/pi-coding-agent'
|
|
414
|
+
import { boundFs } from '../operations/bound'
|
|
415
|
+
import { vercelReadOps, vercelWriteOps, /* … */ } from '../operations/vercel'
|
|
416
|
+
import { vercelGrepTool } from '../vercelGrepTool'
|
|
417
|
+
|
|
418
|
+
export function buildFilesystemAgentTools(bundle: RuntimeBundle): AgentTool[] {
|
|
419
|
+
const cwd = bundleCwd(bundle)
|
|
420
|
+
switch (bundle.mode) {
|
|
421
|
+
case 'direct':
|
|
422
|
+
case 'local':
|
|
423
|
+
// Workspace IS the host fs; pi's defaults are correct. Path-bound
|
|
424
|
+
// read/write/edit/ls; pi's fd/rg are .gitignore-aware on host fs.
|
|
425
|
+
return [
|
|
426
|
+
createReadTool(cwd, { operations: boundFs(cwd).read }),
|
|
427
|
+
createWriteTool(cwd, { operations: boundFs(cwd).write }),
|
|
428
|
+
createEditTool(cwd, { operations: boundFs(cwd).edit }),
|
|
429
|
+
createFindTool(cwd),
|
|
430
|
+
createGrepTool(cwd),
|
|
431
|
+
createLsTool(cwd, { operations: boundFs(cwd).ls }),
|
|
432
|
+
]
|
|
433
|
+
case 'vercel-sandbox':
|
|
434
|
+
// Workspace is in the VM; route Operations to sandbox.*. Grep is a
|
|
435
|
+
// custom AgentTool because pi's GrepOperations can't redirect the
|
|
436
|
+
// actual rg spawn (see "Why grep is a custom AgentTool" below).
|
|
437
|
+
return [
|
|
438
|
+
createReadTool(cwd, { operations: vercelReadOps(bundle.sandbox) }),
|
|
439
|
+
createWriteTool(cwd, { operations: vercelWriteOps(bundle.sandbox) }),
|
|
440
|
+
createEditTool(cwd, { operations: vercelEditOps(bundle.sandbox) }),
|
|
441
|
+
createFindTool(cwd, { operations: vercelFindOps(bundle.sandbox) }),
|
|
442
|
+
vercelGrepTool(bundle.sandbox), // custom AgentTool, see below
|
|
443
|
+
createLsTool(cwd, { operations: vercelLsOps(bundle.sandbox) }),
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Mode-by-mode summary of what's overridden
|
|
450
|
+
|
|
451
|
+
| Tool | direct | local (bwrap) | vercel-sandbox |
|
|
452
|
+
|---|---|---|---|
|
|
453
|
+
| `bash` | pi default | pi default + `spawnHook` (bwrap wrap) | custom Operations → `sandbox.exec` |
|
|
454
|
+
| `read` | pi default + path-bounding wrapper | same as direct | custom Operations → `sandbox.fs.read` |
|
|
455
|
+
| `write` | pi default + path-bounding wrapper | same as direct | custom Operations → `sandbox.writeFiles` |
|
|
456
|
+
| `edit` | pi default + path-bounding wrapper | same as direct | custom Operations → `sandbox.fs.read+write` |
|
|
457
|
+
| `find` | pi default (auto-downloads `fd`) | same as direct | custom Operations runs `fd` in VM via `sandbox.exec` |
|
|
458
|
+
| `grep` | pi default (auto-downloads `rg`) | same as direct | **custom AgentTool** (pi's GrepOperations can't redirect search) |
|
|
459
|
+
| `ls` | pi default + path-bounding wrapper | same as direct | custom Operations → `sandbox.fs.readdir` |
|
|
460
|
+
|
|
461
|
+
`bwrapSpawnHook(root)` and `boundFs(root)` (with `.read`/`.write`/`.edit`/`.ls`
|
|
462
|
+
sub-objects) are tiny helpers in `src/server/tools/operations/bound.ts`
|
|
463
|
+
(~30 LOC). Vercel Operations adapters live in
|
|
464
|
+
`src/server/tools/operations/vercel.ts` (~150 LOC).
|
|
465
|
+
|
|
466
|
+
### `vercelGrepTool` design
|
|
467
|
+
|
|
468
|
+
Mirrors pi's grep schema 1:1 so the model's grep behavior is identical
|
|
469
|
+
across modes. Spawns `rg` inside the VM via `sandbox.exec` and parses
|
|
470
|
+
ripgrep's output the same way pi does.
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
// src/server/tools/vercelGrepTool.ts (sketch)
|
|
474
|
+
import type { AgentTool } from '../../shared/tool'
|
|
475
|
+
import type { Sandbox } from '../../shared/sandbox'
|
|
476
|
+
import { decode, makeError, normalizeLimit, MAX_PATTERN_LENGTH } from './_shared'
|
|
477
|
+
|
|
478
|
+
const DEFAULT_LIMIT = 200
|
|
479
|
+
|
|
480
|
+
export function vercelGrepTool(sandbox: Sandbox): AgentTool {
|
|
481
|
+
return {
|
|
482
|
+
name: 'grep', // match pi's name
|
|
483
|
+
description:
|
|
484
|
+
'Search file contents by pattern across the workspace. Returns ' +
|
|
485
|
+
'file paths, line numbers, and matching text. Prefer this over ' +
|
|
486
|
+
'shell grep loops to keep results bounded and .gitignore-respecting.',
|
|
487
|
+
parameters: {
|
|
488
|
+
type: 'object',
|
|
489
|
+
properties: {
|
|
490
|
+
pattern: { type: 'string', description: 'Regex pattern (or literal if literal=true).' },
|
|
491
|
+
path: { type: 'string', description: 'Sub-tree to search (default: workspace root).' },
|
|
492
|
+
glob: { type: 'string', description: 'File glob filter (e.g. "*.ts").' },
|
|
493
|
+
ignoreCase: { type: 'boolean' },
|
|
494
|
+
literal: { type: 'boolean', description: 'Treat pattern as literal string, not regex.' },
|
|
495
|
+
context: { type: 'integer', minimum: 0 },
|
|
496
|
+
limit: { type: 'integer', minimum: 1, maximum: 5000 },
|
|
497
|
+
},
|
|
498
|
+
required: ['pattern'],
|
|
499
|
+
additionalProperties: false,
|
|
500
|
+
},
|
|
501
|
+
async execute(input, ctx) {
|
|
502
|
+
// Validate + build rg args mirroring pi's `dist/core/tools/grep.js:130-180`
|
|
503
|
+
// (smart-case, --no-heading, -m limit, -g glob, -C context, --fixed-strings if literal).
|
|
504
|
+
// Run inside the VM:
|
|
505
|
+
// sandbox.exec(`rg ${args.join(' ')}`, { signal, timeoutMs })
|
|
506
|
+
// Parse stdout the same way pi does (file:line:text).
|
|
507
|
+
// Return matches via the same `details: { matches, truncated, ... }` shape
|
|
508
|
+
// pi's createGrepTool does so the renderer match its UI affordance.
|
|
509
|
+
...
|
|
510
|
+
},
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Why a custom AgentTool, not a pi factory:
|
|
516
|
+
|
|
517
|
+
Pi's `GrepOperations` interface (`dist/core/tools/grep.d.ts:21-29`) only
|
|
518
|
+
exposes:
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
interface GrepOperations {
|
|
522
|
+
isDirectory: (absolutePath: string) => Promise<boolean> | boolean
|
|
523
|
+
readFile: (absolutePath: string) => Promise<string> | string
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
The actual ripgrep spawn is hardcoded local
|
|
528
|
+
(`grep.js:144: spawn(rgPath, args, { stdio: ... })`) — the Operations only
|
|
529
|
+
plug in the directory-existence check and the readFile-for-context-lines.
|
|
530
|
+
**There's no seam to redirect the search itself to a remote backend.**
|
|
531
|
+
|
|
532
|
+
Notably, pi's own `examples/extensions/ssh.ts` does **not** override
|
|
533
|
+
`grep` — which means in SSH mode that example produces wrong results
|
|
534
|
+
(searches the agent host, not the remote). The example is incomplete on
|
|
535
|
+
this point. Issue [#3782 (closed)](https://github.com/badlogic/pi-mono/issues/3782)
|
|
536
|
+
made dedicated `grep` a default because bare `bash grep` over `node_modules`
|
|
537
|
+
burned ~98k tokens per session. **We can't drop grep**.
|
|
538
|
+
|
|
539
|
+
So in vercel-sandbox we register our own `grep` AgentTool via `customTools`
|
|
540
|
+
that spawns `rg` inside the VM via `sandbox.exec` and parses output.
|
|
541
|
+
~80 LOC. Pi's `customTools` adapter handles the prompt-snippet and
|
|
542
|
+
schema; the tool's `execute` does the work. **One custom tool**, not six.
|
|
543
|
+
|
|
544
|
+
#### What `vercelXxxOps` looks like
|
|
545
|
+
|
|
546
|
+
Mirrors `examples/extensions/ssh.ts:81-112`. For bash:
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
// src/server/catalog/tools/operations/vercel.ts
|
|
550
|
+
export function vercelBashOps(sandbox: Sandbox): BashOperations {
|
|
551
|
+
return {
|
|
552
|
+
exec(command, cwd, { onData, signal, timeout }) {
|
|
553
|
+
return new Promise((resolve, reject) => {
|
|
554
|
+
const child = sandbox.spawn(['bash', '-c', command], { cwd, env: ... })
|
|
555
|
+
child.stdout.on('data', onData)
|
|
556
|
+
child.stderr.on('data', onData)
|
|
557
|
+
signal?.addEventListener('abort', () => child.kill(), { once: true })
|
|
558
|
+
const timer = timeout
|
|
559
|
+
? setTimeout(() => child.kill(), timeout * 1000)
|
|
560
|
+
: undefined
|
|
561
|
+
child.on('exit', (code) => {
|
|
562
|
+
if (timer) clearTimeout(timer)
|
|
563
|
+
if (signal?.aborted) reject(new Error('aborted'))
|
|
564
|
+
else resolve({ exitCode: code })
|
|
565
|
+
})
|
|
566
|
+
})
|
|
567
|
+
},
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Read/write/edit/ls are similar: forward `(absPath, ...)` to
|
|
573
|
+
`sandbox.fs.read` / `sandbox.writeFiles` / etc. Find's `glob` Operations
|
|
574
|
+
runs `fd` inside the VM via `sandbox.exec` and parses stdout.
|
|
575
|
+
|
|
576
|
+
### What we delete
|
|
577
|
+
|
|
578
|
+
- `src/server/catalog/tools/bashTool.ts`
|
|
579
|
+
- `src/server/catalog/tools/readTool.ts`
|
|
580
|
+
- `src/server/catalog/tools/writeTool.ts`
|
|
581
|
+
- `src/server/catalog/tools/editTool.ts`
|
|
582
|
+
- `src/server/catalog/tools/findFilesTool.ts`
|
|
583
|
+
- `src/server/catalog/tools/grepFilesTool.ts`
|
|
584
|
+
- All co-located test files for the above.
|
|
585
|
+
- Per-tool `promptGuidelines` and the double-dash `promptSnippet` in
|
|
586
|
+
`tool-adapter.ts` (no longer needed for standard tools — pi's factories
|
|
587
|
+
carry their own snippets).
|
|
588
|
+
|
|
589
|
+
`tool-adapter.ts` stays for genuinely-custom tools: `vercelGrepTool`,
|
|
590
|
+
`executeIsolatedCodeTool`, plugin tools, host-supplied `extraTools`.
|
|
591
|
+
|
|
592
|
+
### Tool name change: search tools → `find`/`grep`
|
|
593
|
+
|
|
594
|
+
Pi's tool names are `find` and `grep`. Drop the `_files` suffix to align.
|
|
595
|
+
Callsites to update:
|
|
596
|
+
|
|
597
|
+
- `packages/agent/src/ui-shadcn/workspaceToolRenderers.tsx:30` (renderer
|
|
598
|
+
map keyed by name).
|
|
599
|
+
- `packages/workspace/src/components/chat/workspaceToolRenderers.tsx` if
|
|
600
|
+
distinct.
|
|
601
|
+
- Any test mocks asserting the old names.
|
|
602
|
+
- PLUGIN_MODEL.md (lines 1145, 1550-1554) — doc-only update; can be
|
|
603
|
+
co-landed or follow-up.
|
|
604
|
+
- README / AGENTS.md if mentioned.
|
|
605
|
+
|
|
606
|
+
The model is unaffected — pi's name shows up in the tool catalog each turn;
|
|
607
|
+
no persisted state references the old names.
|
|
608
|
+
|
|
609
|
+
### Migration plan (incremental, mode-by-mode reversible)
|
|
610
|
+
|
|
611
|
+
Sequencing: build the seam first (step 1), then flip one mode at a time
|
|
612
|
+
(steps 2-4) so each mode can be reverted independently if it regresses.
|
|
613
|
+
Hand-rolled tools survive until step 5; they're the rollback target if
|
|
614
|
+
any single mode flip fails.
|
|
615
|
+
|
|
616
|
+
Each step is a single PR-sized chunk. After every step: `tsc --noEmit`
|
|
617
|
+
clean, test suite green, manual smoke against the playground in the
|
|
618
|
+
affected mode(s).
|
|
619
|
+
|
|
620
|
+
#### Step 1.0 — Add streaming callbacks to `Sandbox.exec` (precursor)
|
|
621
|
+
|
|
622
|
+
**New precursor.** Today `ExecOptions` has no `onStdout`/`onStderr`
|
|
623
|
+
callbacks; `Sandbox.exec` returns full output at end. Pi's
|
|
624
|
+
`BashOperations.exec` requires incremental `onData(buf)` calls. This step
|
|
625
|
+
extends our shared interface and all three adapters before any tool
|
|
626
|
+
migration begins.
|
|
627
|
+
|
|
628
|
+
- Extend `ExecOptions` in `packages/agent/src/shared/sandbox.ts` with
|
|
629
|
+
`onStdout?: (chunk: Uint8Array) => void` and
|
|
630
|
+
`onStderr?: (chunk: Uint8Array) => void`. Keep buffered `stdout`/`stderr`
|
|
631
|
+
in `ExecResult` for backward compat — adapter logic both streams AND
|
|
632
|
+
collects (the buffered fields stay populated for non-pi consumers).
|
|
633
|
+
- `DirectSandbox`: switch from `child_process.exec` (buffered) to
|
|
634
|
+
`spawn`; wire `child.stdout.on('data', ...)` to invoke `onStdout` and
|
|
635
|
+
also accumulate into the result buffer.
|
|
636
|
+
- `BwrapSandbox`: same pattern, wrap the bwrap-spawn child.
|
|
637
|
+
- `VercelSandboxExec`: replace `command.stdout() + command.stderr()` with
|
|
638
|
+
either `Command.logs()` async-generator or `runCommand({ stdout:
|
|
639
|
+
writable, stderr: writable })` pipe-to-Writable (SDK supports both;
|
|
640
|
+
`Writable` is simpler). Apply the existing `maxOutputBytes` cap as
|
|
641
|
+
bytes arrive; the buffer never holds more than the cap.
|
|
642
|
+
- Tests: each adapter receives ≥ 2 `onStdout` calls for `seq 1 100`,
|
|
643
|
+
AND the final `ExecResult.stdout` is the full collected output, AND
|
|
644
|
+
`maxOutputBytes` truncation fires correctly under streaming.
|
|
645
|
+
|
|
646
|
+
**Acceptance:** typecheck clean, all sandbox adapter tests green, no
|
|
647
|
+
behavior change for callers that don't pass `onStdout`/`onStderr`.
|
|
648
|
+
|
|
649
|
+
#### Step 1 — Build Operations adapters + bundle factories (pure additive)
|
|
650
|
+
|
|
651
|
+
- New file `src/server/tools/operations/bound.ts` exporting
|
|
652
|
+
`boundFs(root)` returning `{ read, write, edit, ls }` Operations
|
|
653
|
+
sub-objects. Each does abs-path-bounding via
|
|
654
|
+
`assertRealPathWithinWorkspace` (port the helper from
|
|
655
|
+
`packages/agent/src/server/workspace/paths.ts`) before delegating to
|
|
656
|
+
pi's default backend. Symlink resolution included — see R8.
|
|
657
|
+
- New file `src/server/tools/operations/vercel.ts` exporting
|
|
658
|
+
`vercelBashOps`, `vercelReadOps`, `vercelWriteOps`, `vercelEditOps`,
|
|
659
|
+
`vercelFindOps`, `vercelLsOps` — each routes to `sandbox.*` methods.
|
|
660
|
+
- New files `src/server/tools/harness/index.ts` and
|
|
661
|
+
`src/server/tools/filesystem/index.ts` exporting
|
|
662
|
+
`buildHarnessAgentTools(bundle)` and `buildFilesystemAgentTools(bundle)`.
|
|
663
|
+
- New file `src/server/tools/vercelGrepTool.ts` (matches pi's grep
|
|
664
|
+
schema 1:1; see "vercelGrepTool design" above).
|
|
665
|
+
- Tests in `__tests__/`:
|
|
666
|
+
- `boundFs` rejects paths outside workspace, including via symlink.
|
|
667
|
+
- Each `vercel*Ops` adapter mocks `sandbox` and asserts correct
|
|
668
|
+
forwarding.
|
|
669
|
+
- `buildHarnessAgentTools(bundle)` returns the right tools per mode.
|
|
670
|
+
- `buildFilesystemAgentTools(bundle)` returns the right tools per mode
|
|
671
|
+
(incl. `vercelGrepTool` instead of `createGrepTool` for vercel-sandbox).
|
|
672
|
+
- `vercelGrepTool` parses ripgrep output correctly against a fake
|
|
673
|
+
`sandbox.exec`.
|
|
674
|
+
- Nothing else changes. `standardCatalog.ts` still produces the hand-rolled
|
|
675
|
+
tools that the harness uses.
|
|
676
|
+
|
|
677
|
+
#### Step 2 — Flip `direct` mode
|
|
678
|
+
|
|
679
|
+
- In `createAgentApp.ts`, when `mode === 'direct'`, swap from
|
|
680
|
+
`standardCatalog(...)` to `[...buildHarnessAgentTools(bundle),
|
|
681
|
+
...(disableDefaultFileTools ? [] : buildFilesystemAgentTools(bundle))]`.
|
|
682
|
+
- Other modes still use `standardCatalog` — they're unaffected.
|
|
683
|
+
- Smoke: playground (which auto-detects direct mode by default) — bash,
|
|
684
|
+
read, write, edit, find, grep all work; system prompt loses ~700 chars
|
|
685
|
+
of double-dash + per-tool-guideline noise.
|
|
686
|
+
- **Rollback:** revert this PR; hand-rolled tools come back into use for
|
|
687
|
+
direct mode.
|
|
688
|
+
|
|
689
|
+
#### Step 3 — Flip `local` mode (bwrap)
|
|
690
|
+
|
|
691
|
+
- Same swap as step 2 but for `mode === 'local'`. Adds the `bwrapSpawnHook`
|
|
692
|
+
to `bashOptionsForMode` for local.
|
|
693
|
+
- Smoke: spin up local mode (bwrap installed); verify bwrap appears in the
|
|
694
|
+
process tree during a `bash` turn; verify file ops are still
|
|
695
|
+
workspace-bounded.
|
|
696
|
+
- **Rollback:** independent revert.
|
|
697
|
+
|
|
698
|
+
#### Step 4 — Flip `vercel-sandbox` mode
|
|
699
|
+
|
|
700
|
+
- Same swap for `mode === 'vercel-sandbox'`. Adds vercel Operations to
|
|
701
|
+
`bashOptionsForMode` and `buildFilesystemAgentTools`'s vercel branch.
|
|
702
|
+
- Smoke: deploy a vercel-sandbox playground; send a turn that exercises
|
|
703
|
+
bash, read, write, edit, find, grep — all should hit `sandbox.*` (verify
|
|
704
|
+
via sandbox-side log).
|
|
705
|
+
- Update `packages/agent/src/ui-shadcn/workspaceToolRenderers.tsx:30` to
|
|
706
|
+
add a renderer for the (custom) `grep` tool keyed by name. Pi's
|
|
707
|
+
schema-driven render works for the other tools as-is.
|
|
708
|
+
- **Rollback:** independent revert.
|
|
709
|
+
|
|
710
|
+
#### Step 5 — Delete hand-rolled tools + standardCatalog
|
|
711
|
+
|
|
712
|
+
After all three modes have been flipped and burned in for a few days:
|
|
713
|
+
|
|
714
|
+
- Delete `src/server/catalog/tools/{bashTool,readTool,writeTool,editTool,findFilesTool,grepFilesTool}.ts`
|
|
715
|
+
and their co-located tests.
|
|
716
|
+
- Delete `src/server/catalog/standardCatalog.ts`.
|
|
717
|
+
- `tool-adapter.ts` no longer needs the double-dash `promptSnippet` or
|
|
718
|
+
per-tool `promptGuidelines` workarounds; final shape is ~30 LOC mapping
|
|
719
|
+
our `AgentTool` to pi's `ToolDefinition` for genuinely-custom tools
|
|
720
|
+
only (`vercelGrepTool`, `executeIsolatedCodeTool`, plugin tools,
|
|
721
|
+
host-supplied `extraTools`).
|
|
722
|
+
|
|
723
|
+
#### Step 6 — Tool-name renames downstream
|
|
724
|
+
|
|
725
|
+
- Rename legacy search-tool references to `find` and `grep` in:
|
|
726
|
+
- `packages/agent/src/ui-shadcn/workspaceToolRenderers.tsx:30` (renderer
|
|
727
|
+
map keyed by name).
|
|
728
|
+
- `packages/workspace/src/components/chat/workspaceToolRenderers.tsx`
|
|
729
|
+
if distinct.
|
|
730
|
+
- Any test mocks asserting old names.
|
|
731
|
+
- PLUGIN_MODEL.md (lines 1145, 1550-1554) — doc-only update.
|
|
732
|
+
- boring-macro-v2 if it asserts tool names anywhere.
|
|
733
|
+
- README / AGENTS.md if tool names mentioned.
|
|
734
|
+
|
|
735
|
+
Could fold into step 4 if testing burden allows; separate PR if the rename
|
|
736
|
+
turns out to touch many files.
|
|
737
|
+
|
|
738
|
+
#### Step 7 — Optional: `ls` + final cleanup
|
|
739
|
+
|
|
740
|
+
- Spec deferred ls; pi's `createLsTool` is a 5-LOC add per mode in
|
|
741
|
+
`buildFilesystemAgentTools`. Adds an explicit "list directory" affordance
|
|
742
|
+
and reduces `bash ls` usage. Independent decision.
|
|
743
|
+
- Update `agent-package-spec.md` decisions #4 and #6 with post-migration
|
|
744
|
+
reality (or mark superseded by this plan).
|
|
745
|
+
|
|
746
|
+
## Risks and open questions
|
|
747
|
+
|
|
748
|
+
### R1 — Pi schema drift from ours (concrete audit)
|
|
749
|
+
|
|
750
|
+
| Tool | Ours (today) | Pi | Compatibility |
|
|
751
|
+
|---|---|---|---|
|
|
752
|
+
| `read` | `{ path, lineOffset?, lineCount? }` | `{ path, offset?, limit? }` | **Breaking rename.** Field semantics identical (1-indexed line slice). Document in PR. |
|
|
753
|
+
| `write` | `{ path, content, createDirs? }` | `{ path, content }` (mkdir-recursive always) | **Breaking simplification.** Pi always creates parent dirs; we have a flag. Drop the flag in favor of pi's behavior — matches user expectation. |
|
|
754
|
+
| `edit` | `{ path, oldString, newString, replaceAll? }` | `{ path, edits: [{ oldText, newText }] }` | **Breaking restructure.** Pi's shape is strictly more powerful (multi-edit per call, atomic). Drop ours, gain batch-edit for free. Update tool renderers for the new render shape. |
|
|
755
|
+
| `bash` | `{ command }` | `{ command, timeout? }` | **Pure superset.** Add timeout support. |
|
|
756
|
+
| `find` | `{ glob, limit? }` before migration | `{ pattern, path?, limit? }` | **Breaking field rename + new field.** `glob -> pattern`, plus optional `path` arg for sub-tree search. Better. |
|
|
757
|
+
| `grep` | `{ pattern, glob?, limit? }` before migration | `{ pattern, path?, glob?, ignoreCase?, literal?, context?, limit? }` | **Pure superset.** Pi gains literal/case/context flags. **`vercelGrepTool` MUST mirror pi's full schema** so model behavior is consistent across modes. |
|
|
758
|
+
| `ls` (new) | (none) | `{ path?, limit? }` | New affordance, no migration. |
|
|
759
|
+
|
|
760
|
+
All schema breaks are model-facing — the AI SDK sends whatever JSON the
|
|
761
|
+
model emits; pi validates against its TypeBox schema and rejects malformed
|
|
762
|
+
calls. Models trained on pi's schemas (which is most of them — pi's names
|
|
763
|
+
are the cross-agent default) will do BETTER, not worse, after the swap.
|
|
764
|
+
|
|
765
|
+
**Mitigation:** capture the full schema diff per tool in the corresponding
|
|
766
|
+
step's PR description; tool renderers update co-landed (they're keyed by
|
|
767
|
+
tool name + render the input schema). One round of e2e tests per mode
|
|
768
|
+
confirms the model emits correct shapes against the new schemas.
|
|
769
|
+
|
|
770
|
+
### R2 — Pi's tool names are `find`/`grep`
|
|
771
|
+
|
|
772
|
+
Already addressed above. Smoothest place to do the rename is Step 4 (the
|
|
773
|
+
delete-hand-rolled-files step) where every other tool-name reference is
|
|
774
|
+
already being touched.
|
|
775
|
+
|
|
776
|
+
### R3 — `vercelGrepTool` doesn't share pi's grep render
|
|
777
|
+
|
|
778
|
+
Pi's `createGrepTool` ships a render component (`grep.d.ts:42` —
|
|
779
|
+
`ToolDefinition<...>` includes render). Our custom AgentTool gets the
|
|
780
|
+
generic fallback renderer. We can either:
|
|
781
|
+
|
|
782
|
+
- Accept the visual asymmetry between modes (vercel-sandbox grep looks
|
|
783
|
+
different from direct/local).
|
|
784
|
+
- File a feature request (`#3782`-style) to add a `search()` method to
|
|
785
|
+
`GrepOperations` so we could use pi's grep tool with a remote backend.
|
|
786
|
+
|
|
787
|
+
**Mitigation:** ship as-is, file the upstream FR; revisit if the asymmetry
|
|
788
|
+
proves annoying. A custom tool renderer in
|
|
789
|
+
`workspaceToolRenderers.tsx` keyed on `grep` could close the gap.
|
|
790
|
+
|
|
791
|
+
### R4 — `tools: []` was load-bearing
|
|
792
|
+
|
|
793
|
+
Today's `tools: []` deliberately disables pi's `codingTools` to avoid name
|
|
794
|
+
collision with our `customTools`. After migration, we want
|
|
795
|
+
`tools: runtimeBundle.tools` (a list of pi-built factories). The empty-array
|
|
796
|
+
trick must be removed or we'll register zero tools.
|
|
797
|
+
|
|
798
|
+
**Mitigation:** mechanical change in `createHarness.ts`; PR review confirms
|
|
799
|
+
`tools` is not the empty array on completion.
|
|
800
|
+
|
|
801
|
+
### R5 — Output truncation regressions + bash streaming
|
|
802
|
+
|
|
803
|
+
Pi's `BashOperations.exec` requires an `onData(buf)` callback that is
|
|
804
|
+
called incrementally as bytes arrive. Our current `Sandbox.exec` is
|
|
805
|
+
buffered — it returns `Promise<ExecResult>` with full `stdout`/`stderr`
|
|
806
|
+
collected at the end (`packages/agent/src/shared/sandbox.ts:5-36`).
|
|
807
|
+
Calling `onData(allBytes)` once at end works for correctness but blocks
|
|
808
|
+
pi's mid-command truncation logic, risking memory blowup on
|
|
809
|
+
unbounded-output commands (`find / -type f` style) — especially in
|
|
810
|
+
`vercel-sandbox` mode where the buffer accumulates in the VM.
|
|
811
|
+
|
|
812
|
+
**Verified resolution (smoke-tested 2026-04-28):** the `@vercel/sandbox`
|
|
813
|
+
SDK supports streaming via two APIs (`Command.logs()` async-generator
|
|
814
|
+
and `runCommand({ stdout, stderr: Writable })` pipe-to-stream). A smoke
|
|
815
|
+
test ran a 100-line command and observed **80 incremental `Writable.write()`
|
|
816
|
+
calls** spread across the 1.3-second run — first chunk at +421ms, not
|
|
817
|
+
buffered-and-dumped. Abort latency ~12ms. The streaming question is
|
|
818
|
+
empirically resolved.
|
|
819
|
+
|
|
820
|
+
Our current `createVercelSandboxExec.ts:91-97` uses the buffered
|
|
821
|
+
convenience accessors `command.stdout()` + `command.stderr()` by choice,
|
|
822
|
+
not necessity. `DirectSandbox` and `BwrapSandbox` use `child_process`
|
|
823
|
+
which has streamed via `child.stdout.on('data', ...)` since forever.
|
|
824
|
+
|
|
825
|
+
**Mitigation: bead `uhwx.5b` (a new precursor)** extends `ExecOptions`
|
|
826
|
+
with optional `onStdout`/`onStderr` callbacks and updates all three
|
|
827
|
+
sandbox adapters to invoke them as bytes arrive. ~4-6 hours total. Gates
|
|
828
|
+
`uhwx.6` (vercel*Ops) and `uhwx.7` (buildHarnessAgentTools) since both
|
|
829
|
+
need streaming bash.
|
|
830
|
+
|
|
831
|
+
After `uhwx.5b`, pi's bash tool plugs in cleanly:
|
|
832
|
+
|
|
833
|
+
```ts
|
|
834
|
+
return {
|
|
835
|
+
exec(command, cwd, { onData, signal, timeout, env }) {
|
|
836
|
+
return bundle.sandbox.exec(command, {
|
|
837
|
+
cwd, env, signal, timeoutMs: timeout && timeout * 1000,
|
|
838
|
+
onStdout: (buf) => onData(Buffer.from(buf)),
|
|
839
|
+
onStderr: (buf) => onData(Buffer.from(buf)),
|
|
840
|
+
}).then(result => ({ exitCode: result.exitCode }))
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
### R6 — Pi version pin
|
|
846
|
+
|
|
847
|
+
Operations API exists in `pi-coding-agent@0.67.68` (current pin). Locked
|
|
848
|
+
floor.
|
|
849
|
+
|
|
850
|
+
**Mitigation:** none needed — we already exact-pin pi.
|
|
851
|
+
|
|
852
|
+
### R7 — Path-bounding error message must reach the model usefully
|
|
853
|
+
|
|
854
|
+
Today, our hand-rolled tools return `{ isError: true, content: [{ type:
|
|
855
|
+
'text', text: 'path is outside workspace' }] }` — the model sees a clear
|
|
856
|
+
text error. After migration, an Operations adapter that throws gets
|
|
857
|
+
formatted by pi's error path. We need to verify the model sees a
|
|
858
|
+
meaningful message (not "Error: ENOENT" or stack-trace noise).
|
|
859
|
+
|
|
860
|
+
**Mitigation:** in `boundFs`, throw with a specific message:
|
|
861
|
+
```ts
|
|
862
|
+
throw new Error(`path "${rel}" is outside workspace`)
|
|
863
|
+
```
|
|
864
|
+
Add a test that exercises this path through a full pi tool call and
|
|
865
|
+
asserts the resulting `text-delta` chunks contain a useful message.
|
|
866
|
+
|
|
867
|
+
### R8 — Symlink escapes via `boundFs`
|
|
868
|
+
|
|
869
|
+
`assertRealPathWithinWorkspace` (per spec line 390) resolves symlinks
|
|
870
|
+
*before* bounding so a symlink inside the workspace pointing to
|
|
871
|
+
`/etc/passwd` is rejected. Pi's `resolveToCwd` does NOT do this — it just
|
|
872
|
+
joins paths. **`boundFs` must call `fs.realpath` (or equivalent)** before
|
|
873
|
+
the `isUnder` check.
|
|
874
|
+
|
|
875
|
+
**Mitigation:** port `assertRealPathWithinWorkspace` from
|
|
876
|
+
`packages/agent/src/server/workspace/paths.ts` into the `boundFs`
|
|
877
|
+
implementation (or call it directly). Add a test creating a symlink in
|
|
878
|
+
the workspace pointing outside it and asserting the read is rejected.
|
|
879
|
+
|
|
880
|
+
### R9 — Streaming compatibility with our `useChat` UI
|
|
881
|
+
|
|
882
|
+
Pi's tool factories emit specific `tool_execution_*` events that our
|
|
883
|
+
`stream-adapter.ts` translates into AI SDK `tool-input-*` /
|
|
884
|
+
`tool-output-*` chunks (per `agent-package-spec.md` line 658-674). If
|
|
885
|
+
pi's events differ shape-wise from what our adapter expects, the UI may
|
|
886
|
+
render tool calls oddly or break.
|
|
887
|
+
|
|
888
|
+
**Mitigation:** the adapter already exists and works for our hand-rolled
|
|
889
|
+
tools (which emit pi-shaped events because they go through pi's tool
|
|
890
|
+
machinery via `customTools`). Pi's factory tools emit the same events.
|
|
891
|
+
No translation change needed. Verify via Step 2 smoke (direct mode)
|
|
892
|
+
before flipping other modes.
|
|
893
|
+
|
|
894
|
+
### R10 — `excludeDefaults` vs `disableDefaultFileTools` (PLUGIN_MODEL v7.0+)
|
|
895
|
+
|
|
896
|
+
Per PLUGIN_MODEL v7.0's separation of concerns, these are two
|
|
897
|
+
**distinct** switches at two **distinct** layers:
|
|
898
|
+
|
|
899
|
+
| Switch | What it removes | Layer |
|
|
900
|
+
|---|---|---|
|
|
901
|
+
| `disableDefaultFileTools: true` on `createAgentApp` | LLM file tools (read/write/edit/find/grep/ls) | Harness |
|
|
902
|
+
| `excludeDefaults: ['filesystem']` on `<WorkspaceProvider>` / `createWorkspaceAgentApp` | Filesystem UI (Files left-tab + code/markdown editor auto-routing) | Workspace |
|
|
903
|
+
|
|
904
|
+
`createWorkspaceAgentApp` does **NOT** pass `disableDefaultFileTools`
|
|
905
|
+
to `createAgentApp` (v7.0+ simplification — no dual-registration to
|
|
906
|
+
coordinate). File tools always flow through the harness; UI is
|
|
907
|
+
gated separately.
|
|
908
|
+
|
|
909
|
+
**Mitigation:** explicit tests in `createWorkspaceAgentApp` that:
|
|
910
|
+
- Default config: file tools register once (via harness); Files
|
|
911
|
+
left-tab visible.
|
|
912
|
+
- With `excludeDefaults: ['filesystem']`: file tools STILL register
|
|
913
|
+
(harness owns them); Files left-tab NOT visible.
|
|
914
|
+
- With `disableDefaultFileTools: true` (forwarded option): zero file
|
|
915
|
+
tools register; Files left-tab still visible (harmless — tools are
|
|
916
|
+
separate from UI).
|
|
917
|
+
- With both: zero tools, no UI.
|
|
918
|
+
|
|
919
|
+
## Test strategy
|
|
920
|
+
|
|
921
|
+
### Unit tests (per step)
|
|
922
|
+
|
|
923
|
+
- Each Operations adapter: happy path + path-bounding rejection +
|
|
924
|
+
symlink-escape rejection + error-shape propagation.
|
|
925
|
+
- Each bundle factory: returns the right tools per mode (assert tool
|
|
926
|
+
`name` array per mode; assert `vercelGrepTool` appears in
|
|
927
|
+
vercel-sandbox bundle and pi's `createGrepTool` does NOT).
|
|
928
|
+
- `vercelGrepTool` parses ripgrep output correctly against a
|
|
929
|
+
fake/recorded `sandbox.exec` response.
|
|
930
|
+
|
|
931
|
+
### Integration tests (per mode flip)
|
|
932
|
+
|
|
933
|
+
- Boot a Fastify app with `createAgentApp({ mode: '<mode>' })`,
|
|
934
|
+
inject a chat request, assert the response stream includes the
|
|
935
|
+
expected tool-call chunks for a known prompt. Spec line 762
|
|
936
|
+
describes this pattern.
|
|
937
|
+
- For vercel-sandbox: use a recorded `Sandbox` mock (we already have
|
|
938
|
+
some test fixtures); no real VM required for CI.
|
|
939
|
+
|
|
940
|
+
### E2E smoke (manual, per mode flip)
|
|
941
|
+
|
|
942
|
+
For each of direct/local/vercel-sandbox after its flip step:
|
|
943
|
+
|
|
944
|
+
1. Start the playground in that mode.
|
|
945
|
+
2. Send: "list files in the repo" → exercises `find` (or `bash ls`).
|
|
946
|
+
3. Send: "find all uses of `useEffect`" → exercises `grep`.
|
|
947
|
+
4. Send: "read package.json" → exercises `read`.
|
|
948
|
+
5. Send: "create a file foo.txt with content 'hi'" → exercises `write`.
|
|
949
|
+
6. Send: "edit foo.txt to say 'hello'" → exercises `edit`.
|
|
950
|
+
7. Send: "run `ls`" → exercises `bash`.
|
|
951
|
+
8. Verify in devtools Network: chat POST returns SSE with
|
|
952
|
+
text-delta + tool-input-available + tool-output-available chunks
|
|
953
|
+
for each tool used; no errors.
|
|
954
|
+
9. Verify in `GET /api/v1/agent/sessions/:id/system-prompt`: no
|
|
955
|
+
double-dash or duplicate-guideline noise; tool names match
|
|
956
|
+
`find`/`grep` (post-rename).
|
|
957
|
+
|
|
958
|
+
### Regression guards
|
|
959
|
+
|
|
960
|
+
- Snapshot test of system-prompt size: assert post-Phase-1 baseline
|
|
961
|
+
is at least 700 chars smaller than the pre-Phase-1 baseline.
|
|
962
|
+
- Tool-renderer test: every tool name present in the bundle factories
|
|
963
|
+
has a renderer entry in
|
|
964
|
+
`packages/agent/src/ui-shadcn/workspaceToolRenderers.tsx`.
|
|
965
|
+
- A grep over `packages/` for legacy search-tool names returns zero
|
|
966
|
+
hits after step 6.
|
|
967
|
+
|
|
968
|
+
## Verification checklist
|
|
969
|
+
|
|
970
|
+
### After Phase 0
|
|
971
|
+
|
|
972
|
+
- [ ] `pnpm exec tsc --noEmit` clean across `packages/agent`.
|
|
973
|
+
- [ ] All 6 existing tool tests + new `_shared.test.ts` green.
|
|
974
|
+
- [ ] `bash scripts/check-invariants.sh packages/agent` passes.
|
|
975
|
+
- [ ] No tool file imports duplicated helpers from anywhere except
|
|
976
|
+
`_shared.ts`.
|
|
977
|
+
|
|
978
|
+
### After Phase 1
|
|
979
|
+
|
|
980
|
+
- [ ] `pnpm exec tsc --noEmit` clean across `packages/agent`.
|
|
981
|
+
- [ ] Full `pnpm exec vitest run` green (modulo two pre-existing failures).
|
|
982
|
+
- [ ] `bash scripts/check-invariants.sh packages/agent` passes.
|
|
983
|
+
- [ ] **Direct mode smoke:** playground turn exercises bash/read/write/edit/find/grep;
|
|
984
|
+
no double-dash or duplicate-guideline noise in
|
|
985
|
+
`GET /api/v1/agent/sessions/:id/system-prompt`.
|
|
986
|
+
- [ ] **Local mode smoke:** same suite, bwrap visible in process tree
|
|
987
|
+
during bash exec.
|
|
988
|
+
- [ ] **Vercel-sandbox mode smoke:** same suite, all tool calls hit the VM
|
|
989
|
+
(verify via `sandbox.exec` log); custom `vercelGrepTool` returns
|
|
990
|
+
results from inside the VM.
|
|
991
|
+
- [ ] System prompt size for the playground baseline drops by at least
|
|
992
|
+
~700 chars (per-tool guideline lines + double-dash slack).
|
|
993
|
+
- [ ] No source file under `src/server/catalog/tools/` defines `bash`,
|
|
994
|
+
`read`, `write`, `edit`, `find`, or `grep`. Only
|
|
995
|
+
`vercelGrepTool`, `executeIsolatedCodeTool`, and `_shared.ts` should
|
|
996
|
+
remain.
|
|
997
|
+
|
|
998
|
+
## Estimated scope
|
|
999
|
+
|
|
1000
|
+
### Phase 0
|
|
1001
|
+
|
|
1002
|
+
- New: `_shared.ts` (~80 LOC) + `_shared.test.ts` (~50 LOC).
|
|
1003
|
+
- Touched: 6 tool files (~30-40% smaller each).
|
|
1004
|
+
- Net: ~ **-20 LOC**.
|
|
1005
|
+
|
|
1006
|
+
### Phase 1
|
|
1007
|
+
|
|
1008
|
+
- New:
|
|
1009
|
+
- `operations/index.ts` (`boundFs` + vercel ops) ~180 LOC.
|
|
1010
|
+
- `operations.test.ts` ~150 LOC.
|
|
1011
|
+
- `vercelGrepTool.ts` ~80 LOC + tests ~80 LOC.
|
|
1012
|
+
- Per-mode tool lists ~40 LOC × 3 modes = ~120 LOC.
|
|
1013
|
+
- Deleted:
|
|
1014
|
+
- 6 hand-rolled tool files ~1100 LOC.
|
|
1015
|
+
- 6 test files ~600 LOC.
|
|
1016
|
+
- `standardCatalog.ts` ~100 LOC.
|
|
1017
|
+
- Half of `tool-adapter.ts` ~25 LOC.
|
|
1018
|
+
- Net: ~ **-1240 LOC**.
|
|
1019
|
+
|
|
1020
|
+
### Combined inventory after Phase 0 + Phase 1
|
|
1021
|
+
|
|
1022
|
+
| What we own | LOC |
|
|
1023
|
+
|---|---:|
|
|
1024
|
+
| `_shared.ts` | ~80 |
|
|
1025
|
+
| `tools/operations/index.ts` (boundFs + vercelOps) | ~180 |
|
|
1026
|
+
| `tools/operations.test.ts` | ~150 |
|
|
1027
|
+
| `tools/vercelGrepTool.ts` (+ tests) | ~160 |
|
|
1028
|
+
| `tools/executeIsolatedCodeTool.ts` (+ tests) | ~120 |
|
|
1029
|
+
| `tool-adapter.ts` (slimmed) | ~30 |
|
|
1030
|
+
| Per-mode tool lists | ~120 |
|
|
1031
|
+
| **Total** | **~840 LOC** |
|
|
1032
|
+
|
|
1033
|
+
vs current ~1830 LOC across the same surface. **Net -990 LOC** with no
|
|
1034
|
+
behavior change for direct/local modes, and a more correct vercel-sandbox
|
|
1035
|
+
mode (today's hand-rolled tools don't actually route to the sandbox in the
|
|
1036
|
+
sense pi's Operations would).
|
|
1037
|
+
|
|
1038
|
+
## Non-goals
|
|
1039
|
+
|
|
1040
|
+
- Not changing how `customTools` work for plugin tools / `extraTools`.
|
|
1041
|
+
- Not removing path-bounding or any security guarantee. The bound moves
|
|
1042
|
+
from inside our hand-rolled tool to inside the Operations adapter.
|
|
1043
|
+
- Not opting out of pi-extension tools (`web_search`, `code_search`, …).
|
|
1044
|
+
Tracked separately under "noExtensions knob".
|
|
1045
|
+
- Not adding `ls` in Phase 1 (Step 6 is optional follow-up).
|
|
1046
|
+
- Not changing the dual-registration story in PLUGIN_MODEL.md.
|
|
1047
|
+
PLUGIN_MODEL ships after this plan; Step 1b's bundle extraction picks up
|
|
1048
|
+
whatever shape `runtime/modes/*.ts` has by then — a pure file move.
|
|
1049
|
+
- Not moving `inferBashFileChanges` out of bash. Independent PR.
|
|
1050
|
+
- Not building a `defineTool(spec)` skeleton in Phase 0. Pi already
|
|
1051
|
+
provides this via TypeBox; rolling our own would die in Phase 1.
|
|
1052
|
+
|
|
1053
|
+
## References
|
|
1054
|
+
|
|
1055
|
+
- Original RFC: [pi-mono#564 — feat(coding-agent): Add pluggable operations](https://github.com/badlogic/pi-mono/issues/564)
|
|
1056
|
+
- Canonical SSH-pattern reference: `examples/extensions/ssh.ts` in pi-coding-agent
|
|
1057
|
+
- Canonical bwrap-pattern reference: `examples/extensions/sandbox/index.ts` in pi-coding-agent
|
|
1058
|
+
- Why grep is default: [pi-mono#3782 — Default to using custom `grep` tool](https://github.com/badlogic/pi-mono/issues/3782)
|
|
1059
|
+
- Active SSH-extension iteration: [pi-mono#3500](https://github.com/badlogic/pi-mono/issues/3500), [#3320](https://github.com/badlogic/pi-mono/issues/3320), [#2965](https://github.com/badlogic/pi-mono/issues/2965)
|
|
1060
|
+
- Original spec: `packages/agent/docs/plans/agent-package-spec.md` (decisions #4, #6)
|
|
1061
|
+
- Companion plan (lands after this): `packages/workspace/docs/plans/archive/PLUGIN_MODEL.md`
|