@easonwumac/computer-linker 0.1.2

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 (82) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +21 -0
  3. package/README.md +539 -0
  4. package/SECURITY.md +48 -0
  5. package/dist/api.d.ts +2 -0
  6. package/dist/api.js +360 -0
  7. package/dist/audit.d.ts +70 -0
  8. package/dist/audit.js +102 -0
  9. package/dist/capabilities.d.ts +98 -0
  10. package/dist/capabilities.js +718 -0
  11. package/dist/capability-policy.d.ts +22 -0
  12. package/dist/capability-policy.js +103 -0
  13. package/dist/chatgpt.d.ts +167 -0
  14. package/dist/chatgpt.js +561 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +4621 -0
  17. package/dist/client-smoke.d.ts +44 -0
  18. package/dist/client-smoke.js +639 -0
  19. package/dist/client.d.ts +217 -0
  20. package/dist/client.js +357 -0
  21. package/dist/codex-runs.d.ts +35 -0
  22. package/dist/codex-runs.js +66 -0
  23. package/dist/computer-contract.d.ts +33 -0
  24. package/dist/computer-contract.js +384 -0
  25. package/dist/computer-operation-registry.d.ts +45 -0
  26. package/dist/computer-operation-registry.js +179 -0
  27. package/dist/config-diagnostics.d.ts +11 -0
  28. package/dist/config-diagnostics.js +185 -0
  29. package/dist/config.d.ts +10 -0
  30. package/dist/config.js +69 -0
  31. package/dist/history-insights.d.ts +132 -0
  32. package/dist/history-insights.js +457 -0
  33. package/dist/http-auth.d.ts +3 -0
  34. package/dist/http-auth.js +15 -0
  35. package/dist/mcp-surface.d.ts +5 -0
  36. package/dist/mcp-surface.js +25 -0
  37. package/dist/oauth-provider.d.ts +52 -0
  38. package/dist/oauth-provider.js +325 -0
  39. package/dist/package-metadata.d.ts +7 -0
  40. package/dist/package-metadata.js +24 -0
  41. package/dist/permissions.d.ts +43 -0
  42. package/dist/permissions.js +150 -0
  43. package/dist/platform-shell.d.ts +28 -0
  44. package/dist/platform-shell.js +124 -0
  45. package/dist/processes.d.ts +50 -0
  46. package/dist/processes.js +178 -0
  47. package/dist/profile.d.ts +159 -0
  48. package/dist/profile.js +416 -0
  49. package/dist/screenshot.d.ts +47 -0
  50. package/dist/screenshot.js +302 -0
  51. package/dist/search.d.ts +34 -0
  52. package/dist/search.js +340 -0
  53. package/dist/security.d.ts +10 -0
  54. package/dist/security.js +108 -0
  55. package/dist/sensitive-files.d.ts +4 -0
  56. package/dist/sensitive-files.js +96 -0
  57. package/dist/server.d.ts +9 -0
  58. package/dist/server.js +713 -0
  59. package/dist/service.d.ts +125 -0
  60. package/dist/service.js +486 -0
  61. package/dist/sessions.d.ts +26 -0
  62. package/dist/sessions.js +34 -0
  63. package/dist/tunnels.d.ts +161 -0
  64. package/dist/tunnels.js +1243 -0
  65. package/dist/workspace-operations.d.ts +170 -0
  66. package/dist/workspace-operations.js +3219 -0
  67. package/dist/workspaces.d.ts +61 -0
  68. package/dist/workspaces.js +353 -0
  69. package/docs/agent-instructions.md +65 -0
  70. package/docs/alpha-evidence.example.json +54 -0
  71. package/docs/api-compatibility.md +56 -0
  72. package/docs/architecture.md +561 -0
  73. package/docs/chatgpt-setup.md +397 -0
  74. package/docs/client-recipes.md +98 -0
  75. package/docs/client-sdk.md +163 -0
  76. package/docs/computer-operation-v1.schema.json +143 -0
  77. package/docs/manual-test-plan.md +322 -0
  78. package/docs/product-spec.md +911 -0
  79. package/docs/release-checklist.md +285 -0
  80. package/docs/service-mode.md +99 -0
  81. package/examples/minimal-mcp-client.mjs +114 -0
  82. package/package.json +87 -0
