@tencent-ai/codebuddy-code 2.119.4 → 2.120.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 (52) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/codebuddy-headless.js +40 -30
  3. package/dist/codebuddy.js +60 -50
  4. package/dist/web-ui/assets/{index-GcU4pZms.js → index-BfmXmm8w.js} +274 -274
  5. package/dist/web-ui/docs/cn/cli/brokered-shell-macos.md +778 -0
  6. package/dist/web-ui/docs/cn/cli/env-vars.md +10 -5
  7. package/dist/web-ui/docs/cn/cli/plugins-reference.md +114 -13
  8. package/dist/web-ui/docs/cn/cli/prewarm.md +18 -5
  9. package/dist/web-ui/docs/cn/cli/release-notes/README.md +11 -0
  10. package/dist/web-ui/docs/cn/cli/release-notes/v2.117.0.md +35 -0
  11. package/dist/web-ui/docs/cn/cli/release-notes/v2.117.1.md +18 -0
  12. package/dist/web-ui/docs/cn/cli/release-notes/v2.117.2.md +28 -0
  13. package/dist/web-ui/docs/cn/cli/release-notes/v2.118.0.md +29 -0
  14. package/dist/web-ui/docs/cn/cli/release-notes/v2.118.1.md +18 -0
  15. package/dist/web-ui/docs/cn/cli/release-notes/v2.118.2.md +13 -0
  16. package/dist/web-ui/docs/cn/cli/release-notes/v2.119.0.md +38 -0
  17. package/dist/web-ui/docs/cn/cli/release-notes/v2.119.1.md +15 -0
  18. package/dist/web-ui/docs/cn/cli/release-notes/v2.119.2.md +23 -0
  19. package/dist/web-ui/docs/cn/cli/release-notes/v2.119.3.md +15 -0
  20. package/dist/web-ui/docs/cn/cli/release-notes/v2.119.4.md +13 -0
  21. package/dist/web-ui/docs/cn/cli/settings.md +2 -0
  22. package/dist/web-ui/docs/cn/cli/sub-agents.md +38 -0
  23. package/dist/web-ui/docs/en/cli/brokered-shell-macos.md +778 -0
  24. package/dist/web-ui/docs/en/cli/env-vars.md +10 -5
  25. package/dist/web-ui/docs/en/cli/plugins-reference.md +106 -13
  26. package/dist/web-ui/docs/en/cli/prewarm.md +18 -5
  27. package/dist/web-ui/docs/en/cli/release-notes/README.md +11 -0
  28. package/dist/web-ui/docs/en/cli/release-notes/v2.117.0.md +35 -0
  29. package/dist/web-ui/docs/en/cli/release-notes/v2.117.1.md +18 -0
  30. package/dist/web-ui/docs/en/cli/release-notes/v2.117.2.md +28 -0
  31. package/dist/web-ui/docs/en/cli/release-notes/v2.118.0.md +29 -0
  32. package/dist/web-ui/docs/en/cli/release-notes/v2.118.1.md +18 -0
  33. package/dist/web-ui/docs/en/cli/release-notes/v2.118.2.md +13 -0
  34. package/dist/web-ui/docs/en/cli/release-notes/v2.119.0.md +38 -0
  35. package/dist/web-ui/docs/en/cli/release-notes/v2.119.1.md +15 -0
  36. package/dist/web-ui/docs/en/cli/release-notes/v2.119.2.md +23 -0
  37. package/dist/web-ui/docs/en/cli/release-notes/v2.119.3.md +15 -0
  38. package/dist/web-ui/docs/en/cli/release-notes/v2.119.4.md +13 -0
  39. package/dist/web-ui/docs/en/cli/settings.md +2 -0
  40. package/dist/web-ui/docs/en/cli/sub-agents.md +38 -0
  41. package/dist/web-ui/docs/search-index-en.json +1 -1
  42. package/dist/web-ui/docs/search-index-zh.json +1 -1
  43. package/dist/web-ui/docs/sidebar-en.json +1 -1
  44. package/dist/web-ui/docs/sidebar-zh.json +1 -1
  45. package/dist/web-ui/index.html +1 -1
  46. package/dist/web-ui/sw.js +1 -1
  47. package/package.json +4 -4
  48. package/product.cloudhosted.json +2 -2
  49. package/product.internal.json +2 -2
  50. package/product.ioa.json +2 -2
  51. package/product.json +2 -2
  52. package/product.selfhosted.json +2 -2
@@ -0,0 +1,778 @@
1
+ # macOS Brokered Shell Design
2
+
3
+ ## Background and Goals
4
+
5
+ The macOS Bash sandbox needs to solve two problems simultaneously:
6
+
7
+ 1. Commands inside the sandbox must be constrained by file system policies; unauthorized reads and writes must go through CodeBuddy's permission checks.
8
+ 2. When a command modifies an existing file, `ModifyBackup` must be invoked before the actual modification occurs, so that `sandbox-cli` can save the pre-modification version and persist the backup cycle via `CommitModifyBackup` at the end of the turn.
9
+
10
+ Relying solely on a plain shell or system commands cannot achieve this. The reasons are:
11
+
12
+ - Redirections such as `>` / `>>` are executed by the shell itself; the outer layer can only see the entire command, not the exact timing of opening the target file.
13
+ - Commands like `sed -i` and `mv` may overwrite the target file via a temporary file plus `rename`, which does not necessarily manifest as a direct write to the target file.
14
+ - `truncate` changes file content through `open(O_WRONLY)` plus `ftruncate`; if the command does not enter the brokered runtime, there is no pre-modification backup opportunity.
15
+ - macOS Seatbelt's `sandbox_extension` token can only be issued by a process outside the sandbox; processes inside the sandbox cannot expand their own permissions.
16
+
17
+ Therefore, the current solution uses a combination of custom `zsh` + custom `toybox` + agent-cli broker:
18
+
19
+ - Custom `zsh` handles the shell semantic layer, covering redirections, glob, conditionals, directory access, and other shell-internal file access.
20
+ - Custom `toybox` handles the common command layer, covering basic commands resolved via `PATH` and their `open` / `rename` / `delete` file operations.
21
+ - `agent-cli` serves as the broker layer, handling permission policies, user approval, macOS sandbox extension token issuance, host operation execution, and `ModifyBackup` triggering.
22
+ - `sandbox-cli` serves as the sandbox execution and backup storage layer, running sandboxed processes and persisting `ModifyBackup` / `CommitModifyBackup` data.
23
+
24
+ The goal is not to write backup logic for every command, but to capture a unified "pre-modification" timing at key semantic layers.
25
+
26
+ ## Overall Architecture
27
+
28
+ ```mermaid
29
+ flowchart LR
30
+ UserTurn["User turn"] --> Interceptor["SandboxAgentRunInterceptor"]
31
+ Interceptor --> SandboxCLI["sandbox-cli session"]
32
+ Interceptor --> BackupConfig["EnableModifyBackup"]
33
+
34
+ BashTool["Bash tool"] --> SandboxShell["SandboxShellService"]
35
+ SandboxShell --> BrokerIPC["BrokeredSandboxIpcServer<br/>Unix socket"]
36
+ SandboxShell --> Zsh["Custom zsh"]
37
+ Zsh --> BrokerEnv["brokered-sandbox-bash-env.sh"]
38
+ BrokerEnv --> BrokeredBin["brokered-bin PATH"]
39
+ BrokeredBin --> Toybox["Custom toybox"]
40
+
41
+ Zsh -->|"text token request<br/>read/write path"| BrokerIPC
42
+ Toybox -->|"text token request<br/>open/fopen"| BrokerIPC
43
+ Toybox -->|"JSON HostFileOperation<br/>delete/rename"| BrokerIPC
44
+
45
+ BrokerIPC --> HostService["BrokeredSandboxHostService"]
46
+ HostService --> Policy["sandbox fileSafety / approval"]
47
+ HostService --> ModifyBackup["ModifyBackup"]
48
+ HostService --> Token["sandbox_extension_issue_file"]
49
+ HostService --> HostOp["host delete/rename/copy/..."]
50
+
51
+ ModifyBackup --> SandboxCLI
52
+ FinalStop["FINAL_STOP hook"] --> CommitBackup["CommitModifyBackup"]
53
+ CommitBackup --> SandboxCLI
54
+ ```
55
+
56
+ Core code and artifact locations:
57
+
58
+ | Module | Location | Responsibility |
59
+ |------|------|------|
60
+ | Sandbox turn configuration | `src/node/agent/interceptors/sandbox-interceptor.ts` | Reads `sandbox.fileBackup`, starts and syncs `sandbox-cli`, sends `EnableModifyBackup` |
61
+ | Sandbox shell execution | `src/node/sandbox-cli/sandbox-shell-service.ts` | Starts broker IPC, injects broker env, switches to custom `zsh` on macOS |
62
+ | Runtime env | `src/node/shell/shell-runtime-env.ts` / `src/node/shell/brokered-shell-env.ts` | Injects `CODEBUDDY_SANDBOX_BROKER_*`, toybox, zsh, brokered bin paths |
63
+ | Broker IPC | `src/node/permission/brokered-sandbox/ipc-server.ts` | Receives zsh/toybox requests, dispatches token requests and host operations |
64
+ | Host service | `src/node/permission/brokered-sandbox/host-service.ts` | Permission checks, token issuance, host operation execution, `ModifyBackup` |
65
+ | Host dispatcher | `src/node/permission/brokered-sandbox/host-dispatcher.ts` | JSON `HostFileOperation` routing |
66
+ | Finalization commit | `src/node/hooks/finalization-modify-backup-hook.ts` | Sends `CommitModifyBackup` at `FINAL_STOP` |
67
+ | Brokered shim | `vendor/shim/brokered-sandbox-bash-env.sh` / `vendor/shim/brokered-bin/*` | Routes common commands to custom toybox |
68
+ | Custom zsh artifact | `vendor/zsh-macos/bin/zsh` | macOS brokered shell |
69
+ | Custom toybox artifact | `vendor/toybox-macos/toybox` | macOS brokered command runtime |
70
+ | Toybox sandbox profile | `vendor/toybox-macos/toybox.sb` | Toybox Seatbelt sandbox rules (see below) |
71
+ | zsh/toybox source | `tsbx-macos` repo | Reproducible build and low-level hook source |
72
+
73
+ ## Enablement Conditions and Runtime Injection
74
+
75
+ ### Toggle Effect Matrix
76
+
77
+ Whether a Bash command enters the brokered shell and whether pre-modification backup is enabled depends on a combination of conditions:
78
+
79
+ | Condition | Behavior when not met |
80
+ |------|---------------|
81
+ | `sandbox.enabled = true` | `SandboxOrchestrator` executes locally directly, without entering the sandbox; brokered shell and backup are both ineffective |
82
+ | `darwin` platform | Brokered shell runtime is not injected (brokered shell is a macOS-specific solution) |
83
+ | WorkBuddy Desktop environment | `injectBrokeredShellEnv()` does not inject managed runtime artifacts |
84
+ | Brokered shell shim complete (`shell-runtime-bash-env.sh`, `brokered-sandbox-bash-env.sh`, `brokered-bin/codebuddy-toybox-dispatch`, `brokered-bin/ls` exist) | Basic brokered shell runtime is not injected; zsh, toybox, toybox.sb are optional artifacts — missing zsh means no custom shell switch, missing toybox/toybox.sb means no toybox runtime injection |
85
+ | `WORKBUDDY_MANAGED_RUNTIME_DISABLED` does not contain `brokeredShell` | When it contains `brokeredShell`, brokered shell runtime injection is skipped |
86
+ | Command not in `sandbox.excludedCommands` | Commands matching excludedCommands fall back to local execution directly |
87
+ | Not `dangerouslyDisableSandbox` | When the model requests bypass, user approval is required → if approved, local execution without sandbox |
88
+ | Not `rawCommand` mode | rawCommand directly spawns the executable, without shell wrapping, without prefixing `brokered-bin`, without switching to custom zsh; WorkBuddy Desktop's default sandbox env still injects basic broker variables |
89
+ | `sandbox.fileBackup.enabled !== false` | Backup is not enabled; brokered shell still works (permission control and token issuance function normally), but `ModifyBackup` is not triggered |
90
+
91
+ Typical normal flow: all conditions above are met → command enters custom zsh + toybox brokered shell → write operations go through broker permission checks → existing regular files trigger `ModifyBackup` → `CommitModifyBackup` at `FINAL_STOP`.
92
+
93
+ ### File Backup Toggle
94
+
95
+ The file backup toggle is parsed by `SandboxAgentRunInterceptor` at the start of each real user turn:
96
+
97
+ - Supported platforms: `win32` and `darwin`. Windows-side file backup is implemented through a separate mechanism that does not involve the brokered shell runtime and is outside the scope of this document.
98
+ - On macOS, it is enabled when `sandbox.fileBackup.enabled !== false`; it is enabled by default when not explicitly disabled.
99
+ - When enabled, it ensures the `sandbox-cli` session is ready and sends `EnableModifyBackup` to `sandbox-cli`.
100
+ - If `sandbox-cli` is unavailable or `EnableModifyBackup` sync fails, `enableFileBackup` is disabled for the current turn to avoid a half-open state.
101
+
102
+ The brokered shell runtime is only injected in the macOS sandbox Bash execution path. The key steps of `SandboxShellService.execute()` are:
103
+
104
+ 1. `waitReady()` / `ensureAlive()` / `switchToSession()` prepare the `sandbox-cli` session.
105
+ 2. `BrokeredSandboxIpcServer.ensureStarted()` creates the out-of-sandbox Unix socket.
106
+ 3. `buildShellRuntimeEnv()` injects the broker env.
107
+ 4. `_resolveSandboxShellConfiguration()` uses `vendor/zsh-macos/bin/zsh -f -c` on `darwin` when custom zsh and broker IPC are available.
108
+ 5. `buildShellRuntimePosixCommand()` sources `brokered-sandbox-bash-env.sh` and prepends `brokered-bin` to `PATH`.
109
+
110
+ Key environment variables:
111
+
112
+ | Variable | Purpose |
113
+ |------|------|
114
+ | `CODEBUDDY_SANDBOX_BROKER_IPC_ADDRESS` | agent-cli broker Unix socket path |
115
+ | `CODEBUDDY_SANDBOX_BROKER_SESSION_ID` | Current CodeBuddy session, used to prevent cross-session requests |
116
+ | `CODEBUDDY_SANDBOX_BROKER_TOOL_CALL_ID` | Current Bash tool call, used for audit and backup attribution |
117
+ | `CODEBUDDY_SANDBOX_BROKER_TRACE_ID` | Single execution trace ID, for log correlation |
118
+ | `CODEBUDDY_SANDBOX_HOST_FILE_OPERATION_COMMAND` | JSON host-op command name, currently `HostFileOperation` |
119
+ | `CODEBUDDY_TOYBOX_BIN` | Custom toybox binary path |
120
+ | `CODEBUDDY_TOYBOX_SANDBOX_PROFILE` | Toybox sandbox profile path |
121
+ | `CODEBUDDY_BROKERED_SHELL_ENV` | Brokered shell bootstrap script |
122
+ | `CODEBUDDY_BROKERED_BIN_DIR` | Brokered command shim directory |
123
+ | `CODEBUDDY_SANDBOX_ZSH_BIN` | Custom zsh binary path |
124
+ | `TOYBOX_SANDBOX_SOCK` | Toybox socket for connecting to the broker, typically derived from the broker IPC address by the bootstrap |
125
+
126
+ `injectBrokeredShellEnv()` also checks the runtime environment:
127
+
128
+ - Non-`darwin` does not inject any brokered environment variables.
129
+ - **Broker IPC environment variables** (`CODEBUDDY_BROKERED_FS_HOOK_ENABLED`, `CODEBUDDY_SANDBOX_BROKER_IPC_ADDRESS`, etc.) are only injected in the WorkBuddy Desktop environment; non-WorkBuddy Desktop environments clean up broker IPC and toybox socket addresses left by the caller; within WorkBuddy Desktop, they are not affected by the `brokeredShell` toggle. This allows the safe-delete shim (bash/Node.js/Python) to send `HostFileOperation delete` via broker IPC even when brokeredShell is disabled, uniformly going through the host service's permission check and `ModifyBackup`.
130
+ - **Managed runtime artifacts** (toybox, zsh, brokered-bin) are only injected when all of the following conditions are met: not `WORKBUDDY_MANAGED_RUNTIME_DISABLED=brokeredShell`, and in a WorkBuddy Desktop environment.
131
+
132
+ ## macOS File Modification Detection Chain
133
+
134
+ There are currently two types of IPC protocols.
135
+
136
+ ### text token request
137
+
138
+ Used for `open`-type file access by zsh and toybox:
139
+
140
+ ```text
141
+ <extension-class>\t<absolute-path>\t<session-id>\t<tool-call-id>\n
142
+ ```
143
+
144
+ `extension-class` currently has two values:
145
+
146
+ - `com.workbuddy.sandbox.read`
147
+ - `com.workbuddy.sandbox.read-write`
148
+
149
+ When agent-cli receives it:
150
+
151
+ 1. Validates that the session ID matches the current session.
152
+ 2. Based on `fileSafety`, determines whether the path is `grant-token`, `sandbox`, `deny`, or requires approval.
153
+ 3. If the path matches a rule requiring approval, the broker records a prompt block, the current IPC request returns `DENY`, and the outer `SandboxOrchestrator` handles unified approval and native rerun.
154
+ 4. If it is a write operation and the final result is not deny, `ModifyBackup` is executed first.
155
+ 5. Returns a sandbox extension token, `SANDBOX`, or `DENY`.
156
+ 6. zsh/toybox calls `sandbox_extension_consume()` after receiving the token, then performs the real `open`.
157
+
158
+ agent-cli's response is a single line of text with three possible forms:
159
+
160
+ - Sandbox extension token string: zsh/toybox calls `sandbox_extension_consume()` then performs the real `open`.
161
+ - `SANDBOX`: The existing sandbox rules already allow access; no token consumption needed; but write operations still go through `ModifyBackup` first.
162
+ - `DENY`: Access denied; zsh/toybox does not perform the real `open`.
163
+
164
+ ### JSON HostFileOperation
165
+
166
+ Used for file operations that should not be completed inside the sandbox, or that require host semantics:
167
+
168
+ ```json
169
+ {
170
+ "id": "req-1",
171
+ "command": "HostFileOperation",
172
+ "operation": "rename",
173
+ "from": "/absolute/source",
174
+ "to": "/absolute/target",
175
+ "sessionId": "...",
176
+ "toolCallId": "...",
177
+ "brokerTraceId": "..."
178
+ }
179
+ ```
180
+
181
+ `id` is used for request-response matching; `brokerTraceId` is used for log correlation. Both are optional but recommended for audit purposes.
182
+
183
+ agent-cli's JSON response structure:
184
+
185
+ ```json
186
+ {
187
+ "id": "req-1",
188
+ "ok": true,
189
+ "operation": "rename",
190
+ "path": "/absolute/source",
191
+ "normalisedPath": "/resolved/source",
192
+ "decision": "host-op",
193
+ "to": "/absolute/target",
194
+ "normalisedTo": "/resolved/target"
195
+ }
196
+ ```
197
+
198
+ On failure, `ok` is `false` with an `error` field. `decision` can be `grant-token`, `host-op`, `sandbox`, or `deny`. (`prompt` is an intermediate state during the approval process and is not returned as a final IPC response to toybox.)
199
+
200
+ The host operations currently supported by the agent-cli TS side include:
201
+
202
+ | operation | Additional parameters | Description |
203
+ |-----------|----------|------|
204
+ | `delete` | `deleteMode?: 'trash' \| 'unlink'`, `recursive?`, `force?`, `safeDeleteReportPath?` | macOS defaults to `deleteMode='trash'` (move to trash); non-macOS defaults to `unlink` |
205
+ | `mkdir` | `recursive?`, `mode?` | Create directory |
206
+ | `rename` | `from`, `to` | Rename/move |
207
+ | `copy` | `from`, `to`, `recursive?` | Copy file or directory |
208
+ | `chmod` | `path`, `mode` | Change permissions |
209
+ | `link` | `from`, `to`, `symbolic?` | Hard link (default) or symbolic link. Hard link checks read permission on source + write permission on target; symbolic link only checks write permission on target |
210
+ | `touch` | `path` | Create or update file timestamp |
211
+
212
+ The key host operations currently reported by the macOS toybox runtime are:
213
+
214
+ - `delete`: `rm` / `rmdir` / `unlink` reported via `toybox_host_delete()`, with the host side connecting to the trash (`deleteMode='trash'`).
215
+ - `rename`: `rename()` macro replaced with `toybox_rename()`, covering `mv`, `sed -i`, and other temporary file overwrite scenarios.
216
+
217
+ `copy`, `touch`, and other TS branches exist for broker capability completeness and future expansion; the current `cp` coverage mainly comes from the text token request for opening the write target file, not from toybox proactively sending a JSON `copy`.
218
+
219
+ ## Custom zsh Responsibilities
220
+
221
+ The core value of custom zsh is capturing the shell's own file access timing, especially for redirections.
222
+
223
+ The source code is in `tsbx-macos/zsh-macos`; the current patch is based on zsh 5.9.1, with the main changes:
224
+
225
+ - `Src/exec.c`: Changed redirection-related `open()` to `codebuddy_brokered_open()`.
226
+ - `Src/zsh_system.h`: Added broker connection, path normalization, token request, `sandbox_extension_consume()`, and brokered `open/stat/access/opendir/chdir` helpers.
227
+ - `Src/init.c` / `Src/glob.c` / `Src/cond.c` / `Src/compat.c`: Changed shell initialization, glob, conditionals, directory switching, and other file access to brokered versions.
228
+
229
+ Typical scenarios handled by zsh:
230
+
231
+ - `echo hi > a.txt`
232
+ - `cat < input.txt`
233
+ - `[[ -f a.txt ]]`
234
+ - `cd some-dir`
235
+ - Paths requiring `stat` / `lstat` during glob expansion
236
+
237
+ Among these, `>` / `>>` are the most critical scenarios for the backup chain. The actual flow is:
238
+
239
+ 1. zsh parses the redirection.
240
+ 2. Executes `codebuddy_brokered_open(path, O_WRONLY | O_CREAT | O_TRUNC, ...)`.
241
+ 3. `codebuddy_brokered_authorize_path(path, "write")` sends a text token request to the agent-cli broker.
242
+ 4. agent-cli sends `ModifyBackup` for existing regular files before granting access.
243
+ 5. zsh performs the real `open()` after receiving the token or `SANDBOX`.
244
+
245
+ This is why the `>` timing must be exposed within custom zsh, rather than relying solely on agent-cli parsing the command string.
246
+
247
+ ## Custom toybox Responsibilities
248
+
249
+ The core value of custom toybox is consolidating common basic commands into a single controllable runtime.
250
+
251
+ The `brokered-bin` directory in the agent-cli vendor contains the dispatcher entry `codebuddy-toybox-dispatch` and a set of command name shims:
252
+
253
+ ```text
254
+ codebuddy-toybox-dispatch # dispatcher entry
255
+ cat chmod cp dd find grep head ln ls mkdir mv readlink realpath rm rmdir sed tail tee touch truncate unlink wc
256
+ ```
257
+
258
+ Each command name shim is a link pointing to `codebuddy-toybox-dispatch`. Execution flow:
259
+
260
+ 1. `brokered-sandbox-bash-env.sh` prepends `CODEBUDDY_BROKERED_BIN_DIR` to `PATH`.
261
+ 2. `sed` / `mv` / `truncate` in user commands hit the brokered shim first.
262
+ 3. `codebuddy-toybox-dispatch` derives the toybox applet name from its own filename.
263
+ 4. The dispatcher executes `CODEBUDDY_TOYBOX_BIN <applet> ...`.
264
+ 5. If toybox does not support the applet or option, the dispatcher removes the brokered bin from `PATH` and falls back to the system command of the same name.
265
+
266
+ The key hooks in the toybox source layer are in `tsbx-macos/toybox-0.8.13`:
267
+
268
+ - `toys.h` macro-replaces `open/openat/creat/fopen/freopen/rename`.
269
+ - `lib/iolog.c` implements `toybox_open()`, `toybox_openat()`, `toybox_creat()`, `toybox_fopen()`, `toybox_freopen()`.
270
+ - The open-type wrappers send a text token request before the real open.
271
+ - `toybox_rename()` sends a JSON `HostFileOperation rename`, and the host performs the real rename.
272
+ - `toybox_host_delete()` sends a JSON `HostFileOperation delete`, and the host performs a safe delete.
273
+
274
+ The toybox open hook uses an eager mode: instead of waiting for the macOS sandbox to reject and then requesting a token, it proactively requests a token from the broker before every open. This allows agent-cli to complete the backup before the actual write.
275
+
276
+ ### toybox sandbox profile (`toybox.sb`)
277
+
278
+ `vendor/toybox-macos/toybox.sb` is the Seatbelt sandbox rule for the toybox runtime, loaded by `sandbox-cli` via `sandbox-exec` when starting the toybox process. Core policies:
279
+
280
+ - `(deny default)`: Denies all file system access by default.
281
+ - `(allow file-read* (subpath "/"))`: Allows global reads (read token requests still go through the broker, but the Seatbelt layer does not block them).
282
+ - `(allow file-write* (subpath "/dev") (subpath "/private/var/folders"))`: Allows writes to `/dev` (stdout/stderr) and temporary directories (intermediate files for `sed -i`, etc.).
283
+ - `(allow file-read* (extension "com.workbuddy.sandbox.read"))`: Allows reading the corresponding path when holding a read token.
284
+ - `(allow file-read* file-write* (extension "com.workbuddy.sandbox.read-write"))`: Allows reading and writing the corresponding path when holding a read-write token.
285
+
286
+ This is the underlying enforcement mechanism of the brokered shell security model: toybox must first obtain a sandbox extension token from the broker before Seatbelt will allow write operations. The eager token request ensures that the broker can complete permission checks and pre-modification backups before Seatbelt grants access.
287
+
288
+ ## Key Command Coverage Methods
289
+
290
+ ### `>`
291
+
292
+ `>` is a zsh redirection, not an external command.
293
+
294
+ ```bash
295
+ echo hi > a.txt
296
+ ```
297
+
298
+ Chain:
299
+
300
+ 1. Custom zsh calls `codebuddy_brokered_open()` during the redirection phase.
301
+ 2. zsh sends a `com.workbuddy.sandbox.read-write` text token request.
302
+ 3. agent-cli checks permissions.
303
+ 4. If `a.txt` already exists and is a regular file, agent-cli sends `ModifyBackup` before returning the token or `SANDBOX`.
304
+ 5. zsh performs the real `open(O_TRUNC)`, and the file is truncated.
305
+
306
+ ### `cp`
307
+
308
+ ```bash
309
+ cp source.txt target.txt
310
+ ```
311
+
312
+ Chain:
313
+
314
+ 1. `cp` hits `brokered-bin/cp`.
315
+ 2. The dispatcher executes toybox `cp`.
316
+ 3. toybox `cp` sends a read token request when opening the source file.
317
+ 4. toybox `cp` sends a write token request when opening the target file; overwriting an existing target typically goes through `openat(..., O_TRUNC, ...)`.
318
+ 5. agent-cli sends `ModifyBackup` for the existing target file before granting the write token.
319
+
320
+ The JSON `copy` host-op is not currently used to cover `cp`. The TS side has a `copyPath()` capability, but the macOS toybox's `cp` coverage mainly comes from opening the write target.
321
+
322
+ ### `mv`
323
+
324
+ ```bash
325
+ mv source.txt target.txt
326
+ ```
327
+
328
+ Chain:
329
+
330
+ 1. `mv` hits `brokered-bin/mv`.
331
+ 2. toybox `mv` calls `rename()`.
332
+ 3. `rename()` is macro-replaced with `toybox_rename()`.
333
+ 4. `toybox_rename()` sends a JSON `HostFileOperation rename`, including `from` and `to`.
334
+ 5. The agent-cli host service checks the delete permission on the source and the write permission on the target separately.
335
+ 6. Before executing the host rename, `ModifyBackup` is performed on existing regular files for both the source and target.
336
+ 7. The host side performs the real `rename(from, to)`, and toybox considers the command successful after receiving ok.
337
+
338
+ If the host rename returns a cross-device error, toybox maps the error to `EXDEV`, preserving `mv`'s own copy + delete fallback semantics. The copy write target and delete source file in the fallback still go through brokered open or host delete respectively.
339
+
340
+ ### `sed -i`
341
+
342
+ ```bash
343
+ sed -i '' 's/a/b/g' file.txt
344
+ ```
345
+
346
+ The common implementation of macOS / toybox `sed -i` is:
347
+
348
+ 1. Read the original file.
349
+ 2. Write to a temporary file.
350
+ 3. Use `rename(temp, file.txt)` to overwrite the original file.
351
+
352
+ If only the write target file is monitored, the actual timing of overwriting the original file is missed, because the write occurs on the temporary file. The current solution handles this through the toybox `rename` hook:
353
+
354
+ 1. `sed` hits `brokered-bin/sed`.
355
+ 2. toybox `sed` writes to a temporary file; the temporary file's open goes through the write token; since the temporary file typically does not exist, no backup is produced.
356
+ 3. toybox `sed` calls `rename(temp, file.txt)`.
357
+ 4. `toybox_rename()` sends a JSON `HostFileOperation rename`.
358
+ 5. agent-cli sends `ModifyBackup` for the existing `file.txt` before the host rename.
359
+ 6. The host side performs the rename overwrite.
360
+
361
+ This is why `mv` and `sed -i` are classified as the same type of problem: both are essentially about the backup timing "before the rename overwrites the target file."
362
+
363
+ ### `truncate`
364
+
365
+ ```bash
366
+ truncate -s 0 file.txt
367
+ ```
368
+
369
+ The toybox `truncate` implementation does not directly use `open(O_TRUNC)`, but instead:
370
+
371
+ 1. `loopfiles_rw(..., O_WRONLY | O_CLOEXEC | ...)` opens the target file.
372
+ 2. Calls `ftruncate(fd, size)` on the fd.
373
+
374
+ The current coverage relies on the first step:
375
+
376
+ 1. `truncate` hits `brokered-bin/truncate`.
377
+ 2. toybox `truncate` triggers `toybox_open()` when opening the target file.
378
+ 3. Since the flags include `O_WRONLY`, toybox sends a write token request.
379
+ 4. agent-cli sends `ModifyBackup` for the existing regular file before returning the token or `SANDBOX`.
380
+ 5. toybox then calls `ftruncate()` to modify the file length.
381
+
382
+ There is currently no separate hook for `ftruncate()`. Therefore, brokered toybox's `truncate` is covered; external binaries that bypass brokered toybox and directly call `ftruncate()` are outside the scope of this solution.
383
+
384
+ ### `dd`
385
+
386
+ ```bash
387
+ dd if=source.bin of=file.bin bs=1M
388
+ ```
389
+
390
+ toybox `dd` writes to the output target in a manner similar to `truncate`/`cp` (opening the target path via `open`), but `of=` is a `key=value` operand, not a positional parameter. `codebuddy-toybox-dispatch` performs an additional shell-layer pre-backup before calling toybox:
391
+
392
+ 1. `dd` hits `brokered-bin/dd`.
393
+ 2. The dispatcher parses all operands, finds `of=<path>`, and performs a write token pre-check + `ModifyBackup` on the existing regular file (reusing `truncate`/`tee`'s `__cb_prebackup_path`).
394
+ 3. The dispatcher executes toybox `dd`, which triggers another `toybox_open()` write token request when opening the `of=` target.
395
+ 4. The content written by both backups is the same (no actual write occurs between the pre-backup and toybox's own open hook), so correctness is not affected; it just adds one extra IPC round-trip.
396
+
397
+ When `dd` does not specify `of=`, output goes to stdout, is not persisted to disk, and no pre-backup is triggered.
398
+
399
+ ## ModifyBackup / CommitModifyBackup Call Chain
400
+
401
+ ### Enabling Backup
402
+
403
+ At the start of each real user turn:
404
+
405
+ 1. `SandboxAgentRunInterceptor` reads `sandbox.fileBackup`.
406
+ 2. Writes `SandboxTurnConfig.enableFileBackup` and `fileBackupMaxSizeMB`.
407
+ 3. Ensures the `sandbox-cli` session is ready.
408
+ 4. Sends `EnableModifyBackup`:
409
+
410
+ ```json
411
+ {
412
+ "enabled": true,
413
+ "fileBackupMaxSizeMB": 10
414
+ }
415
+ ```
416
+
417
+ `fileBackupMaxSizeMB` is normalized against minimum, maximum, and default values; the actual storage limit is enforced by `sandbox-cli`.
418
+
419
+ ### Pre-modification Backup
420
+
421
+ There are two entry points that send `ModifyBackup`.
422
+
423
+ text token request write path:
424
+
425
+ - Entry: `BrokeredSandboxIpcServer.handleToyboxTextLine()`.
426
+ - Condition: operation is `write`, permission result is not deny, `enableFileBackup=true`.
427
+ - Behavior: If the target already exists and is a regular file, sends `ModifyBackup { targetPath }`.
428
+ - Failure strategy: If backup fails, returns `DENY`; the real write does not proceed.
429
+
430
+ JSON host operation:
431
+
432
+ - Entry: `BrokeredSandboxHostService`.
433
+ - Condition: The host operation modifies existing content, `enableFileBackup=true`.
434
+ - `delete` / `touch`: Backs up existing regular files in `path`.
435
+ - `rename`: Backs up existing regular files in `from` and `to`.
436
+ - `copy`: Backs up existing regular files in `to`.
437
+ - `chmod` / `mkdir`: Does not trigger backup (`chmod` only changes metadata, `mkdir` creates a new directory).
438
+ - `link`: Does not trigger backup.
439
+ - Failure strategy: If backup fails, the host action is not executed and the operation returns failure.
440
+
441
+ Both entry points only back up "existing regular files." Newly created files, directories, special files, and pure metadata restorations are not within the current content backup semantics.
442
+
443
+ ### Finalization Commit
444
+
445
+ When the turn ends and enters `FINAL_STOP`, `FinalizationModifyBackupHook` executes:
446
+
447
+ 1. Checks `enableFileBackup`.
448
+ 2. Checks whether an open sandbox session exists.
449
+ 3. Generates a short commit message from the most recent real user message.
450
+ 4. Sends `CommitModifyBackup { commitMsg }` to `sandbox-cli`.
451
+
452
+ This hook does not filter by `final_stop_reason`. As long as file backup was enabled for the current turn and a sandbox session exists, it attempts to commit the backup cycle. Commit failure only logs a message and does not block agent finalization.
453
+
454
+ ## Permission Approval and Backup Ordering
455
+
456
+ The ordering is the core of the security semantics.
457
+
458
+ text token request write path:
459
+
460
+ ```text
461
+ zsh/toybox requests write token
462
+ -> agent-cli validates session
463
+ -> fileSafety policy check
464
+ -> if approval is needed, records brokered prompt block and returns DENY to zsh/toybox
465
+ -> SandboxOrchestrator merges prompt blocks, requests unified user approval
466
+ -> after user approves, native reruns the entire command; if user denies, the sandbox failure result is retained
467
+ ```
468
+
469
+ Write path without approval needed or already directly allowed by rules:
470
+
471
+ ```text
472
+ zsh/toybox requests write token
473
+ -> agent-cli validates session
474
+ -> fileSafety policy check passes
475
+ -> ModifyBackup for existing regular files
476
+ -> returns token or SANDBOX
477
+ -> zsh/toybox performs real open/write/truncate
478
+ ```
479
+
480
+ JSON host operation:
481
+
482
+ ```text
483
+ toybox requests HostFileOperation
484
+ -> agent-cli validates session
485
+ -> source/target permission check
486
+ -> resolved path safety check
487
+ -> if approval is needed, records brokered prompt block and returns failure to toybox
488
+ -> SandboxOrchestrator merges prompt blocks, requests unified user approval
489
+ -> after user approves, native reruns the entire command; if user denies, the sandbox failure result is retained
490
+ ```
491
+
492
+ Host operation without approval needed or already directly allowed by rules:
493
+
494
+ ```text
495
+ toybox requests HostFileOperation
496
+ -> agent-cli validates session
497
+ -> source/target permission check
498
+ -> resolved path safety check passes
499
+ -> ModifyBackup for affected existing regular files
500
+ -> host side executes delete/rename/copy/...
501
+ -> returns result to toybox
502
+ ```
503
+
504
+ Key principles:
505
+
506
+ - The current main flow does not wait for user approval within the IPC; the brokered shell only records prompt blocks, and after outer approval, native rerun is used.
507
+ - When permissions are not granted or approval is needed but not yet obtained from the outer layer, no backup is performed and no real modification is executed.
508
+ - When permissions are granted but backup fails, no real modification is executed.
509
+ - Backup must occur before token return or host action execution.
510
+ - `SANDBOX` means the sandbox rules already allow access, but it does not skip the pre-write backup.
511
+
512
+ ## Runtime Behavior Constraints
513
+
514
+ ### Concurrency Model
515
+
516
+ The current agent-cli `BrokeredSandboxIpcServer` is a single-instance Unix socket server; all Bash tool calls share the same socket endpoint. Concurrent requests are processed serially through the Node.js event loop, without explicit locking.
517
+
518
+ Isolation granularity is provided by `toolCallId`: each Bash tool call injects an independent `CODEBUDDY_SANDBOX_BROKER_TOOL_CALL_ID`, used for audit and backup attribution. `sessionId` validation prevents cross-session request leakage.
519
+
520
+ TOCTOU risk assessment: For the text token request path, there is a time window between backup completion and token return, but the sandboxed process cannot perform real writes through Seatbelt before receiving the token, so there is no traditional TOCTOU "check-then-use tampering" issue. For the host-op path (rename/delete, etc.), backup + real operation are both executed sequentially within the same async function on the broker side (check-then-act); the Node.js event loop guarantees that no other requests from the same socket are interleaved, but it does not prevent out-of-sandbox processes from modifying files within the window — this is a known trust boundary.
521
+
522
+ ### Timeout and Blocking
523
+
524
+ The zsh side has a socket-level timeout setting; the toybox side currently has no explicit client timeout. The current agent-cli main flow does not wait for user approval within the broker IPC: when approval is needed, the broker records a prompt block and returns rejection/failure, letting the sandbox attempt end, and then `SandboxOrchestrator` initiates unified approval and native rerun.
525
+
526
+ If the broker process crashes or the socket is closed, the zsh/toybox side `read()` returns EOF, and the command terminates with an I/O error.
527
+
528
+ ### Performance Impact
529
+
530
+ The toybox open hook uses an eager mode; every `open` call incurs an IPC round-trip. For commands with high-frequency opens (such as `find`, `grep` traversing large directory trees), this introduces per-file socket communication overhead.
531
+
532
+ There is currently no token caching or batch request mechanism. Read-only commands (such as `grep`) also go through the broker on every open, but the broker-side read path does not trigger backup or require approval, so the processing overhead is lower.
533
+
534
+ In practice, with typical sandbox working directory sizes (hundreds to thousands of files), the latency impact is acceptable. If large codebase traversal scenarios need to be covered in the future, a read token cache can be introduced.
535
+
536
+ ### Security Trust Boundary
537
+
538
+ The broker IPC Unix socket uses file permissions for isolation:
539
+
540
+ - Socket directory: `chmod 0o700` (only the current user can enter).
541
+ - Socket file: `chmod 0o600` (only the current user can read/write).
542
+
543
+ This means that processes of other users on the same machine cannot connect to the broker socket. However, sandboxed processes run under the same user as agent-cli, so any process inside the sandbox can connect as long as it knows the socket path.
544
+
545
+ `sessionId` is injected into the sandbox via the environment variable `CODEBUDDY_SANDBOX_BROKER_SESSION_ID`, and the broker validates that the `sessionId` in the request matches the current session. A malicious process inside the sandbox can read the environment variable to obtain a valid session ID, but this is acceptable under the current threat model: processes inside the sandbox are already constrained by Seatbelt, and even if they can send requests, they still need to pass `fileSafety` policy and user approval to obtain write tokens. The broker is not the sole line of defense; it works together with the Seatbelt sandbox, file policies, and user approval to form defense in depth.
546
+
547
+ ### Symlink Path Resolution
548
+
549
+ The host service uses three resolution strategies for file paths:
550
+
551
+ - `resolveExistingPath`: Resolves to the real path; rejects if the path does not exist. Used for `chmod` (target must exist) and hard link source.
552
+ - `resolvePathThroughParent`: Resolves the parent directory to the real path and appends the filename, allowing the target to not yet exist. Used for `delete`, `mkdir`, and `rename`/`copy` source and target.
553
+ - `resolveExistingOrCreatablePath`: If the path exists, resolves to the real path; if not, resolves the parent directory. Used for `touch` (may create a new file) and token requests.
554
+
555
+ All strategies execute `refuseIfResolvedPathNotAllowed` after resolution: if the real path after symlink resolution falls outside the range allowed by the security policy, even if the symlink's own path is allowed, the operation is rejected. This prevents bypassing sandbox file policy boundaries via symlinks.
556
+
557
+ ### Path Normalization and Rule Matching
558
+
559
+ When troubleshooting "why a certain rule was not matched," you need to understand the three layers of path normalization:
560
+
561
+ **zsh side**: Custom zsh's `codebuddy_brokered_authorize_path()` normalizes paths before sending token requests, applying `/private/var` → `/var`, `/private/tmp` → `/tmp` alias normalization (macOS-specific firmlink mapping), ensuring that request paths are consistent with user perception.
562
+
563
+ **toybox side**: `toybox_open()` and other wrappers use `realpath`-style resolution before sending text token requests, resolving symlinks and eliminating `.`/`..`. However, `toybox_rename()` uses lexical absolute paths (without resolving symlinks), to preserve the semantics of the rename operation itself (rename may act on the symlink itself rather than its target).
564
+
565
+ **agent-cli side**: `normalisePathForRuleMatch()` performs the following before fileSafety rule matching:
566
+ - Backslash → forward slash
567
+ - Removes the temporary file prefix produced by macOS BSD `sed -i` (`.!<PID>!filename` → `filename`)
568
+ - Home directory prefix → `~`
569
+ - `normaliseSandboxWritePath()` additionally does `/private/var` → `/var`, `/private/tmp` → `/tmp`
570
+
571
+ When debugging, you can search for `phase=ipc-token` or `phase=host-policy` in the logs; the logs will output both the original path and the normalized path (`path=` vs `normalisedPath=`), allowing you to compare against the rule configuration to determine the matching logic.
572
+
573
+ ## Known Boundaries and Non-covered Scope
574
+
575
+ The current solution covers "Bash commands that enter the macOS brokered shell runtime."
576
+
577
+ ### Execution Paths That Do Not Enter the Brokered Shell
578
+
579
+ The following situations cause commands to bypass the brokered shell runtime; the brokered layer's permission control and backup are both ineffective:
580
+
581
+ - `sandbox.enabled = false` or `SandboxOrchestrator` determines local execution.
582
+ - `sandbox.excludedCommands` match: `SandboxShellService` checks the command root and falls back to local.
583
+ - `dangerouslyDisableSandbox = true`: model requests bypass → user approval passes → local execution.
584
+ - User approves native rerun after sandbox execution failure (session-scoped approval cache): subsequent identical commands execute locally directly.
585
+ - `rawCommand` mode: directly spawns the executable, without shell bootstrap, without prefixing `brokered-bin`, without switching to custom zsh. Basic sandbox/broker env is still injected, but the brokered shell's hook chain is not effective.
586
+ - Incomplete vendor bundle: missing `brokered-sandbox-bash-env.sh`, `brokered-bin/codebuddy-toybox-dispatch`, `toybox`, `toybox.sb`, or `zsh` causes the corresponding capability to degrade — missing zsh means no custom shell switch (redirection hook ineffective); missing toybox or brokered-bin means commands do not go through toybox hooks (open/rename hook ineffective).
587
+
588
+ ### Scopes That Enter the Brokered Shell But Are Not Covered
589
+
590
+ - Calling system commands by absolute path, e.g., `/usr/bin/sed -i ...`, bypasses `brokered-bin`; toybox hooks are not effective.
591
+ - Commands that explicitly rewrite `PATH` and move `brokered-bin` to the back may bypass the toybox shim.
592
+ - When the dispatcher encounters an applet or option not supported by toybox, it falls back to the system command; after fallback, toybox open/rename hooks are no longer available.
593
+ - Internal writes by external binaries, `ftruncate()`, `mmap` writes, native `rename()`, etc. are not visible to toybox hooks.
594
+ - zsh can cover shell's own redirections, but cannot automatically hook libc calls of arbitrary external binaries.
595
+
596
+ ### Backup Semantic Boundaries
597
+
598
+ - The current content backup only handles existing regular files; directories, sockets, device files, symlink metadata, permissions, mtime, etc. are not complete restoration targets.
599
+ - `touch` is primarily a metadata modification. The TS host service can back up existing regular file content on the host-op path, but there is currently no metadata-level restoration semantics.
600
+ - `cp` currently relies mainly on the write target's open token coverage; this does not mean toybox has proactively sent a JSON `copy`.
601
+ - Backup commit relies on `CommitModifyBackup` at `FINAL_STOP`. If the process exits abnormally, the handling of uncommitted backup cycles on the `sandbox-cli` side depends on its storage policy.
602
+ - The brokered shell and Write/Edit/MultiEdit are different chains. Tool-level file editing should call `ModifyBackup` directly before the tool writes, and should not depend on the shell runtime (the macOS side's tool-level backup is not yet connected; see "Future Extension Suggestions" below).
603
+
604
+ ## Verification Methods
605
+
606
+ ### agent-cli Unit Tests
607
+
608
+ Key tests:
609
+
610
+ ```bash
611
+ TS_NODE_PROJECT=tsconfig.tsnode.json npx mocha --require ts-node/register \
612
+ --config ../../dev-packages/component/configs/mocharc.yml \
613
+ --parallel=false \
614
+ "./src/node/shell/brokered-bin-dispatch.spec.ts" \
615
+ "./src/node/permission/brokered-sandbox/ipc-server.spec.ts" \
616
+ "./src/node/permission/brokered-sandbox/host-service.spec.ts" \
617
+ "./src/node/hooks/finalization-modify-backup-hook.spec.ts"
618
+ ```
619
+
620
+ Coverage points:
621
+
622
+ - The `truncate` shim must exist and point to `codebuddy-toybox-dispatch`.
623
+ - text write token triggers `ModifyBackup` on existing regular files.
624
+ - `rename` host-op triggers `ModifyBackup` on source / target.
625
+ - Backup failure rejects continuing write or host operation.
626
+ - `CommitModifyBackup` is sent at `FINAL_STOP`.
627
+
628
+ ### toybox Build and Static Check
629
+
630
+ In the `tsbx-macos` repo:
631
+
632
+ ```bash
633
+ ./build-toybox-macos.sh
634
+ lipo toybox-0.8.13/toybox -verify_arch arm64 x86_64
635
+ strings toybox-0.8.13/toybox | grep '"operation":"rename"'
636
+ ```
637
+
638
+ Coverage points:
639
+
640
+ - The artifact is a macOS universal binary.
641
+ - The binary contains the `HostFileOperation rename` marker.
642
+
643
+ ### Fake Broker Behavior Verification
644
+
645
+ Use a local fake broker to receive socket requests, executing each of the following:
646
+
647
+ ```bash
648
+ echo changed > file.txt
649
+ cp source.txt file.txt
650
+ mv source.txt file.txt
651
+ sed -i '' 's/a/b/g' file.txt
652
+ truncate -s 0 file.txt
653
+ ```
654
+
655
+ Expected:
656
+
657
+ - `>` produces a zsh text write token request.
658
+ - `cp` produces a toybox text write token request for the target.
659
+ - `mv` produces a toybox JSON `HostFileOperation rename`.
660
+ - `sed -i` produces a toybox JSON `HostFileOperation rename` when overwriting the original file.
661
+ - `truncate` produces a toybox text write token request.
662
+
663
+ ### Product-level Verification
664
+
665
+ In the WorkBuddy development build:
666
+
667
+ 1. Enable file safety auto-backup.
668
+ 2. Run sandbox Bash commands that overwrite existing files.
669
+ 3. Check the broker shell logs in `~/.codebuddy/` and WorkBuddy dev logs:
670
+ - `phase=ipc-token`
671
+ - `phase=ipc-token-backup`
672
+ - `phase=ipc-host-op`
673
+ - `phase=host-modify-backup`
674
+ - `finalization-modify-backup`
675
+ 4. Confirm in the backup viewing entry that backup records exist, can locate backups, and can be manually restored (see next section).
676
+
677
+ ### Backup Viewing and Restoration Entry
678
+
679
+ The current backup viewing entry is in the WorkBuddy settings page's file safety panel (`SecurityCenterPanel` → `FileDetail`):
680
+
681
+ 1. User clicks "View Backups" → UI sends an `open-modify-backup-dir` event with the current session's `sessionId`.
682
+ 2. `workbuddy-server`'s `SecurityCenterService.openModifyBackupDir()` constructs the backup directory path: `<configDir>/workspace/sessions/<sessionId>/modify_backup`.
683
+ 3. Calls `openPath()` to open the directory in Finder.
684
+
685
+ This is currently an "open backup directory" operation, not a one-click restore UI. The user needs to manually find the backup file in the directory and restore it. If the directory does not exist (no backups were produced in the current turn), the UI shows "No backup records for the current session."
686
+
687
+ ## Future Extension Suggestions
688
+
689
+ ### Write/Edit/MultiEdit Tool-level Backup
690
+
691
+ The brokered shell only covers Bash commands. Tools such as Write/Edit/MultiEdit/NotebookEdit do not go through zsh/toybox; they should reuse `ModifyBackup` directly before the tool writes:
692
+
693
+ ```text
694
+ Tool resolves target path
695
+ -> permission check
696
+ -> if target exists and is a regular file, sends ModifyBackup
697
+ -> tool performs real write
698
+ ```
699
+
700
+ This chain should not depend on the broker shell, nor should it simulate shell commands. What it shares with the brokered shell is the `sandbox-cli` backup storage protocol, not the zsh/toybox runtime.
701
+
702
+ **Current status**: `SandboxWriteRuleGuard` has implemented tool-level guards for Write/Edit/MultiEdit/NotebookEdit (`GUARDED_WRITE_TOOLS`), which can perform permission checks and rule matching on write targets in sandbox mode. `ModifyBackup` is gated by `isModifyBackupSupportedPlatform()`, currently enabled on both `win32` and `darwin`; auto-grant is gated by a separate `supportsAutoGrantRuleEffect()`, avoiding coupling with the backup platform capability.
703
+
704
+ ### More Complete syscall Coverage
705
+
706
+ If future coverage of arbitrary external binaries is needed, toybox shims are insufficient; a lower-level approach is required, such as:
707
+
708
+ - Introducing controlled wrappers for external binaries.
709
+ - Using auditable dynamic library interposition, but evaluating SIP, signing, stability, and security boundaries.
710
+ - Enhancing file event observation at the sandbox-cli layer, but macOS has limited controllability for "pre-modification" backup timing.
711
+
712
+ The current solution's choice of zsh + toybox is a trade-off between controllability, change volume, and maintainability.
713
+
714
+ ### Metadata Backup
715
+
716
+ The current `ModifyBackup` semantics are primarily oriented toward file content restoration. To support metadata restoration for `touch` / `chmod` / `chown` / xattr, etc., the backup record structure needs to be extended:
717
+
718
+ - mode
719
+ - owner / group
720
+ - atime / mtime
721
+ - xattr
722
+ - symlink itself and target
723
+
724
+ This should be designed as an independent capability to avoid complicating the content backup semantics.
725
+
726
+ ### tsbx Documentation Sync
727
+
728
+ The `tsbx-macos` repo should continue to document the low-level binary builds, patches, and demos. This agent-cli document records the product-level integration chain. If zsh or toybox hooks are modified in the future, the following need to be updated simultaneously:
729
+
730
+ - `tsbx-macos/README.md`
731
+ - `tsbx-macos/zsh-macos/README.md`
732
+ - `packages/agent-cli/docs/brokered-shell-macos.md`
733
+
734
+ ## safe-delete → broker IPC Unified Deletion Chain
735
+
736
+ ### Background
737
+
738
+ The safe-delete shim (bash `rm`/`unlink`/`rmdir`, Node.js `fs.unlinkSync`, etc., Python `os.remove`, etc.) originally moved files to the trash directly via the `genie-trash` binary or platform trash API. This chain **lacks** the following capabilities:
739
+
740
+ 1. fileSafety policy check (permission check)
741
+ 2. Symlink resolution safety check
742
+ 3. `ModifyBackup` (pre-modification backup)
743
+ 4. Centralized audit
744
+
745
+ Meanwhile, the brokered shell's toybox `rm` already implements the above capabilities via `HostFileOperation delete → broker IPC → host service → trash`.
746
+
747
+ ### Design
748
+
749
+ The safe-delete deletion entry is unified and routed to broker IPC, so that **regardless of whether the brokeredShell toggle is on**, deletion operations go through the host service's permission check and backup.
750
+
751
+ ```
752
+ safe-delete shim (bash/Node/Python)
753
+ └─ try_broker_delete()
754
+ ├─ broker success (ok=true) → done, file moved to trash
755
+ ├─ broker deny (deny) → fail-closed, no fallback
756
+ └─ broker unavailable (exit 2) → fallback to local trash_one() / trashItem()
757
+ ```
758
+
759
+ ### Key Changes
760
+
761
+ 1. **`brokered-shell-env.ts`**: Broker IPC environment variables (`CODEBUDDY_BROKERED_FS_HOOK_ENABLED`, `CODEBUDDY_SANDBOX_BROKER_IPC_ADDRESS`, etc.) are only injected in the WorkBuddy Desktop environment, but are not controlled by the `brokeredShell` disabled toggle. `brokeredShell` only controls the injection of managed runtime artifacts (toybox, zsh, brokered-bin).
762
+
763
+ 2. **`safe-delete-broker-delete.cjs`** (new): A standalone CJS script that bash shims can call via `node safe-delete-broker-delete.cjs <path>`. Connects to the broker IPC Unix socket and sends a `HostFileOperation delete` request. Exit codes: 0=success, 1=denied (fail-closed), 2=unavailable (fallback).
764
+
765
+ 3. **`safe-delete-common.sh`**: Added `try_broker_delete()` before `try_trash()`, prioritizing the broker IPC path.
766
+
767
+ 4. **`node-safe-delete-shim.cjs`**: Added `tryBrokerDelete()` before `tryTrash()`, synchronously calling broker IPC (`spawnSync` + helper script mode, consistent with `node-brokered-fs-shim.cjs`).
768
+
769
+ 5. **`safe-delete-env.ts`**: Injects the `CODEBUDDY_SAFE_DELETE_BROKER_DELETE` environment variable (pointing to the `safe-delete-broker-delete.cjs` path).
770
+
771
+ ### Retention of Old Code
772
+
773
+ The local trash logic (`trash_darwin()` / `trashOnMac()` / `_platform_trash()` / `genie-trash` binary) is **not deleted**, serving as a fallback when broker IPC is unavailable:
774
+
775
+ - Non-macOS platforms
776
+ - Sandbox not enabled (IPC server does not exist)
777
+ - agent-cli standalone CLI mode
778
+ - IPC server not started or socket connection failed