@@ -0,0 +1,3219 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { opendir, readFile, stat } from "node:fs/promises";
4
+ import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
5
+ import { errorMessage, previewCommand, readAuditEvents, writeAuditEvent } from "./audit.js";
6
+ import { readCodexRunRecords, writeCodexRunRecord } from "./codex-runs.js";
7
+ import { operationCapabilityPolicy, workspaceCapabilityPolicy } from "./capability-policy.js";
8
+ import { historyInsightFromEvents } from "./history-insights.js";
9
+ import { assertPermission } from "./permissions.js";
10
+ import { executableCommand, shellCommand } from "./platform-shell.js";
11
+ import { listManagedProcesses, readManagedProcess, startManagedProcess, stopManagedProcess } from "./processes.js";
12
+ import { captureScreenshot, listScreenshotTargets, screenshotCapability } from "./screenshot.js";
13
+ import { findFiles, searchSymbols, searchText } from "./search.js";
14
+ import { formatWorkspacePath } from "./workspaces.js";
15
+ export const workspaceOperationNames = [
16
+ "stat",
17
+ "list",
18
+ "list_details",
19
+ "explain_operation",
20
+ "tree",
21
+ "instructions",
22
+ "agent_skills",
23
+ "coding_context",
24
+ "project_overview",
25
+ "history",
26
+ "history_insight",
27
+ "change_summary",
28
+ "repo_status",
29
+ "git_changes",
30
+ "git_diff",
31
+ "git_log",
32
+ "git_show",
33
+ "git_stage",
34
+ "git_unstage",
35
+ "git_commit",
36
+ "git_worktree_list",
37
+ "git_worktree_create",
38
+ "read",
39
+ "read_many",
40
+ "write",
41
+ "create_file",
42
+ "write_if_unchanged",
43
+ "edit",
44
+ "patch",
45
+ "mkdir",
46
+ "delete",
47
+ "move",
48
+ "find_files",
49
+ "search_text",
50
+ "search_symbols",
51
+ "package_run",
52
+ "package_start",
53
+ "command",
54
+ "process_start",
55
+ "process_list",
56
+ "process_read",
57
+ "process_stop",
58
+ "codex_start",
59
+ "codex",
60
+ "codex_plan",
61
+ "codex_review",
62
+ "codex_fix",
63
+ "codex_test",
64
+ "codex_continue",
65
+ "codex_runs",
66
+ "screen_list",
67
+ "screen_capture",
68
+ "screen_capture_window",
69
+ "screen_capture_process",
70
+ "batch",
71
+ ];
72
+ const DEFAULT_COMMAND_OUTPUT_BYTES = 200000;
73
+ const MAX_COMMAND_OUTPUT_BYTES = 10 * 1024 * 1024;
74
+ export const workspaceOperationCatalog = [
75
+ {
76
+ operation: "stat",
77
+ permission: "read",
78
+ description: "Return metadata for a file, directory, symlink, or other path.",
79
+ requiredFields: [],
80
+ optionalFields: ["path"],
81
+ example: { operation: "stat", path: "." },
82
+ },
83
+ {
84
+ operation: "list",
85
+ permission: "read",
86
+ description: "List names in a directory.",
87
+ requiredFields: [],
88
+ optionalFields: ["path"],
89
+ example: { operation: "list", path: "src" },
90
+ },
91
+ {
92
+ operation: "list_details",
93
+ permission: "read",
94
+ description: "List directory entries with type, size, and modified time.",
95
+ requiredFields: [],
96
+ optionalFields: ["path"],
97
+ example: { operation: "list_details", path: "src" },
98
+ },
99
+ {
100
+ operation: "explain_operation",
101
+ permission: "read",
102
+ description: "Explain whether another workspace operation is allowed here, including required permission and boundary metadata.",
103
+ requiredFields: ["operationName"],
104
+ optionalFields: [],
105
+ example: { operation: "explain_operation", operationName: "package_start" },
106
+ },
107
+ {
108
+ operation: "tree",
109
+ permission: "read",
110
+ description: "List a bounded recursive tree for quickly understanding workspace structure.",
111
+ requiredFields: [],
112
+ optionalFields: ["path", "maxDepth", "maxEntries", "includeFiles"],
113
+ example: { operation: "tree", path: ".", maxDepth: 2, maxEntries: 100, includeFiles: true },
114
+ },
115
+ {
116
+ operation: "instructions",
117
+ permission: "read",
118
+ description: "Read AGENTS.md and CLAUDE.md instruction files from the workspace root to the target path.",
119
+ requiredFields: [],
120
+ optionalFields: ["path", "maxBytes"],
121
+ example: { operation: "instructions", path: "src/api.ts", maxBytes: 65536 },
122
+ },
123
+ {
124
+ operation: "agent_skills",
125
+ permission: "read",
126
+ description: "Discover workspace-scoped agent skills from .codex/skills, .claude/skills, and skills directories.",
127
+ requiredFields: [],
128
+ optionalFields: ["maxResults", "maxBytes"],
129
+ example: { operation: "agent_skills", maxResults: 50, maxBytes: 32768 },
130
+ },
131
+ {
132
+ operation: "coding_context",
133
+ permission: "read",
134
+ description: "Return the initial coding context for a workspace in one call: overview, instructions, skills, tree, and change summary.",
135
+ requiredFields: [],
136
+ optionalFields: ["path", "maxDepth", "maxEntries", "maxBytes", "maxResults"],
137
+ example: { operation: "coding_context", path: ".", maxDepth: 2, maxEntries: 100, maxBytes: 32768, maxResults: 10 },
138
+ },
139
+ {
140
+ operation: "project_overview",
141
+ permission: "read",
142
+ description: "Return a coding-oriented project overview with manifests, package scripts, language hints, instruction files, and suggested next operations.",
143
+ requiredFields: [],
144
+ optionalFields: ["path", "maxDepth", "maxEntries"],
145
+ example: { operation: "project_overview", path: ".", maxDepth: 3, maxEntries: 300 },
146
+ },
147
+ {
148
+ operation: "history",
149
+ permission: "read",
150
+ description: "Return recent audit events for this opened workspace, including tool calls, failures, commands, and paths.",
151
+ requiredFields: [],
152
+ optionalFields: ["maxResults", "query"],
153
+ example: { operation: "history", maxResults: 50, query: "test" },
154
+ },
155
+ {
156
+ operation: "history_insight",
157
+ permission: "read",
158
+ description: "Return an agent-friendly last-operation view, summary, timeline, session/connection summaries, failed replay templates, or redacted debug bundle for this workspace.",
159
+ requiredFields: [],
160
+ optionalFields: ["view", "maxResults", "query"],
161
+ example: { operation: "history_insight", view: "last", maxResults: 50, query: "test" },
162
+ },
163
+ {
164
+ operation: "change_summary",
165
+ permission: "read",
166
+ description: "Return a compact coding change summary with Git branch, counts, changed files, and diff stats without shell permission.",
167
+ requiredFields: [],
168
+ optionalFields: ["path", "maxBytes"],
169
+ example: { operation: "change_summary", path: ".", maxBytes: 65536 },
170
+ },
171
+ {
172
+ operation: "repo_status",
173
+ permission: "read",
174
+ description: "Return git status and optional diff information for the workspace without requiring shell permission.",
175
+ requiredFields: [],
176
+ optionalFields: ["path", "includeDiff", "maxBytes"],
177
+ example: { operation: "repo_status", path: ".", includeDiff: true, maxBytes: 65536 },
178
+ },
179
+ {
180
+ operation: "git_changes",
181
+ permission: "read",
182
+ description: "Return structured Git change entries for staged, unstaged, untracked, ignored, and renamed files.",
183
+ requiredFields: [],
184
+ optionalFields: ["path"],
185
+ example: { operation: "git_changes", path: "." },
186
+ },
187
+ {
188
+ operation: "git_diff",
189
+ permission: "read",
190
+ description: "Return a bounded Git diff for the repository or selected workspace paths without shell permission.",
191
+ requiredFields: [],
192
+ optionalFields: ["path", "paths", "staged", "maxBytes"],
193
+ example: { operation: "git_diff", path: ".", paths: ["src/app.ts"], staged: false, maxBytes: 65536 },
194
+ },
195
+ {
196
+ operation: "git_log",
197
+ permission: "read",
198
+ description: "Return recent Git commits for the repository or selected workspace paths without shell permission.",
199
+ requiredFields: [],
200
+ optionalFields: ["path", "paths", "maxResults"],
201
+ example: { operation: "git_log", path: ".", paths: ["src/app.ts"], maxResults: 20 },
202
+ },
203
+ {
204
+ operation: "git_show",
205
+ permission: "read",
206
+ description: "Return a bounded Git commit or object view, optionally limited to selected workspace paths, without shell permission.",
207
+ requiredFields: [],
208
+ optionalFields: ["path", "ref", "paths", "maxBytes"],
209
+ example: { operation: "git_show", path: ".", ref: "HEAD", paths: ["src/app.ts"], maxBytes: 65536 },
210
+ },
211
+ {
212
+ operation: "git_stage",
213
+ permission: "write",
214
+ description: "Stage selected workspace paths in Git without shell permission.",
215
+ requiredFields: ["paths"],
216
+ optionalFields: ["path"],
217
+ example: { operation: "git_stage", path: ".", paths: ["src/app.ts", "README.md"] },
218
+ },
219
+ {
220
+ operation: "git_unstage",
221
+ permission: "write",
222
+ description: "Unstage selected workspace paths in Git without shell permission.",
223
+ requiredFields: ["paths"],
224
+ optionalFields: ["path"],
225
+ example: { operation: "git_unstage", path: ".", paths: ["src/app.ts"] },
226
+ },
227
+ {
228
+ operation: "git_commit",
229
+ permission: "write",
230
+ description: "Create a Git commit from currently staged files after verifying every staged path is inside the workspace.",
231
+ requiredFields: ["message"],
232
+ optionalFields: ["path"],
233
+ example: { operation: "git_commit", path: ".", message: "Implement workspace search" },
234
+ },
235
+ {
236
+ operation: "git_worktree_list",
237
+ permission: "read",
238
+ description: "List Git worktrees for a repository path inside the workspace.",
239
+ requiredFields: [],
240
+ optionalFields: ["path"],
241
+ example: { operation: "git_worktree_list", path: "." },
242
+ },
243
+ {
244
+ operation: "git_worktree_create",
245
+ permission: "write",
246
+ description: "Create an isolated Git worktree at a workspace-bounded target path. This runs git directly without shell expansion.",
247
+ requiredFields: ["toPath"],
248
+ optionalFields: ["path", "branch", "startPoint"],
249
+ example: { operation: "git_worktree_create", path: ".", toPath: ".localport/worktrees/feature-a", branch: "feature-a", startPoint: "HEAD" },
250
+ },
251
+ {
252
+ operation: "read",
253
+ permission: "read",
254
+ description: "Read a UTF-8 file, optionally bounded by line range or byte count.",
255
+ requiredFields: ["path"],
256
+ optionalFields: ["startLine", "lineCount", "maxBytes"],
257
+ example: { operation: "read", path: "README.md", startLine: 1, lineCount: 80, maxBytes: 65536 },
258
+ },
259
+ {
260
+ operation: "read_many",
261
+ permission: "read",
262
+ description: "Read multiple UTF-8 files in one workspace-scoped call, with per-file truncation.",
263
+ requiredFields: ["paths"],
264
+ optionalFields: ["maxBytes"],
265
+ example: { operation: "read_many", paths: ["README.md", "src/index.ts"], maxBytes: 65536 },
266
+ },
267
+ {
268
+ operation: "write",
269
+ permission: "write",
270
+ description: "Create or overwrite a UTF-8 file.",
271
+ requiredFields: ["path", "content"],
272
+ optionalFields: [],
273
+ example: { operation: "write", path: "notes/todo.md", content: "- item\n" },
274
+ },
275
+ {
276
+ operation: "create_file",
277
+ permission: "write",
278
+ description: "Create a new UTF-8 file and fail if the path already exists. Use this for first-run probes or new files when overwriting would be unsafe.",
279
+ requiredFields: ["path", "content"],
280
+ optionalFields: [],
281
+ example: { operation: "create_file", path: "notes/todo.md", content: "- item\n" },
282
+ },
283
+ {
284
+ operation: "write_if_unchanged",
285
+ permission: "write",
286
+ description: "Overwrite a UTF-8 file only if its current full-file sha256 still matches the expected value from a prior read.",
287
+ requiredFields: ["path", "content", "expectedSha256"],
288
+ optionalFields: [],
289
+ example: { operation: "write_if_unchanged", path: "README.md", content: "new content\n", expectedSha256: "..." },
290
+ },
291
+ {
292
+ operation: "edit",
293
+ permission: "write",
294
+ description: "Replace exactly one matching text block in a UTF-8 file.",
295
+ requiredFields: ["path", "oldText", "newText"],
296
+ optionalFields: [],
297
+ example: { operation: "edit", path: "README.md", oldText: "old", newText: "new" },
298
+ },
299
+ {
300
+ operation: "patch",
301
+ permission: "write",
302
+ description: "Apply a unified diff inside the workspace after validating that every touched path stays in bounds.",
303
+ requiredFields: ["patch"],
304
+ optionalFields: [],
305
+ example: { operation: "patch", patch: "diff --git a/README.md b/README.md\n--- a/README.md\n+++ b/README.md\n@@ -1 +1 @@\n-old\n+new\n" },
306
+ },
307
+ {
308
+ operation: "mkdir",
309
+ permission: "write",
310
+ description: "Create a directory and parents if needed.",
311
+ requiredFields: ["path"],
312
+ optionalFields: [],
313
+ example: { operation: "mkdir", path: "tmp/output" },
314
+ },
315
+ {
316
+ operation: "delete",
317
+ permission: "write",
318
+ description: "Delete a file or directory. The workspace root itself cannot be deleted.",
319
+ requiredFields: ["path"],
320
+ optionalFields: ["recursive"],
321
+ example: { operation: "delete", path: "tmp/output", recursive: true },
322
+ },
323
+ {
324
+ operation: "move",
325
+ permission: "write",
326
+ description: "Move or rename a file or directory inside the workspace.",
327
+ requiredFields: ["fromPath", "toPath"],
328
+ optionalFields: [],
329
+ example: { operation: "move", fromPath: "old.txt", toPath: "archive/old.txt" },
330
+ },
331
+ {
332
+ operation: "find_files",
333
+ permission: "read",
334
+ description: "Find file paths quickly using ripgrep when available.",
335
+ requiredFields: [],
336
+ optionalFields: ["path", "pattern", "maxResults"],
337
+ example: { operation: "find_files", path: ".", pattern: "*.ts", maxResults: 50 },
338
+ },
339
+ {
340
+ operation: "search_text",
341
+ permission: "read",
342
+ description: "Search text quickly using ripgrep when available.",
343
+ requiredFields: ["query"],
344
+ optionalFields: ["path", "glob", "fixedStrings", "caseSensitive", "beforeContext", "afterContext", "maxResults"],
345
+ example: { operation: "search_text", path: ".", query: "Computer Linker", glob: "*.ts", beforeContext: 1, afterContext: 2 },
346
+ },
347
+ {
348
+ operation: "search_symbols",
349
+ permission: "read",
350
+ description: "Search common code symbols such as functions, classes, interfaces, types, and enums inside the workspace.",
351
+ requiredFields: [],
352
+ optionalFields: ["path", "query", "glob", "caseSensitive", "maxResults", "maxBytes"],
353
+ example: { operation: "search_symbols", path: ".", query: "Workspace", glob: "*.ts", maxResults: 50 },
354
+ },
355
+ {
356
+ operation: "package_run",
357
+ permission: "shell",
358
+ description: "Run an existing package.json script from the nearest workspace package root without accepting arbitrary shell text.",
359
+ requiredFields: ["script"],
360
+ optionalFields: ["path", "scriptArgs", "timeoutSeconds", "maxOutputBytes"],
361
+ example: { operation: "package_run", path: ".", script: "test", scriptArgs: ["--watch=false"], timeoutSeconds: 120, maxOutputBytes: 200000 },
362
+ },
363
+ {
364
+ operation: "package_start",
365
+ permission: "shell",
366
+ description: "Start an existing package.json script as a managed process without accepting arbitrary shell text.",
367
+ requiredFields: ["script"],
368
+ optionalFields: ["path", "scriptArgs", "timeoutSeconds", "maxOutputBytes"],
369
+ example: { operation: "package_start", path: ".", script: "dev", scriptArgs: ["--host", "127.0.0.1"], timeoutSeconds: 3600, maxOutputBytes: 200000 },
370
+ },
371
+ {
372
+ operation: "command",
373
+ permission: "shell",
374
+ description: "Run a shell command in the workspace or a subdirectory. Non-zero exits return stdout/stderr instead of hiding diagnostic output.",
375
+ requiredFields: ["command"],
376
+ optionalFields: ["workingDirectory", "timeoutSeconds", "maxOutputBytes"],
377
+ example: { operation: "command", command: "npm test", workingDirectory: ".", timeoutSeconds: 120, maxOutputBytes: 200000 },
378
+ },
379
+ {
380
+ operation: "process_start",
381
+ permission: "shell",
382
+ description: "Start a long-running shell process in the workspace, such as a dev server or watch task.",
383
+ requiredFields: ["command"],
384
+ optionalFields: ["workingDirectory", "timeoutSeconds", "maxOutputBytes"],
385
+ example: { operation: "process_start", command: "npm run dev", workingDirectory: ".", timeoutSeconds: 3600, maxOutputBytes: 200000 },
386
+ },
387
+ {
388
+ operation: "process_list",
389
+ permission: "shell",
390
+ description: "List background processes started for this configured workspace.",
391
+ requiredFields: [],
392
+ optionalFields: [],
393
+ example: { operation: "process_list" },
394
+ },
395
+ {
396
+ operation: "process_read",
397
+ permission: "shell",
398
+ description: "Read status and buffered stdout/stderr for a workspace background process.",
399
+ requiredFields: ["processId"],
400
+ optionalFields: [],
401
+ example: { operation: "process_read", processId: "proc_..." },
402
+ },
403
+ {
404
+ operation: "process_stop",
405
+ permission: "shell",
406
+ description: "Stop a workspace background process with SIGTERM by default.",
407
+ requiredFields: ["processId"],
408
+ optionalFields: ["signal"],
409
+ example: { operation: "process_stop", processId: "proc_...", signal: "SIGTERM" },
410
+ },
411
+ {
412
+ operation: "codex_start",
413
+ permission: "codex",
414
+ description: "Start a managed background codex exec task in the workspace. Use process_read and process_stop to inspect or stop it.",
415
+ requiredFields: ["prompt"],
416
+ optionalFields: ["workingDirectory", "timeoutSeconds", "maxOutputBytes"],
417
+ example: { operation: "codex_start", prompt: "Run the tests and summarize failures.", workingDirectory: ".", timeoutSeconds: 1800, maxOutputBytes: 200000 },
418
+ },
419
+ {
420
+ operation: "codex",
421
+ permission: "codex",
422
+ description: "Invoke the local codex CLI in the workspace or a subdirectory. Non-zero exits return stdout/stderr for diagnosis.",
423
+ requiredFields: ["prompt"],
424
+ optionalFields: ["workingDirectory", "timeoutSeconds", "maxOutputBytes"],
425
+ example: { operation: "codex", prompt: "Inspect this repo and summarize failing tests.", workingDirectory: ".", timeoutSeconds: 1800, maxOutputBytes: 200000 },
426
+ },
427
+ {
428
+ operation: "codex_plan",
429
+ permission: "codex",
430
+ description: "Run a Codex planning workflow with project context, current changes, and explicit no-edit planning instructions.",
431
+ requiredFields: ["prompt"],
432
+ optionalFields: ["workingDirectory", "timeoutSeconds", "maxBytes", "maxOutputBytes"],
433
+ example: { operation: "codex_plan", prompt: "Plan the refactor needed to add typed operations.", workingDirectory: ".", timeoutSeconds: 1800, maxOutputBytes: 200000 },
434
+ },
435
+ {
436
+ operation: "codex_review",
437
+ permission: "codex",
438
+ description: "Run a Codex review workflow focused on bugs, regressions, security risks, and missing tests.",
439
+ requiredFields: [],
440
+ optionalFields: ["prompt", "workingDirectory", "timeoutSeconds", "maxBytes", "maxOutputBytes"],
441
+ example: { operation: "codex_review", prompt: "Review the current diff before release.", workingDirectory: ".", timeoutSeconds: 1800, maxOutputBytes: 200000 },
442
+ },
443
+ {
444
+ operation: "codex_fix",
445
+ permission: "codex",
446
+ description: "Run a Codex fix workflow for a user-described issue, asking Codex to edit code and summarize the resulting diff.",
447
+ requiredFields: ["prompt"],
448
+ optionalFields: ["workingDirectory", "timeoutSeconds", "maxBytes", "maxOutputBytes"],
449
+ example: { operation: "codex_fix", prompt: "Fix the failing API test and keep the public contract stable.", workingDirectory: ".", timeoutSeconds: 1800, maxOutputBytes: 200000 },
450
+ },
451
+ {
452
+ operation: "codex_test",
453
+ permission: "codex",
454
+ description: "Run a Codex test workflow that asks Codex to run or reason about the appropriate tests and report failures with next steps.",
455
+ requiredFields: [],
456
+ optionalFields: ["prompt", "script", "workingDirectory", "timeoutSeconds", "maxBytes", "maxOutputBytes"],
457
+ example: { operation: "codex_test", script: "test", prompt: "Verify the workspace operation changes.", workingDirectory: ".", timeoutSeconds: 1800, maxOutputBytes: 200000 },
458
+ },
459
+ {
460
+ operation: "codex_continue",
461
+ permission: "codex",
462
+ description: "Run a Codex continuation workflow using recent Computer Linker history and optional user guidance.",
463
+ requiredFields: [],
464
+ optionalFields: ["prompt", "workflowId", "workingDirectory", "timeoutSeconds", "maxResults", "maxBytes", "maxOutputBytes"],
465
+ example: { operation: "codex_continue", workflowId: "codex_fix_...", prompt: "Continue from the latest failing test.", workingDirectory: ".", maxResults: 50, maxOutputBytes: 200000 },
466
+ },
467
+ {
468
+ operation: "codex_runs",
469
+ permission: "codex",
470
+ description: "List recent persisted Codex workflow run records for this workspace, or inspect one workflow id with bounded stdout/stderr previews and change summaries.",
471
+ requiredFields: [],
472
+ optionalFields: ["workflowId", "maxResults"],
473
+ example: { operation: "codex_runs", workflowId: "codex_fix_...", maxResults: 10 },
474
+ },
475
+ {
476
+ operation: "screen_list",
477
+ permission: "screen",
478
+ description: "List screenshot provider readiness, displays, and capturable windows/processes when available.",
479
+ requiredFields: [],
480
+ optionalFields: [],
481
+ example: { operation: "screen_list" },
482
+ },
483
+ {
484
+ operation: "screen_capture",
485
+ permission: "screen",
486
+ description: "Capture the primary display or selected display when the platform provider supports it.",
487
+ requiredFields: [],
488
+ optionalFields: ["path", "format", "returnMode", "maxWidth", "maxHeight"],
489
+ example: { operation: "screen_capture", path: "primary", format: "png", returnMode: "fileRef" },
490
+ },
491
+ {
492
+ operation: "screen_capture_window",
493
+ permission: "screen",
494
+ description: "Capture a specific visible window when the platform provider supports it.",
495
+ requiredFields: ["path"],
496
+ optionalFields: ["format", "returnMode", "maxWidth", "maxHeight"],
497
+ example: { operation: "screen_capture_window", path: "12345", format: "png", returnMode: "fileRef" },
498
+ },
499
+ {
500
+ operation: "screen_capture_process",
501
+ permission: "screen",
502
+ description: "Capture a visible window for a process id or process name when the platform provider supports it.",
503
+ requiredFields: ["path"],
504
+ optionalFields: ["format", "returnMode", "maxWidth", "maxHeight"],
505
+ example: { operation: "screen_capture_process", path: "Terminal", format: "png", returnMode: "fileRef" },
506
+ },
507
+ {
508
+ operation: "batch",
509
+ permission: "mixed",
510
+ description: "Run up to 25 workspace operations in order and return structured per-operation results. Each item still uses its normal permission check.",
511
+ requiredFields: ["operations"],
512
+ optionalFields: ["continueOnError"],
513
+ example: {
514
+ operation: "batch",
515
+ operations: [
516
+ { operation: "project_overview", path: "." },
517
+ { operation: "read_many", paths: ["README.md", "package.json"], maxBytes: 32768 },
518
+ { operation: "search_text", query: "TODO", glob: "*.ts", maxResults: 20 },
519
+ ],
520
+ continueOnError: true,
521
+ },
522
+ },
523
+ ];
524
+ export const workspaceOperationSafety = workspaceOperationCatalog.map((entry) => ({
525
+ operation: entry.operation,
526
+ permission: entry.permission,
527
+ boundary: operationBoundary(entry.operation),
528
+ note: operationBoundaryNote(entry.operation),
529
+ }));
530
+ export function registerOperation(input) {
531
+ return {
532
+ operation: input.name,
533
+ name: input.name,
534
+ permission: input.permission,
535
+ description: input.description,
536
+ requiredFields: input.schema.requiredFields,
537
+ optionalFields: input.schema.optionalFields,
538
+ example: input.schema.example,
539
+ category: input.category,
540
+ boundary: input.boundary,
541
+ schema: input.schema,
542
+ run: input.run,
543
+ audit: input.audit,
544
+ safetyNote: input.safetyNote,
545
+ capabilities: input.capabilities,
546
+ limits: input.limits,
547
+ };
548
+ }
549
+ export function buildWorkspaceOperationRegistry(catalog = workspaceOperationCatalog) {
550
+ assertOperationCatalogCoverage(catalog);
551
+ return catalog.map((entry) => {
552
+ const safety = workspaceOperationSafety.find((item) => item.operation === entry.operation);
553
+ if (!safety)
554
+ throw new Error(`Missing safety metadata for operation: ${entry.operation}`);
555
+ const policy = operationCapabilityPolicy(entry.operation);
556
+ return registerOperation({
557
+ name: entry.operation,
558
+ category: operationCategory(entry.operation),
559
+ permission: entry.permission,
560
+ boundary: safety.boundary,
561
+ schema: {
562
+ requiredFields: entry.requiredFields,
563
+ optionalFields: entry.optionalFields,
564
+ example: entry.example,
565
+ },
566
+ run: operationRunRegistration(entry.operation),
567
+ audit: {
568
+ eventType: "tool_call",
569
+ fields: "workspaceOperationAuditFields",
570
+ redactions: [
571
+ "file contents",
572
+ "write payloads",
573
+ "patch bodies",
574
+ "screenshot pixels",
575
+ "owner tokens",
576
+ "OAuth tokens",
577
+ ],
578
+ },
579
+ description: entry.description,
580
+ safetyNote: safety.note,
581
+ capabilities: policy.capabilities,
582
+ limits: policy.limits,
583
+ });
584
+ });
585
+ }
586
+ export const workspaceOperationRegistry = buildWorkspaceOperationRegistry();
587
+ export function publicWorkspaceOperationRegistry(registry = workspaceOperationRegistry) {
588
+ return registry.map(({ run, ...entry }) => ({
589
+ ...entry,
590
+ run: {
591
+ type: run.type,
592
+ handler: run.handler,
593
+ },
594
+ }));
595
+ }
596
+ export const workspaceOperationContract = {
597
+ version: 1,
598
+ mcp: {
599
+ tool: "workspace_operation",
600
+ requiredFields: ["workspaceId", "op"],
601
+ },
602
+ jsonApi: {
603
+ endpoint: "POST /api/v1/control",
604
+ action: "operation",
605
+ requiredFields: ["action", "workspace", "op"],
606
+ },
607
+ envelope: {
608
+ workspace: "app",
609
+ op: "read",
610
+ target: "README.md",
611
+ input: {},
612
+ options: { maxBytes: 65536 },
613
+ },
614
+ targetMapping: {
615
+ default: "path",
616
+ command: "workingDirectory",
617
+ process_start: "workingDirectory",
618
+ process_read: "processId",
619
+ process_stop: "processId",
620
+ codex: "workingDirectory",
621
+ codex_start: "workingDirectory",
622
+ codex_plan: "workingDirectory",
623
+ codex_review: "workingDirectory",
624
+ codex_fix: "workingDirectory",
625
+ codex_test: "workingDirectory",
626
+ codex_continue: "workingDirectory",
627
+ codex_runs: "workflowId",
628
+ screen_capture: "displayId",
629
+ screen_capture_window: "windowId",
630
+ screen_capture_process: "processIdOrName",
631
+ explain_operation: "operationName",
632
+ git_worktree_create: "toPath",
633
+ },
634
+ guidance: [
635
+ "Keep the outer envelope stable: workspace/workspaceId, op, target, input, options.",
636
+ "Put operation-specific payload fields in input and bounded controls such as maxBytes, maxResults, maxOutputBytes, and timeoutSeconds in options.",
637
+ "Check allowedOperations or operation_registry before write, package, process, shell, Git write, or Codex operations.",
638
+ "Use op=coding_context first for coding tasks, then search_text/search_symbols before reading many files.",
639
+ ],
640
+ };
641
+ function assertOperationCatalogCoverage(catalog) {
642
+ const knownOperations = new Set(workspaceOperationNames);
643
+ const seen = new Set();
644
+ for (const entry of catalog) {
645
+ if (!knownOperations.has(entry.operation)) {
646
+ throw new Error(`Unknown operation registered in catalog: ${entry.operation}`);
647
+ }
648
+ if (seen.has(entry.operation)) {
649
+ throw new Error(`Duplicate operation registered in catalog: ${entry.operation}`);
650
+ }
651
+ seen.add(entry.operation);
652
+ }
653
+ const missing = workspaceOperationNames.filter((operation) => !seen.has(operation));
654
+ if (missing.length > 0) {
655
+ throw new Error(`Missing registered operations: ${missing.join(", ")}`);
656
+ }
657
+ }
658
+ export const workspaceOperationRegistryByName = new Map(workspaceOperationRegistry.map((entry) => [entry.operation, entry]));
659
+ export function workspaceOperationEntry(operation) {
660
+ const entry = workspaceOperationRegistryByName.get(operation);
661
+ if (!entry)
662
+ throw new Error(`Unknown operation: ${operation}`);
663
+ return entry;
664
+ }
665
+ function operationRunRegistration(operation) {
666
+ if (isFileSearchOperation(operation)) {
667
+ return {
668
+ type: "workspace-operation-dispatch",
669
+ handler: "runFileSearchOperation",
670
+ execute: runFileSearchOperation,
671
+ };
672
+ }
673
+ if (isMetadataOperation(operation)) {
674
+ return {
675
+ type: "workspace-operation-dispatch",
676
+ handler: "runMetadataOperation",
677
+ execute: runMetadataOperation,
678
+ };
679
+ }
680
+ if (isCodexOperation(operation)) {
681
+ return {
682
+ type: "workspace-operation-dispatch",
683
+ handler: "runCodexOperation",
684
+ execute: runCodexOperation,
685
+ };
686
+ }
687
+ if (isScreenOperation(operation)) {
688
+ return {
689
+ type: "workspace-operation-dispatch",
690
+ handler: "runScreenOperation",
691
+ execute: runScreenOperation,
692
+ };
693
+ }
694
+ return {
695
+ type: "workspace-operation-dispatch",
696
+ handler: "runWorkspaceOperation",
697
+ execute: dispatchWorkspaceOperation,
698
+ };
699
+ }
700
+ function isCodexOperation(operation) {
701
+ return isCodexExecutionOperation(operation) || operation === "codex_runs";
702
+ }
703
+ function isMetadataOperation(operation) {
704
+ return operation === "explain_operation" ||
705
+ operation === "history" ||
706
+ operation === "history_insight";
707
+ }
708
+ function isFileSearchOperation(operation) {
709
+ return operation === "stat" ||
710
+ operation === "list" ||
711
+ operation === "list_details" ||
712
+ operation === "tree" ||
713
+ operation === "read" ||
714
+ operation === "read_many" ||
715
+ operation === "write" ||
716
+ operation === "create_file" ||
717
+ operation === "write_if_unchanged" ||
718
+ operation === "edit" ||
719
+ operation === "mkdir" ||
720
+ operation === "delete" ||
721
+ operation === "move" ||
722
+ operation === "find_files" ||
723
+ operation === "search_text" ||
724
+ operation === "search_symbols";
725
+ }
726
+ function isScreenOperation(operation) {
727
+ return operation === "screen_list" ||
728
+ operation === "screen_capture" ||
729
+ operation === "screen_capture_window" ||
730
+ operation === "screen_capture_process";
731
+ }
732
+ export function allowedWorkspaceOperations(permissions) {
733
+ const policy = workspaceCapabilityPolicy(permissions);
734
+ return workspaceOperationRegistry
735
+ .filter((entry) => (operationAllowedByLegacyPermission(entry, permissions) &&
736
+ operationAllowedByCapabilityPolicy(entry, policy) &&
737
+ operationSupportedByCurrentRuntime(entry.operation)))
738
+ .map((entry) => entry.operation);
739
+ }
740
+ function operationSupportedByCurrentRuntime(operation) {
741
+ if (operation === "screen_list")
742
+ return true;
743
+ const screenMode = screenCaptureModeForOperation(operation);
744
+ if (!screenMode)
745
+ return true;
746
+ const capability = screenshotCapability();
747
+ return capability.supported && capability.modes.includes(screenMode);
748
+ }
749
+ function screenCaptureModeForOperation(operation) {
750
+ if (operation === "screen_capture")
751
+ return "display";
752
+ if (operation === "screen_capture_window")
753
+ return "window";
754
+ if (operation === "screen_capture_process")
755
+ return "process";
756
+ return undefined;
757
+ }
758
+ function operationAllowedByLegacyPermission(entry, permissions) {
759
+ return entry.permission === "mixed"
760
+ ? permissions.read || permissions.write || permissions.shell || permissions.codex || Boolean(permissions.screen)
761
+ : Boolean(permissions[entry.permission]);
762
+ }
763
+ function operationAllowedByCapabilityPolicy(entry, policy) {
764
+ const capabilities = new Set(policy.capabilities);
765
+ return missingCapabilities(entry, capabilities).length === 0;
766
+ }
767
+ function missingCapabilities(entry, capabilities) {
768
+ return entry.capabilities.filter((capability) => !capabilities.has(capability));
769
+ }
770
+ export function normalizeWorkspaceOperationInput(body) {
771
+ const operation = operationNameFrom(body.operation ?? body.op);
772
+ const input = objectValue(body.input);
773
+ const options = objectValue(body.options);
774
+ const merged = {
775
+ ...options,
776
+ ...input,
777
+ ...body,
778
+ operation,
779
+ };
780
+ delete merged.op;
781
+ delete merged.target;
782
+ delete merged.input;
783
+ delete merged.options;
784
+ applyTarget(operation, body.target, merged);
785
+ return {
786
+ operation,
787
+ operationName: optionalString(merged.operationName),
788
+ path: optionalString(merged.path),
789
+ paths: optionalStringArray(merged.paths),
790
+ content: typeof merged.content === "string" ? merged.content : undefined,
791
+ patch: typeof merged.patch === "string" ? merged.patch : undefined,
792
+ oldText: typeof merged.oldText === "string" ? merged.oldText : undefined,
793
+ newText: typeof merged.newText === "string" ? merged.newText : undefined,
794
+ fromPath: optionalString(merged.fromPath),
795
+ toPath: optionalString(merged.toPath),
796
+ recursive: optionalBoolean(merged.recursive),
797
+ pattern: optionalString(merged.pattern),
798
+ query: optionalString(merged.query),
799
+ glob: optionalString(merged.glob),
800
+ fixedStrings: optionalBoolean(merged.fixedStrings),
801
+ caseSensitive: optionalBoolean(merged.caseSensitive),
802
+ maxResults: optionalPositiveInteger(merged.maxResults),
803
+ view: optionalString(merged.view),
804
+ beforeContext: optionalBoundedNonNegativeInteger(merged.beforeContext, 20),
805
+ afterContext: optionalBoundedNonNegativeInteger(merged.afterContext, 20),
806
+ maxDepth: optionalPositiveInteger(merged.maxDepth),
807
+ maxEntries: optionalPositiveInteger(merged.maxEntries),
808
+ startLine: optionalPositiveInteger(merged.startLine),
809
+ lineCount: optionalBoundedPositiveInteger(merged.lineCount, 10000),
810
+ includeFiles: optionalBoolean(merged.includeFiles),
811
+ maxBytes: optionalBoundedPositiveInteger(merged.maxBytes, 256 * 1024),
812
+ includeDiff: optionalBoolean(merged.includeDiff),
813
+ staged: optionalBoolean(merged.staged),
814
+ expectedSha256: optionalString(merged.expectedSha256),
815
+ message: optionalString(merged.message),
816
+ ref: optionalString(merged.ref),
817
+ script: optionalString(merged.script),
818
+ scriptArgs: optionalStringArray(merged.scriptArgs),
819
+ branch: optionalString(merged.branch),
820
+ startPoint: optionalString(merged.startPoint),
821
+ command: optionalString(merged.command),
822
+ processId: optionalString(merged.processId),
823
+ signal: optionalString(merged.signal),
824
+ prompt: optionalString(merged.prompt),
825
+ workflowId: optionalString(merged.workflowId),
826
+ format: optionalString(merged.format),
827
+ returnMode: optionalString(merged.returnMode ?? merged.return),
828
+ maxWidth: optionalPositiveInteger(merged.maxWidth),
829
+ maxHeight: optionalPositiveInteger(merged.maxHeight),
830
+ workingDirectory: optionalString(merged.workingDirectory),
831
+ timeoutSeconds: optionalPositiveInteger(merged.timeoutSeconds),
832
+ maxOutputBytes: optionalBoundedPositiveInteger(merged.maxOutputBytes, 10 * 1024 * 1024),
833
+ operations: optionalOperationArray(merged.operations),
834
+ continueOnError: optionalBoolean(merged.continueOnError),
835
+ };
836
+ }
837
+ function operationNameFrom(value) {
838
+ const operation = optionalString(value);
839
+ if (!operation || !workspaceOperationNames.includes(operation)) {
840
+ throw new Error(`operation must be one of: ${workspaceOperationNames.join(", ")}`);
841
+ }
842
+ return operation;
843
+ }
844
+ function objectValue(value) {
845
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
846
+ }
847
+ function applyTarget(operation, target, merged) {
848
+ const value = optionalString(target);
849
+ if (!value)
850
+ return;
851
+ if (operation === "process_read" || operation === "process_stop") {
852
+ merged.processId ??= value;
853
+ return;
854
+ }
855
+ if (operation === "codex_runs") {
856
+ merged.workflowId ??= value;
857
+ return;
858
+ }
859
+ if (operation === "explain_operation") {
860
+ merged.operationName ??= value;
861
+ return;
862
+ }
863
+ if (operation === "command" || operation === "process_start" || isCodexExecutionOperation(operation)) {
864
+ merged.workingDirectory ??= value;
865
+ return;
866
+ }
867
+ if (operation === "git_worktree_create") {
868
+ merged.toPath ??= value;
869
+ return;
870
+ }
871
+ merged.path ??= value;
872
+ }
873
+ function operationCategory(operation) {
874
+ if (operation === "batch")
875
+ return "batch";
876
+ if (isCodexExecutionOperation(operation) || operation === "codex_runs")
877
+ return "codex";
878
+ if (operation === "command" || operation === "package_run" || operation === "package_start")
879
+ return "package";
880
+ if (operation === "process_start" || operation === "process_list" || operation === "process_read" || operation === "process_stop")
881
+ return "process";
882
+ if (operation.startsWith("git_") || operation === "repo_status" || operation === "change_summary")
883
+ return "git";
884
+ if (operation === "screen_list" || operation === "screen_capture" || operation === "screen_capture_window" || operation === "screen_capture_process")
885
+ return "screen";
886
+ if (operation === "find_files" || operation === "search_text" || operation === "search_symbols")
887
+ return "search";
888
+ if (operation === "instructions" || operation === "agent_skills" || operation === "coding_context" || operation === "project_overview")
889
+ return "coding";
890
+ if (operation === "history" || operation === "history_insight" || operation === "explain_operation")
891
+ return "metadata";
892
+ return "files";
893
+ }
894
+ function optionalString(value) {
895
+ const text = String(value ?? "").trim();
896
+ return text || undefined;
897
+ }
898
+ function optionalPositiveInteger(value) {
899
+ return optionalBoundedPositiveInteger(value, 1000);
900
+ }
901
+ function optionalBoundedPositiveInteger(value, max) {
902
+ const text = optionalString(value);
903
+ if (!text)
904
+ return undefined;
905
+ const parsed = Number.parseInt(text, 10);
906
+ return Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, max) : undefined;
907
+ }
908
+ function optionalBoundedNonNegativeInteger(value, max) {
909
+ const text = optionalString(value);
910
+ if (!text)
911
+ return undefined;
912
+ const parsed = Number.parseInt(text, 10);
913
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.min(parsed, max) : undefined;
914
+ }
915
+ function optionalBoolean(value) {
916
+ if (value === true || value === "true" || value === "on" || value === "1")
917
+ return true;
918
+ if (value === false || value === "false" || value === "off" || value === "0")
919
+ return false;
920
+ return undefined;
921
+ }
922
+ function optionalStringArray(value) {
923
+ if (!Array.isArray(value))
924
+ return undefined;
925
+ const values = value.map(optionalString).filter((item) => Boolean(item));
926
+ return values.length ? values.slice(0, 100) : undefined;
927
+ }
928
+ function optionalOperationArray(value) {
929
+ if (!Array.isArray(value))
930
+ return undefined;
931
+ const operations = value
932
+ .filter((operation) => Boolean(operation) && typeof operation === "object" && !Array.isArray(operation))
933
+ .slice(0, 25)
934
+ .map(normalizeWorkspaceOperationInput);
935
+ return operations.length ? operations : undefined;
936
+ }
937
+ function operationBoundary(operation) {
938
+ if (operation === "batch")
939
+ return "mixed";
940
+ if (operation === "history" || operation === "history_insight" || operation === "coding_context" || operation === "project_overview" || operation === "instructions" || operation === "agent_skills" || operation === "process_list" || operation === "process_read" || operation === "process_stop" || operation === "codex_runs" || operation === "screen_list") {
941
+ return "workspace-scoped-metadata";
942
+ }
943
+ if (operation === "command" || operation === "package_run" || operation === "package_start" || operation === "process_start" || isCodexExecutionOperation(operation) || operation === "screen_capture" || operation === "screen_capture_window" || operation === "screen_capture_process") {
944
+ return "workspace-cwd-only";
945
+ }
946
+ return "workspace-path-enforced";
947
+ }
948
+ function operationBoundaryNote(operation) {
949
+ if (operation === "batch")
950
+ return "Each child operation keeps its own permission and boundary behavior.";
951
+ if (operation === "command" || operation === "package_run" || operation === "package_start" || operation === "process_start")
952
+ return "The process starts in the workspace, but local package scripts and shell commands are not filesystem sandboxes.";
953
+ if (isCodexExecutionOperation(operation))
954
+ return "Codex starts in the workspace, but Codex and tools it invokes may access broader OS resources.";
955
+ if (operation === "codex_runs")
956
+ return "Returns bounded Codex workflow run records for this configured workspace without full prompts.";
957
+ if (operation === "screen_list")
958
+ return "Returns screenshot provider capability and permission metadata without capturing pixels.";
959
+ if (operation === "screen_capture" || operation === "screen_capture_window" || operation === "screen_capture_process")
960
+ return "Captures local screen pixels through the platform provider and is gated by explicit screen permission.";
961
+ if (operation === "process_list" || operation === "process_read" || operation === "process_stop")
962
+ return "Managed-process access is limited to allowed process kinds Computer Linker started for this configured workspace.";
963
+ if (operation === "history" || operation === "history_insight")
964
+ return "Returns audit metadata for this workspace without file contents or token values.";
965
+ if (operation === "coding_context" || operation === "project_overview" || operation === "instructions" || operation === "agent_skills")
966
+ return "Reads bounded workspace metadata and selected workspace files only.";
967
+ if (operation === "git_stage" || operation === "git_unstage")
968
+ return "Git pathspecs are validated inside the workspace before mutating the repository index.";
969
+ if (operation === "git_commit")
970
+ return "Staged Git paths are checked against the workspace before creating a commit.";
971
+ return "All filesystem paths are resolved and validated inside the opened workspace before execution.";
972
+ }
973
+ function isCodexExecutionOperation(operation) {
974
+ return operation === "codex" ||
975
+ operation === "codex_start" ||
976
+ operation === "codex_plan" ||
977
+ operation === "codex_review" ||
978
+ operation === "codex_fix" ||
979
+ operation === "codex_test" ||
980
+ operation === "codex_continue";
981
+ }
982
+ function allowedProcessKinds(workspace) {
983
+ const kinds = [];
984
+ if (workspace.exposedPath.permissions.shell)
985
+ kinds.push("shell");
986
+ if (workspace.exposedPath.permissions.codex)
987
+ kinds.push("codex");
988
+ if (kinds.length === 0) {
989
+ throw new Error(`shell or codex permission is required for managed processes on exposed path ${workspace.exposedPath.id} (${workspace.exposedPath.path})`);
990
+ }
991
+ return kinds;
992
+ }
993
+ function explainOperation(workspace, operationName) {
994
+ if (!workspaceOperationNames.includes(operationName)) {
995
+ throw new Error(`operationName must be one of: ${workspaceOperationNames.join(", ")}`);
996
+ }
997
+ const operation = operationName;
998
+ const registryEntry = workspaceOperationEntry(operation);
999
+ const capabilityPolicy = workspaceCapabilityPolicy(workspace.exposedPath.permissions);
1000
+ const missingCapabilityList = missingCapabilities(registryEntry, new Set(capabilityPolicy.capabilities));
1001
+ const safety = {
1002
+ operation: registryEntry.operation,
1003
+ permission: registryEntry.permission,
1004
+ boundary: registryEntry.boundary,
1005
+ note: registryEntry.safetyNote,
1006
+ };
1007
+ const allowed = allowedWorkspaceOperations(workspace.exposedPath.permissions).includes(operation);
1008
+ const requiredPermission = registryEntry.permission;
1009
+ const missingPermission = requiredPermission === "mixed"
1010
+ ? undefined
1011
+ : workspace.exposedPath.permissions[requiredPermission] ? undefined : requiredPermission;
1012
+ return {
1013
+ operation,
1014
+ allowed,
1015
+ requiredPermission,
1016
+ missingPermission,
1017
+ workspace: {
1018
+ id: workspace.exposedPath.id,
1019
+ name: workspace.exposedPath.name,
1020
+ permissions: workspace.exposedPath.permissions,
1021
+ capabilityPolicy,
1022
+ },
1023
+ requiredCapabilities: registryEntry.capabilities,
1024
+ missingCapabilities: missingCapabilityList,
1025
+ catalog: registryEntry,
1026
+ registry: registryEntry,
1027
+ safety,
1028
+ };
1029
+ }
1030
+ export async function runWorkspaceOperation(registry, workspace, input) {
1031
+ return workspaceOperationEntry(input.operation).run.execute(registry, workspace, input);
1032
+ }
1033
+ async function runMetadataOperation(_registry, workspace, input) {
1034
+ switch (input.operation) {
1035
+ case "explain_operation": {
1036
+ return explainOperation(workspace, required(input.operationName, "operationName"));
1037
+ }
1038
+ case "history": {
1039
+ return {
1040
+ events: workspaceHistory(workspace, {
1041
+ maxResults: input.maxResults,
1042
+ query: input.query,
1043
+ }),
1044
+ };
1045
+ }
1046
+ case "history_insight": {
1047
+ return workspaceHistoryInsight(workspace, {
1048
+ view: input.view,
1049
+ maxResults: input.maxResults,
1050
+ query: input.query,
1051
+ });
1052
+ }
1053
+ default:
1054
+ throw new Error(`runMetadataOperation cannot execute operation: ${input.operation}`);
1055
+ }
1056
+ }
1057
+ async function runFileSearchOperation(registry, workspace, input) {
1058
+ switch (input.operation) {
1059
+ case "stat": {
1060
+ return { entry: await registry.statPath(workspace.id, input.path ?? ".") };
1061
+ }
1062
+ case "list": {
1063
+ return { entries: await registry.listDirectory(workspace.id, input.path ?? ".") };
1064
+ }
1065
+ case "list_details": {
1066
+ return { entries: await registry.listDirectoryEntries(workspace.id, input.path ?? ".") };
1067
+ }
1068
+ case "tree": {
1069
+ return {
1070
+ entries: await registry.tree(workspace.id, input.path ?? ".", {
1071
+ maxDepth: input.maxDepth,
1072
+ maxEntries: input.maxEntries,
1073
+ includeFiles: input.includeFiles,
1074
+ }),
1075
+ };
1076
+ }
1077
+ case "read": {
1078
+ const path = required(input.path, "path");
1079
+ return {
1080
+ path,
1081
+ ...readResult(await registry.readFile(workspace.id, path), {
1082
+ startLine: input.startLine,
1083
+ lineCount: input.lineCount,
1084
+ maxBytes: input.maxBytes,
1085
+ }),
1086
+ };
1087
+ }
1088
+ case "read_many": {
1089
+ const maxBytes = normalizeBoundedPositiveInteger(input.maxBytes, 64 * 1024, 256 * 1024);
1090
+ return {
1091
+ files: await Promise.all(requiredPaths(input.paths).map(async (path) => {
1092
+ const content = await registry.readFile(workspace.id, path);
1093
+ return {
1094
+ path,
1095
+ content: truncateText(content, maxBytes),
1096
+ sizeBytes: Buffer.byteLength(content, "utf8"),
1097
+ sha256: sha256(content),
1098
+ truncated: Buffer.byteLength(content, "utf8") > maxBytes,
1099
+ };
1100
+ })),
1101
+ };
1102
+ }
1103
+ case "write": {
1104
+ const path = required(input.path, "path");
1105
+ await registry.writeFile(workspace.id, path, requiredRaw(input.content, "content"));
1106
+ return { path };
1107
+ }
1108
+ case "create_file": {
1109
+ const path = required(input.path, "path");
1110
+ const content = requiredRaw(input.content, "content");
1111
+ await registry.createFile(workspace.id, path, content);
1112
+ return {
1113
+ path,
1114
+ created: true,
1115
+ sizeBytes: Buffer.byteLength(content, "utf8"),
1116
+ sha256: sha256(content),
1117
+ };
1118
+ }
1119
+ case "write_if_unchanged": {
1120
+ const path = required(input.path, "path");
1121
+ const content = requiredRaw(input.content, "content");
1122
+ const expectedSha256 = required(input.expectedSha256, "expectedSha256").toLowerCase();
1123
+ const current = await registry.readFile(workspace.id, path);
1124
+ const currentSha256 = sha256(current);
1125
+ if (currentSha256 !== expectedSha256) {
1126
+ return {
1127
+ path,
1128
+ written: false,
1129
+ currentSha256,
1130
+ expectedSha256,
1131
+ conflict: true,
1132
+ };
1133
+ }
1134
+ await registry.writeFile(workspace.id, path, content);
1135
+ return {
1136
+ path,
1137
+ written: true,
1138
+ previousSha256: currentSha256,
1139
+ sha256: sha256(content),
1140
+ conflict: false,
1141
+ };
1142
+ }
1143
+ case "edit": {
1144
+ return {
1145
+ replacements: await registry.editFile(workspace.id, required(input.path, "path"), requiredRaw(input.oldText, "oldText"), requiredRaw(input.newText, "newText")),
1146
+ };
1147
+ }
1148
+ case "mkdir": {
1149
+ const path = required(input.path, "path");
1150
+ await registry.createDirectory(workspace.id, path);
1151
+ return { path };
1152
+ }
1153
+ case "delete": {
1154
+ const path = required(input.path, "path");
1155
+ await registry.deletePath(workspace.id, path, Boolean(input.recursive));
1156
+ return { path };
1157
+ }
1158
+ case "move": {
1159
+ const fromPath = required(input.fromPath, "fromPath");
1160
+ const toPath = required(input.toPath, "toPath");
1161
+ await registry.movePath(workspace.id, fromPath, toPath);
1162
+ return { fromPath, toPath };
1163
+ }
1164
+ case "find_files": {
1165
+ assertPermission(workspace.exposedPath, "read");
1166
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1167
+ return {
1168
+ matches: splitSearchOutput(await findFiles({
1169
+ cwd,
1170
+ pattern: input.pattern ?? "**/*",
1171
+ maxResults: normalizeMaxResults(input.maxResults),
1172
+ })),
1173
+ };
1174
+ }
1175
+ case "search_text": {
1176
+ assertPermission(workspace.exposedPath, "read");
1177
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1178
+ return {
1179
+ matches: splitSearchOutput(await searchText({
1180
+ cwd,
1181
+ query: required(input.query, "query"),
1182
+ glob: input.glob,
1183
+ fixedStrings: input.fixedStrings ?? true,
1184
+ caseSensitive: input.caseSensitive ?? false,
1185
+ beforeContext: input.beforeContext,
1186
+ afterContext: input.afterContext,
1187
+ maxResults: normalizeMaxResults(input.maxResults),
1188
+ })),
1189
+ };
1190
+ }
1191
+ case "search_symbols": {
1192
+ assertPermission(workspace.exposedPath, "read");
1193
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1194
+ return {
1195
+ symbols: await searchSymbols({
1196
+ cwd,
1197
+ query: input.query,
1198
+ glob: input.glob,
1199
+ caseSensitive: input.caseSensitive ?? false,
1200
+ maxResults: normalizeMaxResults(input.maxResults),
1201
+ maxBytes: normalizeBoundedPositiveInteger(input.maxBytes, 256 * 1024, 1024 * 1024),
1202
+ }),
1203
+ };
1204
+ }
1205
+ default:
1206
+ throw new Error(`runFileSearchOperation cannot execute operation: ${input.operation}`);
1207
+ }
1208
+ }
1209
+ async function runCodexOperation(registry, workspace, input) {
1210
+ switch (input.operation) {
1211
+ case "codex_start": {
1212
+ assertPermission(workspace.exposedPath, "codex");
1213
+ const cwd = await registry.resolveExistingPath(workspace, input.workingDirectory ?? ".");
1214
+ const prompt = required(input.prompt, "prompt");
1215
+ const limits = managedCommandPolicyLimits(workspace, "codex exec -", input);
1216
+ return {
1217
+ process: startManagedProcess({
1218
+ kind: "codex",
1219
+ workspaceId: workspace.exposedPath.id,
1220
+ workspaceRoot: workspace.root,
1221
+ cwd,
1222
+ command: "codex",
1223
+ args: ["exec", "-"],
1224
+ commandPreview: `codex exec -: ${previewCommand(prompt)}`,
1225
+ timeoutMs: limits.timeoutMs,
1226
+ maxOutputBytes: limits.maxOutputBytes,
1227
+ stdin: prompt,
1228
+ }),
1229
+ };
1230
+ }
1231
+ case "codex": {
1232
+ assertPermission(workspace.exposedPath, "codex");
1233
+ const cwd = await registry.resolveExistingPath(workspace, input.workingDirectory ?? ".");
1234
+ const limits = commandPolicyLimits(workspace, "codex exec -", input, 1800);
1235
+ return runProcess("codex", ["exec", "-"], cwd, limits.timeoutMs, required(input.prompt, "prompt"), limits.maxOutputBytes);
1236
+ }
1237
+ case "codex_plan":
1238
+ case "codex_review":
1239
+ case "codex_fix":
1240
+ case "codex_test":
1241
+ case "codex_continue": {
1242
+ assertPermission(workspace.exposedPath, "codex");
1243
+ const cwd = await registry.resolveExistingPath(workspace, input.workingDirectory ?? ".");
1244
+ return codexWorkflow(workspace, cwd, input);
1245
+ }
1246
+ case "codex_runs": {
1247
+ assertPermission(workspace.exposedPath, "codex");
1248
+ return {
1249
+ runs: readCodexRunRecords({
1250
+ workspaceId: workspace.exposedPath.id,
1251
+ workflowId: input.workflowId,
1252
+ maxResults: input.maxResults,
1253
+ }),
1254
+ };
1255
+ }
1256
+ default:
1257
+ throw new Error(`runCodexOperation cannot execute operation: ${input.operation}`);
1258
+ }
1259
+ }
1260
+ async function runScreenOperation(_registry, workspace, input) {
1261
+ switch (input.operation) {
1262
+ case "screen_list": {
1263
+ assertPermission(workspace.exposedPath, "screen");
1264
+ return listScreenshotTargets();
1265
+ }
1266
+ case "screen_capture": {
1267
+ assertPermission(workspace.exposedPath, "screen");
1268
+ return captureScreenshot({
1269
+ source: "display",
1270
+ target: input.path,
1271
+ format: input.format,
1272
+ returnMode: input.returnMode,
1273
+ maxWidth: input.maxWidth,
1274
+ maxHeight: input.maxHeight,
1275
+ });
1276
+ }
1277
+ case "screen_capture_window": {
1278
+ assertPermission(workspace.exposedPath, "screen");
1279
+ return captureScreenshot({
1280
+ source: "window",
1281
+ target: required(input.path, "path"),
1282
+ format: input.format,
1283
+ returnMode: input.returnMode,
1284
+ maxWidth: input.maxWidth,
1285
+ maxHeight: input.maxHeight,
1286
+ });
1287
+ }
1288
+ case "screen_capture_process": {
1289
+ assertPermission(workspace.exposedPath, "screen");
1290
+ return captureScreenshot({
1291
+ source: "process",
1292
+ target: required(input.path, "path"),
1293
+ format: input.format,
1294
+ returnMode: input.returnMode,
1295
+ maxWidth: input.maxWidth,
1296
+ maxHeight: input.maxHeight,
1297
+ });
1298
+ }
1299
+ default:
1300
+ throw new Error(`runScreenOperation cannot execute operation: ${input.operation}`);
1301
+ }
1302
+ }
1303
+ async function dispatchWorkspaceOperation(registry, workspace, input) {
1304
+ switch (input.operation) {
1305
+ case "instructions": {
1306
+ return {
1307
+ files: await registry.instructions(workspace.id, input.path ?? ".", {
1308
+ maxBytes: input.maxBytes,
1309
+ }),
1310
+ };
1311
+ }
1312
+ case "agent_skills": {
1313
+ assertPermission(workspace.exposedPath, "read");
1314
+ return agentSkills(registry, workspace, {
1315
+ maxResults: input.maxResults,
1316
+ maxBytes: input.maxBytes,
1317
+ });
1318
+ }
1319
+ case "coding_context": {
1320
+ assertPermission(workspace.exposedPath, "read");
1321
+ return codingContext(registry, workspace, {
1322
+ path: input.path ?? ".",
1323
+ maxDepth: input.maxDepth,
1324
+ maxEntries: input.maxEntries,
1325
+ maxBytes: input.maxBytes,
1326
+ maxResults: input.maxResults,
1327
+ });
1328
+ }
1329
+ case "project_overview": {
1330
+ assertPermission(workspace.exposedPath, "read");
1331
+ return projectOverview(registry, workspace, {
1332
+ path: input.path ?? ".",
1333
+ maxDepth: input.maxDepth,
1334
+ maxEntries: input.maxEntries,
1335
+ });
1336
+ }
1337
+ case "change_summary": {
1338
+ assertPermission(workspace.exposedPath, "read");
1339
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1340
+ return changeSummary(cwd, { maxBytes: input.maxBytes });
1341
+ }
1342
+ case "repo_status": {
1343
+ assertPermission(workspace.exposedPath, "read");
1344
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1345
+ return repoStatus(cwd, {
1346
+ includeDiff: input.includeDiff ?? true,
1347
+ maxBytes: input.maxBytes,
1348
+ });
1349
+ }
1350
+ case "git_changes": {
1351
+ assertPermission(workspace.exposedPath, "read");
1352
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1353
+ return gitChanges(cwd);
1354
+ }
1355
+ case "git_diff": {
1356
+ assertPermission(workspace.exposedPath, "read");
1357
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1358
+ const paths = await validateGitPathspecs(registry, workspace, cwd, input.paths);
1359
+ return gitDiff(cwd, {
1360
+ paths: paths.map((path) => path.inputPath),
1361
+ pathspecs: paths.map((path) => path.gitPathspec),
1362
+ staged: input.staged ?? false,
1363
+ maxBytes: input.maxBytes,
1364
+ });
1365
+ }
1366
+ case "git_log": {
1367
+ assertPermission(workspace.exposedPath, "read");
1368
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1369
+ const paths = await validateGitPathspecs(registry, workspace, cwd, input.paths);
1370
+ return gitLog(cwd, {
1371
+ paths: paths.map((path) => path.inputPath),
1372
+ pathspecs: paths.map((path) => path.gitPathspec),
1373
+ maxResults: input.maxResults,
1374
+ });
1375
+ }
1376
+ case "git_show": {
1377
+ assertPermission(workspace.exposedPath, "read");
1378
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1379
+ const paths = await validateGitPathspecs(registry, workspace, cwd, input.paths);
1380
+ return gitShow(cwd, {
1381
+ ref: input.ref,
1382
+ paths: paths.map((path) => path.inputPath),
1383
+ pathspecs: paths.map((path) => path.gitPathspec),
1384
+ maxBytes: input.maxBytes,
1385
+ });
1386
+ }
1387
+ case "git_stage": {
1388
+ assertPermission(workspace.exposedPath, "write");
1389
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1390
+ const paths = requireGitPathspecs(await validateGitPathspecs(registry, workspace, cwd, input.paths));
1391
+ return gitIndexUpdate(cwd, {
1392
+ action: "stage",
1393
+ commandArgs: ["add", "--"],
1394
+ paths: paths.map((path) => path.inputPath),
1395
+ pathspecs: paths.map((path) => path.gitPathspec),
1396
+ });
1397
+ }
1398
+ case "git_unstage": {
1399
+ assertPermission(workspace.exposedPath, "write");
1400
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1401
+ const paths = requireGitPathspecs(await validateGitPathspecs(registry, workspace, cwd, input.paths));
1402
+ return gitIndexUpdate(cwd, {
1403
+ action: "unstage",
1404
+ commandArgs: ["restore", "--staged", "--"],
1405
+ paths: paths.map((path) => path.inputPath),
1406
+ pathspecs: paths.map((path) => path.gitPathspec),
1407
+ });
1408
+ }
1409
+ case "git_commit": {
1410
+ assertPermission(workspace.exposedPath, "write");
1411
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1412
+ return gitCommit(workspace, cwd, required(input.message, "message"));
1413
+ }
1414
+ case "git_worktree_list": {
1415
+ assertPermission(workspace.exposedPath, "read");
1416
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1417
+ return gitWorktreeList(cwd);
1418
+ }
1419
+ case "git_worktree_create": {
1420
+ assertPermission(workspace.exposedPath, "write");
1421
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1422
+ const target = await registry.resolveWritablePath(workspace, required(input.toPath, "toPath"));
1423
+ return gitWorktreeCreate(cwd, target, {
1424
+ branch: input.branch,
1425
+ startPoint: input.startPoint,
1426
+ targetPath: formatWorkspacePath(target, workspace),
1427
+ });
1428
+ }
1429
+ case "patch": {
1430
+ assertPermission(workspace.exposedPath, "write");
1431
+ const patch = requiredRaw(input.patch, "patch");
1432
+ await validatePatchPaths(registry, workspace, patch);
1433
+ const check = await runProcess("git", ["apply", "--check", "-"], workspace.root, 30_000, patch);
1434
+ if (check.exitCode !== 0) {
1435
+ return { applied: false, check };
1436
+ }
1437
+ const apply = await runProcess("git", ["apply", "-"], workspace.root, 30_000, patch);
1438
+ return { applied: apply.exitCode === 0, check, apply };
1439
+ }
1440
+ case "package_run": {
1441
+ assertPermission(workspace.exposedPath, "shell");
1442
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1443
+ return packageRun(registry, workspace, cwd, {
1444
+ script: required(input.script, "script"),
1445
+ scriptArgs: input.scriptArgs,
1446
+ timeoutSeconds: input.timeoutSeconds,
1447
+ maxOutputBytes: input.maxOutputBytes,
1448
+ });
1449
+ }
1450
+ case "package_start": {
1451
+ assertPermission(workspace.exposedPath, "shell");
1452
+ const cwd = await registry.resolveExistingPath(workspace, input.path ?? ".");
1453
+ return packageStart(registry, workspace, cwd, {
1454
+ script: required(input.script, "script"),
1455
+ scriptArgs: input.scriptArgs,
1456
+ timeoutSeconds: input.timeoutSeconds,
1457
+ maxOutputBytes: input.maxOutputBytes,
1458
+ });
1459
+ }
1460
+ case "command": {
1461
+ assertPermission(workspace.exposedPath, "shell");
1462
+ const cwd = await registry.resolveExistingPath(workspace, input.workingDirectory ?? ".");
1463
+ const command = required(input.command, "command");
1464
+ const limits = commandPolicyLimits(workspace, command, input, 120);
1465
+ const shell = shellCommand(command);
1466
+ return runProcess(shell.command, shell.args, cwd, limits.timeoutMs, undefined, limits.maxOutputBytes);
1467
+ }
1468
+ case "process_start": {
1469
+ assertPermission(workspace.exposedPath, "shell");
1470
+ const cwd = await registry.resolveExistingPath(workspace, input.workingDirectory ?? ".");
1471
+ const command = required(input.command, "command");
1472
+ const limits = managedCommandPolicyLimits(workspace, command, input);
1473
+ return {
1474
+ process: startManagedProcess({
1475
+ kind: "shell",
1476
+ workspaceId: workspace.exposedPath.id,
1477
+ workspaceRoot: workspace.root,
1478
+ cwd,
1479
+ command,
1480
+ commandPreview: previewCommand(command),
1481
+ timeoutMs: limits.timeoutMs,
1482
+ maxOutputBytes: limits.maxOutputBytes,
1483
+ }),
1484
+ };
1485
+ }
1486
+ case "process_list": {
1487
+ const kinds = allowedProcessKinds(workspace);
1488
+ return {
1489
+ processes: listManagedProcesses({
1490
+ workspaceId: workspace.exposedPath.id,
1491
+ workspaceRoot: workspace.root,
1492
+ kinds,
1493
+ }),
1494
+ };
1495
+ }
1496
+ case "process_read": {
1497
+ const kinds = allowedProcessKinds(workspace);
1498
+ return {
1499
+ process: readManagedProcess({
1500
+ processId: required(input.processId, "processId"),
1501
+ workspaceId: workspace.exposedPath.id,
1502
+ workspaceRoot: workspace.root,
1503
+ kinds,
1504
+ }),
1505
+ };
1506
+ }
1507
+ case "process_stop": {
1508
+ const kinds = allowedProcessKinds(workspace);
1509
+ return {
1510
+ process: await stopManagedProcess({
1511
+ processId: required(input.processId, "processId"),
1512
+ workspaceId: workspace.exposedPath.id,
1513
+ workspaceRoot: workspace.root,
1514
+ signal: input.signal,
1515
+ kinds,
1516
+ }),
1517
+ };
1518
+ }
1519
+ case "batch": {
1520
+ const results = [];
1521
+ for (const [index, operation] of requiredOperations(input.operations).entries()) {
1522
+ if (operation.operation === "batch") {
1523
+ writeBatchItemAudit(workspace, index, operation, false, 0, "nested batch operations are not supported");
1524
+ const result = {
1525
+ index,
1526
+ operation: operation.operation,
1527
+ ok: false,
1528
+ error: "nested batch operations are not supported",
1529
+ };
1530
+ results.push(result);
1531
+ if (!input.continueOnError)
1532
+ break;
1533
+ continue;
1534
+ }
1535
+ const startedAt = performance.now();
1536
+ try {
1537
+ const data = await runWorkspaceOperation(registry, workspace, operation);
1538
+ writeBatchItemAudit(workspace, index, operation, true, Math.round(performance.now() - startedAt));
1539
+ results.push({
1540
+ index,
1541
+ operation: operation.operation,
1542
+ ok: true,
1543
+ data,
1544
+ });
1545
+ }
1546
+ catch (error) {
1547
+ writeBatchItemAudit(workspace, index, operation, false, Math.round(performance.now() - startedAt), errorMessage(error));
1548
+ results.push({
1549
+ index,
1550
+ operation: operation.operation,
1551
+ ok: false,
1552
+ error: errorMessage(error),
1553
+ });
1554
+ if (!input.continueOnError)
1555
+ break;
1556
+ }
1557
+ }
1558
+ return {
1559
+ results,
1560
+ completed: results.every((result) => result.ok),
1561
+ };
1562
+ }
1563
+ }
1564
+ }
1565
+ export function workspaceOperationAuditFields(input) {
1566
+ return {
1567
+ operation: input.operation,
1568
+ target: auditTarget(input),
1569
+ path: input.path ?? input.fromPath,
1570
+ workingDirectory: input.workingDirectory,
1571
+ commandPreview: input.command ? previewCommand(input.command) : input.prompt ? previewCommand(input.prompt) : undefined,
1572
+ replay: workspaceOperationReplayTemplate(input),
1573
+ detail: input.operation === "process_read" || input.operation === "process_stop"
1574
+ ? input.processId
1575
+ : input.operation === "move"
1576
+ ? input.toPath
1577
+ : input.operation === "git_worktree_create"
1578
+ ? input.toPath
1579
+ : input.operation === "git_stage" || input.operation === "git_unstage"
1580
+ ? input.paths?.join(",")
1581
+ : input.operation === "git_commit"
1582
+ ? input.message ? previewCommand(input.message) : "message"
1583
+ : input.operation === "write_if_unchanged"
1584
+ ? "expectedSha256"
1585
+ : input.operation === "find_files"
1586
+ ? input.pattern
1587
+ : input.operation === "search_text"
1588
+ ? input.glob ?? input.query
1589
+ : input.operation === "search_symbols"
1590
+ ? input.glob ?? input.query
1591
+ : input.operation === "package_run" || input.operation === "package_start"
1592
+ ? input.script
1593
+ : input.operation === "batch"
1594
+ ? input.operations?.map((operation) => operation.operation).join(",")
1595
+ : input.operation === "codex_runs"
1596
+ ? input.workflowId
1597
+ : input.operation === "screen_capture" || input.operation === "screen_capture_window" || input.operation === "screen_capture_process"
1598
+ ? input.path
1599
+ : input.operation,
1600
+ };
1601
+ }
1602
+ function workspaceOperationReplayTemplate(input) {
1603
+ const target = auditTarget(input);
1604
+ const baseInput = {
1605
+ op: input.operation,
1606
+ target,
1607
+ input: {},
1608
+ options: {},
1609
+ };
1610
+ switch (input.operation) {
1611
+ case "stat":
1612
+ case "list":
1613
+ case "list_details":
1614
+ case "instructions":
1615
+ case "change_summary":
1616
+ case "repo_status":
1617
+ case "git_changes":
1618
+ case "git_worktree_list":
1619
+ case "read":
1620
+ return replayableTemplate(baseInput, {
1621
+ input: {
1622
+ path: input.path,
1623
+ startLine: input.startLine,
1624
+ lineCount: input.lineCount,
1625
+ includeDiff: input.includeDiff,
1626
+ maxBytes: input.maxBytes,
1627
+ },
1628
+ });
1629
+ case "read_many":
1630
+ return replayableTemplate(baseInput, {
1631
+ input: {
1632
+ paths: input.paths,
1633
+ maxBytes: input.maxBytes,
1634
+ },
1635
+ });
1636
+ case "tree":
1637
+ case "coding_context":
1638
+ case "project_overview":
1639
+ return replayableTemplate(baseInput, {
1640
+ input: {
1641
+ path: input.path,
1642
+ maxDepth: input.maxDepth,
1643
+ maxEntries: input.maxEntries,
1644
+ includeFiles: input.includeFiles,
1645
+ maxBytes: input.maxBytes,
1646
+ maxResults: input.maxResults,
1647
+ },
1648
+ });
1649
+ case "history":
1650
+ case "history_insight":
1651
+ return replayableTemplate(baseInput, {
1652
+ input: {
1653
+ view: input.view,
1654
+ query: input.query,
1655
+ maxResults: input.maxResults,
1656
+ },
1657
+ });
1658
+ case "write":
1659
+ case "create_file":
1660
+ return replayableTemplate(baseInput, {
1661
+ input: {
1662
+ path: input.path,
1663
+ },
1664
+ }, {
1665
+ replayable: false,
1666
+ reason: "File write contents are not stored in the audit log; provide content before replaying.",
1667
+ requiresInput: ["content"],
1668
+ });
1669
+ case "write_if_unchanged":
1670
+ return replayableTemplate(baseInput, {
1671
+ input: {
1672
+ path: input.path,
1673
+ expectedSha256: input.expectedSha256,
1674
+ },
1675
+ }, {
1676
+ replayable: false,
1677
+ reason: "File write contents are not stored in the audit log; provide content before replaying.",
1678
+ requiresInput: ["content"],
1679
+ });
1680
+ case "edit":
1681
+ return replayableTemplate(baseInput, {
1682
+ input: {
1683
+ path: input.path,
1684
+ },
1685
+ }, {
1686
+ replayable: false,
1687
+ reason: "Edit replacement text is not stored in the audit log; provide oldText and newText before replaying.",
1688
+ requiresInput: ["oldText", "newText"],
1689
+ });
1690
+ case "patch":
1691
+ return replayableTemplate(baseInput, {
1692
+ input: {},
1693
+ }, {
1694
+ replayable: false,
1695
+ reason: "Patch bodies are not stored in the audit log; provide patch before replaying.",
1696
+ requiresInput: ["patch"],
1697
+ });
1698
+ case "mkdir":
1699
+ case "delete":
1700
+ return replayableTemplate(baseInput, {
1701
+ input: {
1702
+ path: input.path,
1703
+ recursive: input.recursive,
1704
+ },
1705
+ });
1706
+ case "move":
1707
+ return replayableTemplate(baseInput, {
1708
+ input: {
1709
+ fromPath: input.fromPath,
1710
+ toPath: input.toPath,
1711
+ },
1712
+ });
1713
+ case "find_files":
1714
+ return replayableTemplate(baseInput, {
1715
+ input: {
1716
+ pattern: input.pattern,
1717
+ maxResults: input.maxResults,
1718
+ },
1719
+ });
1720
+ case "search_text":
1721
+ case "search_symbols":
1722
+ return replayableTemplate(baseInput, {
1723
+ input: {
1724
+ query: input.query,
1725
+ glob: input.glob,
1726
+ fixedStrings: input.fixedStrings,
1727
+ caseSensitive: input.caseSensitive,
1728
+ beforeContext: input.beforeContext,
1729
+ afterContext: input.afterContext,
1730
+ maxResults: input.maxResults,
1731
+ },
1732
+ });
1733
+ case "package_run":
1734
+ case "package_start":
1735
+ return replayableTemplate(baseInput, {
1736
+ input: {
1737
+ script: input.script,
1738
+ scriptArgs: input.scriptArgs,
1739
+ workingDirectory: input.workingDirectory,
1740
+ timeoutSeconds: input.timeoutSeconds,
1741
+ maxBytes: input.maxBytes,
1742
+ },
1743
+ });
1744
+ case "process_read":
1745
+ case "process_stop":
1746
+ return replayableTemplate(baseInput, {
1747
+ input: {
1748
+ processId: input.processId,
1749
+ signal: input.signal,
1750
+ maxBytes: input.maxBytes,
1751
+ },
1752
+ });
1753
+ case "git_diff":
1754
+ case "git_log":
1755
+ case "git_show":
1756
+ return replayableTemplate(baseInput, {
1757
+ input: {
1758
+ path: input.path,
1759
+ paths: input.paths,
1760
+ ref: input.ref,
1761
+ staged: input.staged,
1762
+ maxResults: input.maxResults,
1763
+ maxBytes: input.maxBytes,
1764
+ },
1765
+ });
1766
+ case "git_stage":
1767
+ case "git_unstage":
1768
+ return replayableTemplate(baseInput, {
1769
+ input: {
1770
+ paths: input.paths,
1771
+ },
1772
+ });
1773
+ case "git_commit":
1774
+ return replayableTemplate(baseInput, {
1775
+ input: {
1776
+ message: input.message ? previewCommand(input.message) : undefined,
1777
+ },
1778
+ }, {
1779
+ replayable: false,
1780
+ reason: "Git commit messages are stored only as a preview; provide the full message before replaying.",
1781
+ requiresInput: ["message"],
1782
+ });
1783
+ case "git_worktree_create":
1784
+ return replayableTemplate(baseInput, {
1785
+ input: {
1786
+ toPath: input.toPath,
1787
+ branch: input.branch,
1788
+ startPoint: input.startPoint,
1789
+ },
1790
+ });
1791
+ case "process_list":
1792
+ return replayableTemplate(baseInput, {
1793
+ input: {},
1794
+ });
1795
+ case "command":
1796
+ case "process_start":
1797
+ return replayableTemplate(baseInput, {
1798
+ input: {
1799
+ workingDirectory: input.workingDirectory,
1800
+ timeoutSeconds: input.timeoutSeconds,
1801
+ maxBytes: input.maxBytes,
1802
+ },
1803
+ }, {
1804
+ replayable: false,
1805
+ reason: "Raw shell commands are not stored in the audit log; provide command before replaying.",
1806
+ requiresInput: ["command"],
1807
+ });
1808
+ case "codex":
1809
+ case "codex_start":
1810
+ case "codex_plan":
1811
+ case "codex_review":
1812
+ case "codex_fix":
1813
+ case "codex_test":
1814
+ case "codex_continue":
1815
+ return replayableTemplate(baseInput, {
1816
+ input: {
1817
+ workflowId: input.workflowId,
1818
+ script: input.script,
1819
+ workingDirectory: input.workingDirectory,
1820
+ timeoutSeconds: input.timeoutSeconds,
1821
+ maxBytes: input.maxBytes,
1822
+ },
1823
+ }, {
1824
+ replayable: false,
1825
+ reason: "Codex prompts are not stored in the audit log; provide prompt before replaying.",
1826
+ requiresInput: ["prompt"],
1827
+ });
1828
+ case "codex_runs":
1829
+ return replayableTemplate(baseInput, {
1830
+ input: {
1831
+ workflowId: input.workflowId,
1832
+ maxResults: input.maxResults,
1833
+ },
1834
+ });
1835
+ case "screen_list":
1836
+ return replayableTemplate(baseInput, {
1837
+ input: {},
1838
+ });
1839
+ case "screen_capture":
1840
+ case "screen_capture_window":
1841
+ case "screen_capture_process":
1842
+ return replayableTemplate(baseInput, {
1843
+ input: {
1844
+ path: input.path,
1845
+ format: input.format,
1846
+ returnMode: input.returnMode,
1847
+ maxWidth: input.maxWidth,
1848
+ maxHeight: input.maxHeight,
1849
+ },
1850
+ }, {
1851
+ replayable: false,
1852
+ reason: "Screenshot captures can expose current screen pixels; request a fresh explicit capture instead of replaying from history.",
1853
+ requiresInput: ["screen-capture-confirmation"],
1854
+ });
1855
+ case "batch":
1856
+ return replayableTemplate(baseInput, {
1857
+ input: {},
1858
+ }, {
1859
+ replayable: false,
1860
+ reason: "Batch child operation payloads are not stored in the audit log; provide operations before replaying.",
1861
+ requiresInput: ["operations"],
1862
+ });
1863
+ default:
1864
+ return replayableTemplate(baseInput, {
1865
+ input: {},
1866
+ });
1867
+ }
1868
+ }
1869
+ function replayableTemplate(baseInput, values, metadata = { replayable: true }) {
1870
+ return {
1871
+ action: "workspace_operation",
1872
+ replayable: metadata.replayable,
1873
+ reason: metadata.reason,
1874
+ requiresInput: metadata.requiresInput,
1875
+ input: {
1876
+ op: baseInput.op,
1877
+ target: baseInput.target,
1878
+ input: cleanReplayObject(values.input ?? {}),
1879
+ options: cleanReplayObject(values.options ?? {}),
1880
+ },
1881
+ };
1882
+ }
1883
+ function cleanReplayObject(input) {
1884
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
1885
+ }
1886
+ function writeBatchItemAudit(workspace, index, operation, success, durationMs, error) {
1887
+ writeAuditEvent({
1888
+ type: "tool_call",
1889
+ tool: "workspace_operation.batch_item",
1890
+ success,
1891
+ durationMs,
1892
+ workspaceId: workspace.exposedPath.id,
1893
+ workspaceRoot: workspace.root,
1894
+ ...workspaceOperationAuditFields(operation),
1895
+ detail: `batch[${index}]: ${operation.operation}`,
1896
+ error,
1897
+ });
1898
+ }
1899
+ function required(value, name) {
1900
+ const text = value?.trim();
1901
+ if (!text)
1902
+ throw new Error(`${name} is required for this operation`);
1903
+ return text;
1904
+ }
1905
+ function requiredRaw(value, name) {
1906
+ if (!value)
1907
+ throw new Error(`${name} is required for this operation`);
1908
+ return value;
1909
+ }
1910
+ function requiredPaths(value) {
1911
+ if (!value || value.length === 0) {
1912
+ throw new Error("paths is required for this operation");
1913
+ }
1914
+ if (value.length > 100) {
1915
+ throw new Error("paths supports at most 100 files per call");
1916
+ }
1917
+ return value.map((path) => required(path, "paths[]"));
1918
+ }
1919
+ function requiredOperations(value) {
1920
+ if (!value || value.length === 0) {
1921
+ throw new Error("operations is required for this operation");
1922
+ }
1923
+ if (value.length > 25) {
1924
+ throw new Error("batch supports at most 25 operations per call");
1925
+ }
1926
+ for (const [index, operation] of value.entries()) {
1927
+ if (!workspaceOperationNames.includes(operation.operation)) {
1928
+ throw new Error(`operations[${index}].operation must be one of: ${workspaceOperationNames.join(", ")}`);
1929
+ }
1930
+ }
1931
+ return value;
1932
+ }
1933
+ function readResult(content, options) {
1934
+ const sizeBytes = Buffer.byteLength(content, "utf8");
1935
+ const startLine = normalizeOptionalPositiveInteger(options.startLine);
1936
+ const lineCount = normalizeOptionalPositiveInteger(options.lineCount);
1937
+ const maxBytes = options.maxBytes === undefined ? undefined : normalizeBoundedPositiveInteger(options.maxBytes, sizeBytes, 256 * 1024);
1938
+ let selected = content;
1939
+ let endLine;
1940
+ let totalLines;
1941
+ if (startLine || lineCount) {
1942
+ const lines = content.split(/\r?\n/);
1943
+ totalLines = lines.length;
1944
+ const startIndex = Math.max(0, (startLine ?? 1) - 1);
1945
+ const endIndex = lineCount ? startIndex + lineCount : lines.length;
1946
+ selected = lines.slice(startIndex, endIndex).join("\n");
1947
+ endLine = Math.min(lines.length, endIndex);
1948
+ }
1949
+ const truncatedContent = maxBytes === undefined ? selected : truncateText(selected, maxBytes);
1950
+ return {
1951
+ content: truncatedContent,
1952
+ sizeBytes,
1953
+ sha256: sha256(content),
1954
+ truncated: truncatedContent !== selected,
1955
+ startLine: startLine ?? undefined,
1956
+ endLine,
1957
+ totalLines,
1958
+ };
1959
+ }
1960
+ function sha256(value) {
1961
+ return createHash("sha256").update(value, "utf8").digest("hex");
1962
+ }
1963
+ function normalizeOptionalPositiveInteger(value) {
1964
+ return Number.isInteger(value) && value && value > 0 ? value : undefined;
1965
+ }
1966
+ function normalizeMaxResults(value) {
1967
+ return Number.isInteger(value) && value && value > 0 ? Math.min(value, 1000) : 200;
1968
+ }
1969
+ function normalizeTimeoutMs(value, defaultSeconds) {
1970
+ const seconds = Number.isInteger(value) && value && value > 0 ? Math.min(value, 3600) : defaultSeconds;
1971
+ return seconds * 1000;
1972
+ }
1973
+ function commandPolicyLimits(workspace, command, input, defaultTimeoutSeconds) {
1974
+ assertCommandAllowedByPolicy(workspace.exposedPath.policy, command);
1975
+ return {
1976
+ timeoutMs: normalizeTimeoutMs(commandPolicyTimeoutSeconds(workspace.exposedPath.policy, input.timeoutSeconds, defaultTimeoutSeconds), defaultTimeoutSeconds),
1977
+ maxOutputBytes: commandPolicyOutputBytes(workspace.exposedPath.policy, input.maxOutputBytes),
1978
+ };
1979
+ }
1980
+ function managedCommandPolicyLimits(workspace, command, input) {
1981
+ assertCommandAllowedByPolicy(workspace.exposedPath.policy, command);
1982
+ return {
1983
+ timeoutMs: managedCommandPolicyTimeoutMs(workspace.exposedPath.policy, input.timeoutSeconds),
1984
+ maxOutputBytes: commandPolicyOutputBytes(workspace.exposedPath.policy, input.maxOutputBytes),
1985
+ };
1986
+ }
1987
+ function commandPolicyTimeoutSeconds(policy, requestedSeconds, defaultSeconds) {
1988
+ const requested = requestedSeconds ?? defaultSeconds;
1989
+ return policy?.maxRuntimeSeconds ? Math.min(requested, policy.maxRuntimeSeconds) : requested;
1990
+ }
1991
+ function managedCommandPolicyTimeoutMs(policy, requestedSeconds) {
1992
+ if (requestedSeconds === undefined && !policy?.maxRuntimeSeconds)
1993
+ return undefined;
1994
+ const seconds = policy?.maxRuntimeSeconds
1995
+ ? Math.min(requestedSeconds ?? policy.maxRuntimeSeconds, policy.maxRuntimeSeconds)
1996
+ : requestedSeconds;
1997
+ return normalizeTimeoutMs(seconds, 3600);
1998
+ }
1999
+ function commandPolicyOutputBytes(policy, requestedBytes) {
2000
+ const policyMax = normalizeBoundedPositiveInteger(policy?.maxOutputBytes, DEFAULT_COMMAND_OUTPUT_BYTES, MAX_COMMAND_OUTPUT_BYTES);
2001
+ const capped = requestedBytes === undefined ? policyMax : Math.min(requestedBytes, policyMax);
2002
+ return normalizeBoundedPositiveInteger(capped, policyMax, policyMax);
2003
+ }
2004
+ function assertCommandAllowedByPolicy(policy, command) {
2005
+ if (!policy)
2006
+ return;
2007
+ const deniedPattern = policy.deniedCommands?.find((pattern) => commandPolicyPatternMatches(pattern, command));
2008
+ if (deniedPattern) {
2009
+ throw new Error(`Command permission denied by workspace policy (${deniedPattern}): ${previewCommand(command)}`);
2010
+ }
2011
+ if (policy.allowedCommands?.length && !policy.allowedCommands.some((pattern) => commandPolicyPatternMatches(pattern, command))) {
2012
+ throw new Error(`Command permission denied by workspace policy: ${previewCommand(command)}`);
2013
+ }
2014
+ }
2015
+ function commandPolicyPatternMatches(pattern, command) {
2016
+ const normalizedPattern = normalizeCommandPolicyText(pattern);
2017
+ if (!normalizedPattern)
2018
+ return false;
2019
+ const normalizedCommand = normalizeCommandPolicyText(command);
2020
+ const source = normalizedPattern
2021
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
2022
+ .replace(/\*/g, ".*")
2023
+ .replace(/\?/g, ".");
2024
+ return new RegExp(`^${source}$`, process.platform === "win32" ? "i" : "").test(normalizedCommand);
2025
+ }
2026
+ function normalizeCommandPolicyText(value) {
2027
+ return value.trim().replace(/\s+/g, " ");
2028
+ }
2029
+ function workspaceHistory(workspace, options) {
2030
+ const events = readAuditEvents({
2031
+ query: options.query,
2032
+ }).filter((event) => (event.workspaceId === workspace.id ||
2033
+ event.workspaceId === workspace.exposedPath.id ||
2034
+ event.workspaceRef === workspace.exposedPath.id ||
2035
+ event.workspaceRef === workspace.exposedPath.name ||
2036
+ event.workspaceRef === workspace.exposedPath.path ||
2037
+ event.workspaceRoot === workspace.root));
2038
+ return events.slice(0, normalizeMaxResults(options.maxResults));
2039
+ }
2040
+ function workspaceHistoryInsight(workspace, options) {
2041
+ const events = workspaceHistory(workspace, {
2042
+ maxResults: options.maxResults,
2043
+ query: options.query,
2044
+ });
2045
+ return historyInsightFromEvents(events, {
2046
+ view: options.view,
2047
+ limit: normalizeMaxResults(options.maxResults),
2048
+ query: options.query,
2049
+ workspaceId: workspace.exposedPath.id,
2050
+ });
2051
+ }
2052
+ async function codexWorkflow(workspace, cwd, input) {
2053
+ if (!isCodexWorkflowOperation(input.operation)) {
2054
+ throw new Error(`Unsupported Codex workflow: ${input.operation}`);
2055
+ }
2056
+ const maxBytes = normalizeBoundedPositiveInteger(input.maxBytes, 64 * 1024, 256 * 1024);
2057
+ const preRunChangeSummary = await changeSummary(cwd, { maxBytes });
2058
+ const history = input.operation === "codex_continue"
2059
+ ? historyInsightFromEvents(workspaceHistory(workspace, { maxResults: input.maxResults ?? 50 }), {
2060
+ view: "debug_bundle",
2061
+ limit: normalizeMaxResults(input.maxResults ?? 50),
2062
+ workspaceId: workspace.exposedPath.id,
2063
+ })
2064
+ : undefined;
2065
+ const workflowId = input.workflowId ?? codexWorkflowId(input.operation, workspace.exposedPath.id, input.prompt);
2066
+ const workflowPrompt = buildCodexWorkflowPrompt(input.operation, {
2067
+ userPrompt: codexWorkflowUserPrompt(input),
2068
+ script: input.script,
2069
+ workflowId,
2070
+ workspace,
2071
+ workingDirectory: formatWorkspacePath(cwd, workspace),
2072
+ preRunChangeSummary,
2073
+ history,
2074
+ });
2075
+ const limits = commandPolicyLimits(workspace, "codex exec -", input, 1800);
2076
+ const result = await runProcess("codex", ["exec", "-"], cwd, limits.timeoutMs, workflowPrompt, limits.maxOutputBytes);
2077
+ const postRunChangeSummary = await changeSummary(cwd, { maxBytes });
2078
+ const workflow = {
2079
+ id: workflowId,
2080
+ type: input.operation,
2081
+ workspaceId: workspace.exposedPath.id,
2082
+ workingDirectory: formatWorkspacePath(cwd, workspace),
2083
+ promptPreview: previewCommand(workflowPrompt),
2084
+ userPromptPreview: input.prompt ? previewCommand(input.prompt) : undefined,
2085
+ continuedFromWorkflowId: input.operation === "codex_continue" ? input.workflowId : undefined,
2086
+ };
2087
+ const runRecord = writeCodexRunRecord({
2088
+ workflowId,
2089
+ workflowType: input.operation,
2090
+ workspaceId: workspace.exposedPath.id,
2091
+ workspaceRoot: workspace.root,
2092
+ workingDirectory: workflow.workingDirectory,
2093
+ continuedFromWorkflowId: workflow.continuedFromWorkflowId,
2094
+ promptPreview: workflow.promptPreview,
2095
+ userPromptPreview: workflow.userPromptPreview,
2096
+ result,
2097
+ preRunChangeSummary,
2098
+ postRunChangeSummary,
2099
+ historyInsight: history,
2100
+ maxPreviewBytes: maxBytes,
2101
+ });
2102
+ return {
2103
+ workflow,
2104
+ result,
2105
+ preRunChangeSummary,
2106
+ postRunChangeSummary,
2107
+ historyInsight: history,
2108
+ runRecord,
2109
+ };
2110
+ }
2111
+ function isCodexWorkflowOperation(operation) {
2112
+ return operation === "codex_plan" ||
2113
+ operation === "codex_review" ||
2114
+ operation === "codex_fix" ||
2115
+ operation === "codex_test" ||
2116
+ operation === "codex_continue";
2117
+ }
2118
+ function codexWorkflowUserPrompt(input) {
2119
+ if (input.operation === "codex_plan" || input.operation === "codex_fix") {
2120
+ return required(input.prompt, "prompt");
2121
+ }
2122
+ if (input.operation === "codex_review") {
2123
+ return input.prompt ?? "Review the current workspace changes for bugs, regressions, security risks, and missing tests.";
2124
+ }
2125
+ if (input.operation === "codex_test") {
2126
+ return input.prompt ?? (input.script ? `Run or inspect the package script named ${input.script} and summarize failures.` : "Run or inspect the appropriate project tests and summarize failures.");
2127
+ }
2128
+ return input.prompt ?? "Continue from the recent Computer Linker history, resolve the latest failure if one exists, and summarize the next concrete action.";
2129
+ }
2130
+ function buildCodexWorkflowPrompt(operation, context) {
2131
+ return [
2132
+ `Computer Linker Codex workflow: ${operation}`,
2133
+ `Workflow id: ${context.workflowId}`,
2134
+ `Workspace: ${context.workspace.exposedPath.id} (${context.workspace.exposedPath.name})`,
2135
+ `Working directory: ${context.workingDirectory}`,
2136
+ "",
2137
+ "User request:",
2138
+ context.userPrompt,
2139
+ "",
2140
+ "Current change summary:",
2141
+ serializePromptContext(context.preRunChangeSummary),
2142
+ context.history ? ["", "Recent Computer Linker history/debug bundle:", serializePromptContext(context.history)].join("\n") : "",
2143
+ "",
2144
+ codexWorkflowInstructions(operation, context.script),
2145
+ ].filter(Boolean).join("\n");
2146
+ }
2147
+ function codexWorkflowInstructions(operation, script) {
2148
+ switch (operation) {
2149
+ case "codex_plan":
2150
+ return [
2151
+ "Instructions:",
2152
+ "- Produce a concrete implementation plan.",
2153
+ "- Do not edit files unless the user explicitly asked for implementation inside the request.",
2154
+ "- Identify affected files, risks, and verification commands.",
2155
+ ].join("\n");
2156
+ case "codex_review":
2157
+ return [
2158
+ "Instructions:",
2159
+ "- Review as a code reviewer.",
2160
+ "- Lead with bugs, regressions, security risks, and missing tests.",
2161
+ "- Include file references where possible and avoid broad style commentary.",
2162
+ ].join("\n");
2163
+ case "codex_fix":
2164
+ return [
2165
+ "Instructions:",
2166
+ "- Implement the requested fix in this workspace.",
2167
+ "- Keep edits scoped and preserve existing behavior unless the request requires a change.",
2168
+ "- Run or recommend the relevant verification and summarize the resulting diff.",
2169
+ ].join("\n");
2170
+ case "codex_test":
2171
+ return [
2172
+ "Instructions:",
2173
+ script ? `- Prefer package script: ${script}.` : "- Select the most relevant test command from project metadata.",
2174
+ "- Run or inspect tests, capture failures, and propose the smallest next fix.",
2175
+ "- If tests cannot run, explain the blocker and the exact command that should be run.",
2176
+ ].join("\n");
2177
+ case "codex_continue":
2178
+ return [
2179
+ "Instructions:",
2180
+ "- Continue from the supplied recent Computer Linker history.",
2181
+ "- If a failed replay template is present, use it to understand the failed operation before acting.",
2182
+ "- Summarize what was continued, what changed, and what remains.",
2183
+ ].join("\n");
2184
+ }
2185
+ }
2186
+ function codexWorkflowId(operation, workspaceId, prompt) {
2187
+ const digest = createHash("sha256")
2188
+ .update(`${operation}\n${workspaceId}\n${prompt ?? ""}\n${Date.now()}`)
2189
+ .digest("hex")
2190
+ .slice(0, 12);
2191
+ return `${operation}_${digest}`;
2192
+ }
2193
+ function serializePromptContext(value) {
2194
+ return truncateText(JSON.stringify(value, null, 2), 64 * 1024);
2195
+ }
2196
+ function auditTarget(input) {
2197
+ if (input.operation === "process_read" || input.operation === "process_stop")
2198
+ return input.processId;
2199
+ if (input.operation === "codex_runs")
2200
+ return input.workflowId;
2201
+ if (input.operation === "explain_operation")
2202
+ return input.operationName;
2203
+ if (input.operation === "command" || input.operation === "process_start" || isCodexExecutionOperation(input.operation))
2204
+ return input.workingDirectory ?? ".";
2205
+ if (input.operation === "git_worktree_create")
2206
+ return input.toPath;
2207
+ if (input.operation === "move")
2208
+ return input.toPath;
2209
+ if (input.operation === "git_stage" || input.operation === "git_unstage")
2210
+ return input.paths?.join(",");
2211
+ return input.path ?? input.fromPath;
2212
+ }
2213
+ async function projectOverview(registry, workspace, options) {
2214
+ const target = await registry.resolveExistingPath(workspace, options.path);
2215
+ const targetInfo = await stat(target);
2216
+ const targetDirectory = targetInfo.isDirectory() ? target : dirname(target);
2217
+ const targetWorkspacePath = formatWorkspacePath(targetDirectory, workspace);
2218
+ const projectRoot = await nearestProjectRoot(registry, workspace, targetDirectory) ?? targetDirectory;
2219
+ const projectRootPath = formatWorkspacePath(projectRoot, workspace);
2220
+ const packageJson = await readPackageJson(registry, workspace, projectRoot);
2221
+ const treeEntries = await registry.tree(workspace.id, targetWorkspacePath, {
2222
+ maxDepth: options.maxDepth ?? 3,
2223
+ maxEntries: options.maxEntries ?? 300,
2224
+ includeFiles: true,
2225
+ });
2226
+ const lockfiles = await existingWorkspaceFiles(registry, workspace, projectRoot, [
2227
+ "package-lock.json",
2228
+ "pnpm-lock.yaml",
2229
+ "yarn.lock",
2230
+ "bun.lock",
2231
+ "bun.lockb",
2232
+ ]);
2233
+ const configFiles = await existingWorkspaceFiles(registry, workspace, projectRoot, [
2234
+ "package.json",
2235
+ "tsconfig.json",
2236
+ "jsconfig.json",
2237
+ "vite.config.ts",
2238
+ "vite.config.js",
2239
+ "next.config.js",
2240
+ "next.config.mjs",
2241
+ "eslint.config.js",
2242
+ ".eslintrc.json",
2243
+ "prettier.config.js",
2244
+ ".prettierrc",
2245
+ "pyproject.toml",
2246
+ "requirements.txt",
2247
+ "Cargo.toml",
2248
+ "go.mod",
2249
+ "Dockerfile",
2250
+ "docker-compose.yml",
2251
+ "compose.yml",
2252
+ "Makefile",
2253
+ ]);
2254
+ const instructions = await registry.instructions(workspace.id, targetWorkspacePath, { maxBytes: 1 });
2255
+ return {
2256
+ path: targetWorkspacePath,
2257
+ projectRoot: projectRootPath,
2258
+ packageManagers: packageManagers(packageJson?.packageManager, lockfiles),
2259
+ packageScripts: packageJson?.scripts ? Object.keys(packageJson.scripts).sort() : [],
2260
+ packageName: typeof packageJson?.name === "string" ? packageJson.name : undefined,
2261
+ packageType: typeof packageJson?.type === "string" ? packageJson.type : undefined,
2262
+ configFiles,
2263
+ instructionFiles: instructions.map((file) => file.path),
2264
+ languages: languageHints(treeEntries),
2265
+ git: {
2266
+ detected: Boolean(await nearestExistingWorkspacePath(registry, workspace, targetDirectory, ".git")),
2267
+ },
2268
+ suggestedNextOperations: suggestedNextOperations(workspace),
2269
+ };
2270
+ }
2271
+ async function codingContext(registry, workspace, options) {
2272
+ const target = await registry.resolveExistingPath(workspace, options.path);
2273
+ const targetInfo = await stat(target);
2274
+ const targetDirectory = targetInfo.isDirectory() ? target : dirname(target);
2275
+ const targetWorkspacePath = formatWorkspacePath(targetDirectory, workspace);
2276
+ const maxBytes = normalizeBoundedPositiveInteger(options.maxBytes, 32 * 1024, 128 * 1024);
2277
+ const maxResults = Math.min(normalizeMaxResults(options.maxResults), 50);
2278
+ return {
2279
+ path: targetWorkspacePath,
2280
+ overview: await projectOverview(registry, workspace, {
2281
+ path: targetWorkspacePath,
2282
+ maxDepth: options.maxDepth ?? 3,
2283
+ maxEntries: options.maxEntries ?? 300,
2284
+ }),
2285
+ instructions: await registry.instructions(workspace.id, targetWorkspacePath, {
2286
+ maxBytes,
2287
+ }),
2288
+ tree: await registry.tree(workspace.id, targetWorkspacePath, {
2289
+ maxDepth: options.maxDepth ?? 2,
2290
+ maxEntries: options.maxEntries ?? 100,
2291
+ includeFiles: true,
2292
+ }),
2293
+ agentSkills: await agentSkills(registry, workspace, {
2294
+ maxResults,
2295
+ maxBytes,
2296
+ }),
2297
+ changeSummary: await changeSummary(targetDirectory, {
2298
+ maxBytes,
2299
+ }),
2300
+ };
2301
+ }
2302
+ async function agentSkills(registry, workspace, options) {
2303
+ const maxResults = normalizeMaxResults(options.maxResults);
2304
+ const maxBytes = normalizeBoundedPositiveInteger(options.maxBytes, 32 * 1024, 128 * 1024);
2305
+ const skills = [];
2306
+ const searchedRoots = [];
2307
+ for (const root of AGENT_SKILL_ROOTS) {
2308
+ if (skills.length >= maxResults)
2309
+ break;
2310
+ let absoluteRoot;
2311
+ try {
2312
+ absoluteRoot = await registry.resolveExistingPath(workspace, root);
2313
+ }
2314
+ catch {
2315
+ continue;
2316
+ }
2317
+ searchedRoots.push(root);
2318
+ for (const skillFile of await findSkillFiles(absoluteRoot, workspace, maxResults - skills.length)) {
2319
+ const path = formatWorkspacePath(skillFile, workspace);
2320
+ const content = await registry.readFile(workspace.id, path);
2321
+ skills.push(parseSkillFile(path, content.slice(0, maxBytes), Buffer.byteLength(content, "utf8") > maxBytes));
2322
+ if (skills.length >= maxResults)
2323
+ break;
2324
+ }
2325
+ }
2326
+ return {
2327
+ scope: "workspace",
2328
+ searchedRoots,
2329
+ skills,
2330
+ };
2331
+ }
2332
+ async function packageRun(registry, workspace, cwd, options) {
2333
+ const resolved = await resolvePackageScript(registry, workspace, cwd, options);
2334
+ const commandText = `${resolved.packageManager} ${resolved.args.join(" ")}`;
2335
+ const limits = commandPolicyLimits(workspace, commandText, options, 120);
2336
+ const process = await runProcess(resolved.packageManager, resolved.args, resolved.packageRootAbsolute, limits.timeoutMs, undefined, limits.maxOutputBytes);
2337
+ return {
2338
+ packageRoot: resolved.packageRoot,
2339
+ packageManager: resolved.packageManager,
2340
+ script: options.script,
2341
+ scriptArgs: resolved.scriptArgs,
2342
+ process,
2343
+ };
2344
+ }
2345
+ async function packageStart(registry, workspace, cwd, options) {
2346
+ const resolved = await resolvePackageScript(registry, workspace, cwd, options);
2347
+ const commandText = `${resolved.packageManager} ${resolved.args.join(" ")}`;
2348
+ const limits = managedCommandPolicyLimits(workspace, commandText, options);
2349
+ return {
2350
+ packageRoot: resolved.packageRoot,
2351
+ packageManager: resolved.packageManager,
2352
+ script: options.script,
2353
+ scriptArgs: resolved.scriptArgs,
2354
+ process: startManagedProcess({
2355
+ kind: "shell",
2356
+ workspaceId: workspace.exposedPath.id,
2357
+ workspaceRoot: workspace.root,
2358
+ cwd: resolved.packageRootAbsolute,
2359
+ command: resolved.packageManager,
2360
+ args: resolved.args,
2361
+ commandPreview: previewCommand(commandText),
2362
+ timeoutMs: limits.timeoutMs,
2363
+ maxOutputBytes: limits.maxOutputBytes,
2364
+ }),
2365
+ };
2366
+ }
2367
+ async function resolvePackageScript(registry, workspace, cwd, options) {
2368
+ const targetInfo = await stat(cwd);
2369
+ const targetDirectory = targetInfo.isDirectory() ? cwd : dirname(cwd);
2370
+ const projectRoot = await nearestProjectRoot(registry, workspace, targetDirectory);
2371
+ if (!projectRoot)
2372
+ throw new Error("No package.json found for package script operation");
2373
+ const packageJson = await readPackageJson(registry, workspace, projectRoot);
2374
+ const scripts = packageJson?.scripts ?? {};
2375
+ if (!Object.prototype.hasOwnProperty.call(scripts, options.script)) {
2376
+ throw new Error(`Unknown package script: ${options.script}`);
2377
+ }
2378
+ const packageManager = await packageManagerForRun(registry, workspace, projectRoot, packageJson?.packageManager);
2379
+ const scriptArgs = (options.scriptArgs ?? []).map((arg) => {
2380
+ if (arg.includes("\0"))
2381
+ throw new Error("scriptArgs must not contain NUL bytes");
2382
+ return arg;
2383
+ });
2384
+ return {
2385
+ packageRoot: formatWorkspacePath(projectRoot, workspace),
2386
+ packageRootAbsolute: projectRoot,
2387
+ packageManager,
2388
+ scriptArgs,
2389
+ args: packageRunArgs(packageManager, options.script, scriptArgs),
2390
+ };
2391
+ }
2392
+ async function findSkillFiles(root, workspace, maxResults) {
2393
+ const results = [];
2394
+ async function walk(directory, depth) {
2395
+ if (results.length >= maxResults || depth > 4)
2396
+ return;
2397
+ let entries;
2398
+ try {
2399
+ entries = await opendir(directory);
2400
+ }
2401
+ catch {
2402
+ return;
2403
+ }
2404
+ const directories = [];
2405
+ for await (const entry of entries) {
2406
+ const absolutePath = join(directory, entry.name);
2407
+ if (entry.isFile() && entry.name === "SKILL.md") {
2408
+ results.push(absolutePath);
2409
+ if (results.length >= maxResults)
2410
+ return;
2411
+ }
2412
+ else if (entry.isDirectory() && !SKIPPED_SKILL_DIRECTORIES.has(entry.name)) {
2413
+ directories.push(absolutePath);
2414
+ }
2415
+ }
2416
+ directories.sort((a, b) => basename(a).localeCompare(basename(b)));
2417
+ for (const child of directories) {
2418
+ if (results.length >= maxResults)
2419
+ return;
2420
+ if (!formatWorkspacePath(child, workspace).startsWith("..")) {
2421
+ await walk(child, depth + 1);
2422
+ }
2423
+ }
2424
+ }
2425
+ await walk(root, 1);
2426
+ return results.sort((a, b) => a.localeCompare(b));
2427
+ }
2428
+ function parseSkillFile(path, content, truncated) {
2429
+ const lines = content.split(/\r?\n/);
2430
+ const title = lines.find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
2431
+ const description = firstDescription(lines);
2432
+ return {
2433
+ name: basename(dirname(path)),
2434
+ path,
2435
+ title: title || basename(dirname(path)),
2436
+ description,
2437
+ truncated,
2438
+ };
2439
+ }
2440
+ function firstDescription(lines) {
2441
+ const frontmatterDescription = lines
2442
+ .map((line) => line.match(/^description:\s*(.+)$/i)?.[1]?.trim())
2443
+ .find(Boolean);
2444
+ if (frontmatterDescription)
2445
+ return truncateDescription(frontmatterDescription);
2446
+ for (const line of lines) {
2447
+ const text = line.trim();
2448
+ if (!text || text === "---" || text.startsWith("#") || text.includes(":"))
2449
+ continue;
2450
+ return truncateDescription(text);
2451
+ }
2452
+ return undefined;
2453
+ }
2454
+ function truncateDescription(value) {
2455
+ return value.length > 300 ? `${value.slice(0, 297)}...` : value;
2456
+ }
2457
+ async function nearestProjectRoot(registry, workspace, startDirectory) {
2458
+ let current = startDirectory;
2459
+ while (true) {
2460
+ for (const name of ["package.json", "pyproject.toml", "Cargo.toml", "go.mod", "composer.json"]) {
2461
+ if (await workspacePathExists(registry, workspace, join(current, name)))
2462
+ return current;
2463
+ }
2464
+ if (current === workspace.root)
2465
+ return undefined;
2466
+ current = dirname(current);
2467
+ }
2468
+ }
2469
+ async function nearestExistingWorkspacePath(registry, workspace, startDirectory, name) {
2470
+ let current = startDirectory;
2471
+ while (true) {
2472
+ const candidate = join(current, name);
2473
+ if (await workspacePathExists(registry, workspace, candidate))
2474
+ return candidate;
2475
+ if (current === workspace.root)
2476
+ return undefined;
2477
+ current = dirname(current);
2478
+ }
2479
+ }
2480
+ async function existingWorkspaceFiles(registry, workspace, directory, names) {
2481
+ const files = [];
2482
+ for (const name of names) {
2483
+ const absolutePath = join(directory, name);
2484
+ if (await workspacePathExists(registry, workspace, absolutePath)) {
2485
+ files.push(formatWorkspacePath(absolutePath, workspace));
2486
+ }
2487
+ }
2488
+ return files;
2489
+ }
2490
+ async function readPackageJson(registry, workspace, directory) {
2491
+ const packagePath = join(directory, "package.json");
2492
+ if (!(await workspacePathExists(registry, workspace, packagePath)))
2493
+ return undefined;
2494
+ const content = await readFile(await registry.resolveExistingPath(workspace, formatWorkspacePath(packagePath, workspace)), "utf8");
2495
+ const parsed = JSON.parse(content);
2496
+ return {
2497
+ name: parsed.name,
2498
+ type: parsed.type,
2499
+ packageManager: parsed.packageManager,
2500
+ scripts: parsed.scripts && typeof parsed.scripts === "object" && !Array.isArray(parsed.scripts)
2501
+ ? parsed.scripts
2502
+ : undefined,
2503
+ };
2504
+ }
2505
+ async function workspacePathExists(registry, workspace, absolutePath) {
2506
+ try {
2507
+ await registry.resolveExistingPath(workspace, formatWorkspacePath(absolutePath, workspace));
2508
+ return true;
2509
+ }
2510
+ catch {
2511
+ return false;
2512
+ }
2513
+ }
2514
+ function packageManagers(packageManager, lockfiles) {
2515
+ const managers = new Set();
2516
+ if (typeof packageManager === "string" && packageManager.trim()) {
2517
+ managers.add(packageManager.split("@")[0] ?? packageManager);
2518
+ }
2519
+ for (const lockfile of lockfiles.map((path) => basename(path))) {
2520
+ if (lockfile === "package-lock.json")
2521
+ managers.add("npm");
2522
+ if (lockfile === "pnpm-lock.yaml")
2523
+ managers.add("pnpm");
2524
+ if (lockfile === "yarn.lock")
2525
+ managers.add("yarn");
2526
+ if (lockfile === "bun.lock" || lockfile === "bun.lockb")
2527
+ managers.add("bun");
2528
+ }
2529
+ return [...managers].sort();
2530
+ }
2531
+ async function packageManagerForRun(registry, workspace, projectRoot, packageManager) {
2532
+ if (typeof packageManager === "string" && packageManager.trim()) {
2533
+ return packageManager.split("@")[0] || "npm";
2534
+ }
2535
+ const lockfiles = await existingWorkspaceFiles(registry, workspace, projectRoot, [
2536
+ "pnpm-lock.yaml",
2537
+ "yarn.lock",
2538
+ "bun.lock",
2539
+ "bun.lockb",
2540
+ "package-lock.json",
2541
+ ]);
2542
+ const names = lockfiles.map((path) => basename(path));
2543
+ if (names.includes("pnpm-lock.yaml"))
2544
+ return "pnpm";
2545
+ if (names.includes("yarn.lock"))
2546
+ return "yarn";
2547
+ if (names.includes("bun.lock") || names.includes("bun.lockb"))
2548
+ return "bun";
2549
+ return "npm";
2550
+ }
2551
+ function packageRunArgs(manager, script, scriptArgs) {
2552
+ if (manager === "npm")
2553
+ return ["run", script, "--", ...scriptArgs];
2554
+ if (manager === "pnpm")
2555
+ return ["run", script, "--", ...scriptArgs];
2556
+ if (manager === "yarn")
2557
+ return ["run", script, ...scriptArgs];
2558
+ if (manager === "bun")
2559
+ return ["run", script, ...scriptArgs];
2560
+ return ["run", script, "--", ...scriptArgs];
2561
+ }
2562
+ function languageHints(entries) {
2563
+ const counts = new Map();
2564
+ for (const entry of entries) {
2565
+ if (entry.type !== "file")
2566
+ continue;
2567
+ const language = languageForPath(entry.path || entry.name);
2568
+ if (!language)
2569
+ continue;
2570
+ counts.set(language, (counts.get(language) ?? 0) + 1);
2571
+ }
2572
+ return [...counts.entries()]
2573
+ .map(([language, files]) => ({ language, files }))
2574
+ .sort((a, b) => b.files - a.files || a.language.localeCompare(b.language));
2575
+ }
2576
+ function languageForPath(path) {
2577
+ const name = basename(path);
2578
+ if (name === "Dockerfile")
2579
+ return "Dockerfile";
2580
+ if (name === "Makefile")
2581
+ return "Make";
2582
+ switch (extname(path)) {
2583
+ case ".ts":
2584
+ case ".tsx":
2585
+ return "TypeScript";
2586
+ case ".js":
2587
+ case ".jsx":
2588
+ case ".mjs":
2589
+ case ".cjs":
2590
+ return "JavaScript";
2591
+ case ".py":
2592
+ return "Python";
2593
+ case ".go":
2594
+ return "Go";
2595
+ case ".rs":
2596
+ return "Rust";
2597
+ case ".swift":
2598
+ return "Swift";
2599
+ case ".java":
2600
+ return "Java";
2601
+ case ".kt":
2602
+ case ".kts":
2603
+ return "Kotlin";
2604
+ case ".rb":
2605
+ return "Ruby";
2606
+ case ".php":
2607
+ return "PHP";
2608
+ case ".css":
2609
+ return "CSS";
2610
+ case ".html":
2611
+ return "HTML";
2612
+ case ".md":
2613
+ return "Markdown";
2614
+ case ".json":
2615
+ return "JSON";
2616
+ case ".yml":
2617
+ case ".yaml":
2618
+ return "YAML";
2619
+ default:
2620
+ return undefined;
2621
+ }
2622
+ }
2623
+ function suggestedNextOperations(workspace) {
2624
+ return [
2625
+ "coding_context",
2626
+ "instructions",
2627
+ "agent_skills",
2628
+ "change_summary",
2629
+ "repo_status",
2630
+ "git_changes",
2631
+ "git_diff",
2632
+ "git_log",
2633
+ "git_show",
2634
+ "git_worktree_list",
2635
+ "tree",
2636
+ "search_symbols",
2637
+ "search_text",
2638
+ "read_many",
2639
+ ...(workspace.exposedPath.permissions.write ? ["git_stage", "git_unstage", "git_commit", "git_worktree_create"] : []),
2640
+ ...(workspace.exposedPath.permissions.shell ? ["package_run", "package_start", "command", "process_start", "process_list"] : []),
2641
+ ...(workspace.exposedPath.permissions.codex ? ["codex_plan", "codex_review", "codex_fix", "codex_test", "codex_continue", "codex_runs", "codex"] : []),
2642
+ ];
2643
+ }
2644
+ async function validatePatchPaths(registry, workspace, patch) {
2645
+ const paths = extractPatchPaths(patch);
2646
+ if (paths.length === 0) {
2647
+ throw new Error("patch must include at least one workspace path");
2648
+ }
2649
+ for (const path of paths) {
2650
+ if (path.startsWith("/") || path.includes("\0")) {
2651
+ throw new Error(`Patch path is not allowed: ${path}`);
2652
+ }
2653
+ await registry.resolveWritablePath(workspace, path);
2654
+ }
2655
+ }
2656
+ function extractPatchPaths(patch) {
2657
+ const paths = new Set();
2658
+ for (const line of patch.split(/\r?\n/)) {
2659
+ if (line.startsWith("diff --git ")) {
2660
+ const parts = line.slice("diff --git ".length).trim().split(/\s+/);
2661
+ for (const part of parts)
2662
+ addPatchPath(paths, stripGitPrefix(part));
2663
+ continue;
2664
+ }
2665
+ if (line.startsWith("--- ") || line.startsWith("+++ ")) {
2666
+ addPatchPath(paths, stripGitPrefix(line.slice(4).trim().split(/\s+/)[0] ?? ""));
2667
+ continue;
2668
+ }
2669
+ if (line.startsWith("rename from ") || line.startsWith("rename to ")) {
2670
+ addPatchPath(paths, line.replace(/^rename (from|to) /, "").trim());
2671
+ continue;
2672
+ }
2673
+ if (line.startsWith("copy from ") || line.startsWith("copy to ")) {
2674
+ addPatchPath(paths, line.replace(/^copy (from|to) /, "").trim());
2675
+ }
2676
+ }
2677
+ return [...paths];
2678
+ }
2679
+ function addPatchPath(paths, path) {
2680
+ if (!path || path === "/dev/null")
2681
+ return;
2682
+ paths.add(path);
2683
+ }
2684
+ function stripGitPrefix(path) {
2685
+ const normalized = path.replace(/^"|"$/g, "");
2686
+ return normalized.startsWith("a/") || normalized.startsWith("b/") ? normalized.slice(2) : normalized;
2687
+ }
2688
+ async function repoStatus(cwd, options) {
2689
+ const maxBytes = normalizeBoundedPositiveInteger(options.maxBytes, 64 * 1024, 256 * 1024);
2690
+ const status = await runProcess("git", ["status", "--short", "--branch"], cwd, 10_000);
2691
+ if (status.exitCode !== 0) {
2692
+ return {
2693
+ isGitRepository: false,
2694
+ status,
2695
+ };
2696
+ }
2697
+ const diffStat = await runProcess("git", ["diff", "--stat"], cwd, 10_000);
2698
+ const stagedDiffStat = await runProcess("git", ["diff", "--cached", "--stat"], cwd, 10_000);
2699
+ const result = {
2700
+ isGitRepository: true,
2701
+ status: status.stdout,
2702
+ diffStat: diffStat.stdout,
2703
+ stagedDiffStat: stagedDiffStat.stdout,
2704
+ };
2705
+ if (options.includeDiff) {
2706
+ const diff = await runProcess("git", ["diff", "--"], cwd, 10_000);
2707
+ const stagedDiff = await runProcess("git", ["diff", "--cached", "--"], cwd, 10_000);
2708
+ result.diff = truncateText(diff.stdout, maxBytes);
2709
+ result.diffTruncated = diff.stdout.length > maxBytes;
2710
+ result.stagedDiff = truncateText(stagedDiff.stdout, maxBytes);
2711
+ result.stagedDiffTruncated = stagedDiff.stdout.length > maxBytes;
2712
+ }
2713
+ return result;
2714
+ }
2715
+ async function gitChanges(cwd) {
2716
+ const process = await runProcess("git", ["status", "--porcelain=v1", "--branch"], cwd, 10_000);
2717
+ if (process.exitCode !== 0) {
2718
+ return {
2719
+ isGitRepository: false,
2720
+ process,
2721
+ clean: false,
2722
+ entries: [],
2723
+ counts: { total: 0, staged: 0, unstaged: 0, untracked: 0, ignored: 0 },
2724
+ };
2725
+ }
2726
+ const parsed = parseGitChanges(process.stdout);
2727
+ return {
2728
+ isGitRepository: true,
2729
+ ...parsed,
2730
+ };
2731
+ }
2732
+ async function changeSummary(cwd, options) {
2733
+ const maxBytes = normalizeBoundedPositiveInteger(options.maxBytes, 64 * 1024, 256 * 1024);
2734
+ const status = await runProcess("git", ["status", "--porcelain=v1", "--branch"], cwd, 10_000);
2735
+ if (status.exitCode !== 0) {
2736
+ return {
2737
+ isGitRepository: false,
2738
+ status,
2739
+ clean: false,
2740
+ branchLine: undefined,
2741
+ counts: { total: 0, staged: 0, unstaged: 0, untracked: 0, ignored: 0 },
2742
+ entries: [],
2743
+ diffStat: "",
2744
+ stagedDiffStat: "",
2745
+ diffStatTruncated: false,
2746
+ stagedDiffStatTruncated: false,
2747
+ };
2748
+ }
2749
+ const parsed = parseGitChanges(status.stdout);
2750
+ const diffStat = await runProcess("git", ["diff", "--stat"], cwd, 10_000);
2751
+ const stagedDiffStat = await runProcess("git", ["diff", "--cached", "--stat"], cwd, 10_000);
2752
+ return {
2753
+ isGitRepository: true,
2754
+ branchLine: parsed.branchLine,
2755
+ clean: parsed.clean,
2756
+ counts: parsed.counts,
2757
+ entries: parsed.entries,
2758
+ diffStat: truncateText(diffStat.stdout, maxBytes),
2759
+ stagedDiffStat: truncateText(stagedDiffStat.stdout, maxBytes),
2760
+ diffStatTruncated: Buffer.byteLength(diffStat.stdout, "utf8") > maxBytes,
2761
+ stagedDiffStatTruncated: Buffer.byteLength(stagedDiffStat.stdout, "utf8") > maxBytes,
2762
+ };
2763
+ }
2764
+ async function gitDiff(cwd, options) {
2765
+ const maxBytes = normalizeBoundedPositiveInteger(options.maxBytes, 64 * 1024, 256 * 1024);
2766
+ const repositoryCheck = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], cwd, 10_000);
2767
+ if (repositoryCheck.exitCode !== 0) {
2768
+ return {
2769
+ isGitRepository: false,
2770
+ staged: options.staged,
2771
+ paths: options.paths,
2772
+ pathspecs: options.pathspecs,
2773
+ process: repositoryCheck,
2774
+ diff: "",
2775
+ truncated: false,
2776
+ };
2777
+ }
2778
+ const args = ["diff"];
2779
+ if (options.staged)
2780
+ args.push("--cached");
2781
+ args.push("--", ...options.pathspecs);
2782
+ const process = await runProcess("git", args, cwd, 10_000);
2783
+ if (process.exitCode !== 0) {
2784
+ return {
2785
+ isGitRepository: false,
2786
+ staged: options.staged,
2787
+ paths: options.paths,
2788
+ pathspecs: options.pathspecs,
2789
+ process,
2790
+ diff: "",
2791
+ truncated: false,
2792
+ };
2793
+ }
2794
+ return {
2795
+ isGitRepository: true,
2796
+ staged: options.staged,
2797
+ paths: options.paths,
2798
+ pathspecs: options.pathspecs,
2799
+ diff: truncateText(process.stdout, maxBytes),
2800
+ sizeBytes: Buffer.byteLength(process.stdout, "utf8"),
2801
+ truncated: Buffer.byteLength(process.stdout, "utf8") > maxBytes,
2802
+ };
2803
+ }
2804
+ async function gitLog(cwd, options) {
2805
+ const maxResults = Math.min(normalizeMaxResults(options.maxResults), 200);
2806
+ const repositoryCheck = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], cwd, 10_000);
2807
+ if (repositoryCheck.exitCode !== 0) {
2808
+ return {
2809
+ isGitRepository: false,
2810
+ paths: options.paths,
2811
+ pathspecs: options.pathspecs,
2812
+ process: repositoryCheck,
2813
+ commits: [],
2814
+ };
2815
+ }
2816
+ const args = [
2817
+ "log",
2818
+ `--max-count=${maxResults}`,
2819
+ "--date=iso-strict",
2820
+ "--pretty=format:%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s%x1e",
2821
+ "--",
2822
+ ...options.pathspecs,
2823
+ ];
2824
+ const process = await runProcess("git", args, cwd, 10_000);
2825
+ if (process.exitCode !== 0) {
2826
+ return {
2827
+ isGitRepository: true,
2828
+ paths: options.paths,
2829
+ pathspecs: options.pathspecs,
2830
+ process,
2831
+ commits: [],
2832
+ };
2833
+ }
2834
+ return {
2835
+ isGitRepository: true,
2836
+ paths: options.paths,
2837
+ pathspecs: options.pathspecs,
2838
+ commits: parseGitLog(process.stdout),
2839
+ };
2840
+ }
2841
+ async function gitShow(cwd, options) {
2842
+ const ref = optionalGitRef(options.ref, "ref") ?? "HEAD";
2843
+ const maxBytes = normalizeBoundedPositiveInteger(options.maxBytes, 64 * 1024, 256 * 1024);
2844
+ const repositoryCheck = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], cwd, 10_000);
2845
+ if (repositoryCheck.exitCode !== 0) {
2846
+ return {
2847
+ isGitRepository: false,
2848
+ ref,
2849
+ paths: options.paths,
2850
+ pathspecs: options.pathspecs,
2851
+ process: repositoryCheck,
2852
+ output: "",
2853
+ truncated: false,
2854
+ };
2855
+ }
2856
+ const args = ["show", "--stat", "--patch", "--format=fuller", ref, "--", ...options.pathspecs];
2857
+ const process = await runProcess("git", args, cwd, 10_000);
2858
+ if (process.exitCode !== 0) {
2859
+ return {
2860
+ isGitRepository: true,
2861
+ ref,
2862
+ paths: options.paths,
2863
+ pathspecs: options.pathspecs,
2864
+ process,
2865
+ output: "",
2866
+ truncated: false,
2867
+ };
2868
+ }
2869
+ return {
2870
+ isGitRepository: true,
2871
+ ref,
2872
+ paths: options.paths,
2873
+ pathspecs: options.pathspecs,
2874
+ output: truncateText(process.stdout, maxBytes),
2875
+ sizeBytes: Buffer.byteLength(process.stdout, "utf8"),
2876
+ truncated: Buffer.byteLength(process.stdout, "utf8") > maxBytes,
2877
+ };
2878
+ }
2879
+ async function gitIndexUpdate(cwd, options) {
2880
+ const repositoryCheck = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], cwd, 10_000);
2881
+ if (repositoryCheck.exitCode !== 0) {
2882
+ return {
2883
+ isGitRepository: false,
2884
+ action: options.action,
2885
+ paths: options.paths,
2886
+ pathspecs: options.pathspecs,
2887
+ process: repositoryCheck,
2888
+ updated: false,
2889
+ };
2890
+ }
2891
+ const process = await runProcess("git", [...options.commandArgs, ...options.pathspecs], cwd, 30_000);
2892
+ return {
2893
+ isGitRepository: true,
2894
+ action: options.action,
2895
+ paths: options.paths,
2896
+ pathspecs: options.pathspecs,
2897
+ process,
2898
+ updated: process.exitCode === 0,
2899
+ };
2900
+ }
2901
+ async function gitCommit(workspace, cwd, message) {
2902
+ const repositoryCheck = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], cwd, 10_000);
2903
+ if (repositoryCheck.exitCode !== 0) {
2904
+ return {
2905
+ isGitRepository: false,
2906
+ committed: false,
2907
+ process: repositoryCheck,
2908
+ stagedPaths: [],
2909
+ };
2910
+ }
2911
+ const rootResult = await runProcess("git", ["rev-parse", "--show-toplevel"], cwd, 10_000);
2912
+ if (rootResult.exitCode !== 0) {
2913
+ return {
2914
+ isGitRepository: false,
2915
+ committed: false,
2916
+ process: rootResult,
2917
+ stagedPaths: [],
2918
+ };
2919
+ }
2920
+ const repositoryRoot = rootResult.stdout.trim();
2921
+ const staged = await runProcess("git", ["diff", "--cached", "--name-only", "-z"], cwd, 10_000);
2922
+ if (staged.exitCode !== 0) {
2923
+ return {
2924
+ isGitRepository: true,
2925
+ committed: false,
2926
+ repositoryRoot,
2927
+ process: staged,
2928
+ stagedPaths: [],
2929
+ };
2930
+ }
2931
+ const stagedPaths = staged.stdout.split("\0").filter(Boolean);
2932
+ if (stagedPaths.length === 0) {
2933
+ return {
2934
+ isGitRepository: true,
2935
+ committed: false,
2936
+ repositoryRoot,
2937
+ stagedPaths,
2938
+ error: "no staged files to commit",
2939
+ };
2940
+ }
2941
+ const workspaceRoot = resolve(workspace.root);
2942
+ const outsidePaths = stagedPaths.filter((path) => {
2943
+ const absolutePath = resolve(repositoryRoot, path);
2944
+ return !isInsideResolvedRoot(absolutePath, workspaceRoot);
2945
+ });
2946
+ if (outsidePaths.length > 0) {
2947
+ return {
2948
+ isGitRepository: true,
2949
+ committed: false,
2950
+ repositoryRoot,
2951
+ stagedPaths,
2952
+ outsideWorkspacePaths: outsidePaths,
2953
+ error: "staged files include paths outside the workspace",
2954
+ };
2955
+ }
2956
+ const process = await runProcess("git", ["commit", "-m", message], cwd, 60_000);
2957
+ return {
2958
+ isGitRepository: true,
2959
+ committed: process.exitCode === 0,
2960
+ repositoryRoot,
2961
+ stagedPaths,
2962
+ process,
2963
+ };
2964
+ }
2965
+ async function validateGitPathspecs(registry, workspace, cwd, paths) {
2966
+ if (!paths || paths.length === 0)
2967
+ return [];
2968
+ if (paths.length > 100)
2969
+ throw new Error("paths supports at most 100 files per call");
2970
+ const normalized = [];
2971
+ for (const path of paths.map((value) => required(value, "paths[]"))) {
2972
+ if (path.startsWith("/") || path.startsWith("-") || path.includes("\0")) {
2973
+ throw new Error(`Git pathspec is not allowed: ${path}`);
2974
+ }
2975
+ const absolutePath = registry.resolvePath(workspace, path);
2976
+ try {
2977
+ await registry.resolveExistingPath(workspace, path);
2978
+ }
2979
+ catch {
2980
+ await registry.resolveExistingPath(workspace, dirname(path));
2981
+ }
2982
+ normalized.push({
2983
+ inputPath: path,
2984
+ gitPathspec: formatGitPathspec(absolutePath, cwd),
2985
+ });
2986
+ }
2987
+ return normalized;
2988
+ }
2989
+ function formatGitPathspec(path, cwd) {
2990
+ const pathspec = relative(cwd, path);
2991
+ return pathspec ? pathspec.split(sep).join("/") : ".";
2992
+ }
2993
+ function isInsideResolvedRoot(path, root) {
2994
+ const relationship = relative(root, path);
2995
+ return relationship === "" || (relationship !== ".." && !relationship.startsWith(`..${sep}`));
2996
+ }
2997
+ function requireGitPathspecs(paths) {
2998
+ if (paths.length === 0) {
2999
+ throw new Error("paths is required for this operation");
3000
+ }
3001
+ return paths;
3002
+ }
3003
+ function parseGitChanges(output) {
3004
+ const entries = [];
3005
+ let branchLine;
3006
+ for (const line of output.split(/\r?\n/).filter(Boolean)) {
3007
+ if (line.startsWith("## ")) {
3008
+ branchLine = line.slice(3);
3009
+ continue;
3010
+ }
3011
+ const indexStatus = line[0] ?? " ";
3012
+ const workingTreeStatus = line[1] ?? " ";
3013
+ const rawPath = line.slice(3);
3014
+ const renameParts = rawPath.includes(" -> ") ? rawPath.split(" -> ") : undefined;
3015
+ const rawStatus = `${indexStatus}${workingTreeStatus}`;
3016
+ const untracked = rawStatus === "??";
3017
+ const ignored = rawStatus === "!!";
3018
+ entries.push({
3019
+ path: renameParts?.[1] ?? rawPath,
3020
+ originalPath: renameParts?.[0],
3021
+ rawStatus,
3022
+ indexStatus: statusName(indexStatus),
3023
+ workingTreeStatus: statusName(workingTreeStatus),
3024
+ staged: !untracked && !ignored && indexStatus !== " ",
3025
+ unstaged: !untracked && !ignored && workingTreeStatus !== " ",
3026
+ untracked,
3027
+ ignored,
3028
+ });
3029
+ }
3030
+ return {
3031
+ branchLine,
3032
+ clean: entries.length === 0,
3033
+ counts: {
3034
+ total: entries.length,
3035
+ staged: entries.filter((entry) => entry.staged).length,
3036
+ unstaged: entries.filter((entry) => entry.unstaged).length,
3037
+ untracked: entries.filter((entry) => entry.untracked).length,
3038
+ ignored: entries.filter((entry) => entry.ignored).length,
3039
+ },
3040
+ entries,
3041
+ };
3042
+ }
3043
+ function parseGitLog(output) {
3044
+ return output
3045
+ .split("\x1e")
3046
+ .map((entry) => entry.trim())
3047
+ .filter(Boolean)
3048
+ .map((entry) => {
3049
+ const [hash, shortHash, authorName, authorEmail, authoredAt, subject] = entry.split("\x1f");
3050
+ return {
3051
+ hash,
3052
+ shortHash,
3053
+ authorName,
3054
+ authorEmail,
3055
+ authoredAt,
3056
+ subject,
3057
+ };
3058
+ });
3059
+ }
3060
+ function statusName(status) {
3061
+ switch (status) {
3062
+ case " ":
3063
+ return undefined;
3064
+ case "M":
3065
+ return "modified";
3066
+ case "A":
3067
+ return "added";
3068
+ case "D":
3069
+ return "deleted";
3070
+ case "R":
3071
+ return "renamed";
3072
+ case "C":
3073
+ return "copied";
3074
+ case "U":
3075
+ return "unmerged";
3076
+ case "?":
3077
+ return "untracked";
3078
+ case "!":
3079
+ return "ignored";
3080
+ default:
3081
+ return status;
3082
+ }
3083
+ }
3084
+ async function gitWorktreeList(cwd) {
3085
+ const result = await runProcess("git", ["worktree", "list", "--porcelain"], cwd, 10_000);
3086
+ if (result.exitCode !== 0) {
3087
+ return {
3088
+ isGitRepository: false,
3089
+ process: result,
3090
+ worktrees: [],
3091
+ };
3092
+ }
3093
+ return {
3094
+ isGitRepository: true,
3095
+ worktrees: parseGitWorktrees(result.stdout),
3096
+ };
3097
+ }
3098
+ async function gitWorktreeCreate(cwd, target, options) {
3099
+ const branch = optionalGitRef(options.branch, "branch");
3100
+ const startPoint = optionalGitRef(options.startPoint, "startPoint");
3101
+ const args = ["worktree", "add"];
3102
+ if (branch)
3103
+ args.push("-b", branch);
3104
+ args.push(target);
3105
+ if (startPoint)
3106
+ args.push(startPoint);
3107
+ const process = await runProcess("git", args, cwd, 60_000);
3108
+ return {
3109
+ created: process.exitCode === 0,
3110
+ targetPath: options.targetPath,
3111
+ branch,
3112
+ startPoint,
3113
+ process,
3114
+ };
3115
+ }
3116
+ function parseGitWorktrees(output) {
3117
+ const worktrees = [];
3118
+ let current;
3119
+ for (const line of output.split(/\r?\n/)) {
3120
+ if (!line) {
3121
+ if (current)
3122
+ worktrees.push(current);
3123
+ current = undefined;
3124
+ continue;
3125
+ }
3126
+ const [key, ...rest] = line.split(" ");
3127
+ const value = rest.join(" ");
3128
+ if (key === "worktree") {
3129
+ if (current)
3130
+ worktrees.push(current);
3131
+ current = { path: value };
3132
+ continue;
3133
+ }
3134
+ if (!current)
3135
+ continue;
3136
+ if (key === "HEAD")
3137
+ current.head = value;
3138
+ else if (key === "branch")
3139
+ current.branch = value;
3140
+ else if (key === "bare" || key === "detached" || key === "locked" || key === "prunable")
3141
+ current[key] = true;
3142
+ else
3143
+ current[key] = value || true;
3144
+ }
3145
+ if (current)
3146
+ worktrees.push(current);
3147
+ return worktrees;
3148
+ }
3149
+ function optionalGitRef(value, name) {
3150
+ const text = value?.trim();
3151
+ if (!text)
3152
+ return undefined;
3153
+ if (text.startsWith("-") || text.includes("\0")) {
3154
+ throw new Error(`${name} is not allowed`);
3155
+ }
3156
+ return text;
3157
+ }
3158
+ function normalizeBoundedPositiveInteger(value, fallback, max) {
3159
+ return Number.isInteger(value) && value && value > 0 ? Math.min(value, max) : fallback;
3160
+ }
3161
+ function truncateText(value, maxBytes) {
3162
+ return Buffer.byteLength(value, "utf8") > maxBytes
3163
+ ? Buffer.from(value, "utf8").subarray(0, maxBytes).toString("utf8")
3164
+ : value;
3165
+ }
3166
+ async function runProcess(command, args, cwd, timeoutMs, stdin, maxOutputBytes) {
3167
+ return new Promise((resolve, reject) => {
3168
+ const executable = executableCommand(command, args);
3169
+ const child = execFile(executable.command, executable.args, {
3170
+ cwd,
3171
+ timeout: timeoutMs,
3172
+ maxBuffer: 1024 * 1024 * 10,
3173
+ windowsVerbatimArguments: executable.windowsVerbatimArguments,
3174
+ }, (error, stdout, stderr) => {
3175
+ if (error && !isExecError(error)) {
3176
+ reject(error);
3177
+ return;
3178
+ }
3179
+ const boundedStdout = maxOutputBytes === undefined ? { text: stdout, truncated: false } : boundedProcessOutput(stdout, maxOutputBytes);
3180
+ const boundedStderr = maxOutputBytes === undefined ? { text: stderr, truncated: false } : boundedProcessOutput(stderr, maxOutputBytes);
3181
+ resolve({
3182
+ exitCode: error ? error.code ?? null : 0,
3183
+ signal: error?.signal ?? undefined,
3184
+ timedOut: Boolean(error?.killed && error?.signal === "SIGTERM"),
3185
+ stdout: boundedStdout.text,
3186
+ stderr: boundedStderr.text,
3187
+ stdoutTruncated: boundedStdout.truncated || undefined,
3188
+ stderrTruncated: boundedStderr.truncated || undefined,
3189
+ });
3190
+ });
3191
+ if (stdin !== undefined) {
3192
+ child.stdin?.end(stdin);
3193
+ }
3194
+ });
3195
+ }
3196
+ function boundedProcessOutput(value, maxOutputBytes) {
3197
+ const truncated = Buffer.byteLength(value, "utf8") > maxOutputBytes;
3198
+ return {
3199
+ text: truncated ? truncateText(value, maxOutputBytes) : value,
3200
+ truncated,
3201
+ };
3202
+ }
3203
+ function isExecError(error) {
3204
+ return error instanceof Error && ("code" in error || "signal" in error || "killed" in error);
3205
+ }
3206
+ function splitSearchOutput(output) {
3207
+ if (output === "No matches." || output === "No files found.")
3208
+ return [];
3209
+ return output.split("\n").filter((line) => line && line !== "--");
3210
+ }
3211
+ const AGENT_SKILL_ROOTS = [".codex/skills", ".claude/skills", "skills"];
3212
+ const SKIPPED_SKILL_DIRECTORIES = new Set([
3213
+ ".git",
3214
+ "node_modules",
3215
+ "dist",
3216
+ "build",
3217
+ ".next",
3218
+ ".cache",
3219
+ ]);