@akira-tl/forgerelay 0.2.1 → 0.2.3

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 (88) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/file-mutations.js +50 -0
  3. package/dist/mcp/server-instructions.js +15 -6
  4. package/dist/server.js +145 -2
  5. package/dist/ui/.vite/manifest.json +280 -280
  6. package/dist/ui/assets/{angular-html-CovppXt5.js → angular-html-CQmCUfkv.js} +1 -1
  7. package/dist/ui/assets/{angular-ts-0Y9l6YXu.js → angular-ts-BSbfxpMS.js} +1 -1
  8. package/dist/ui/assets/{apl-BmBhiklA.js → apl-CWCaH-E3.js} +1 -1
  9. package/dist/ui/assets/{astro-BAmqcqyV.js → astro-R_p4G0xA.js} +1 -1
  10. package/dist/ui/assets/{blade-BjFcmRJo.js → blade-irHensDY.js} +1 -1
  11. package/dist/ui/assets/{c-CaOJn4x0.js → c-_XEQUoq6.js} +1 -1
  12. package/dist/ui/assets/{cobol-DR4zWFMt.js → cobol-Css6VQjJ.js} +1 -1
  13. package/dist/ui/assets/{coffee-ReGSThmM.js → coffee-CpXUkyDm.js} +1 -1
  14. package/dist/ui/assets/{cpp-CnlQh6MO.js → cpp-BQHsPWwI.js} +1 -1
  15. package/dist/ui/assets/{crystal-BrpPbH1U.js → crystal-Cbv8J9Fm.js} +1 -1
  16. package/dist/ui/assets/{css-DRBEvyPU.js → css-D8DkrS3U.js} +1 -1
  17. package/dist/ui/assets/{edge-cfwpdvl9.js → edge-bBL_XDAG.js} +1 -1
  18. package/dist/ui/assets/{elixir-7owrJxb6.js → elixir-CQjsM9sA.js} +1 -1
  19. package/dist/ui/assets/{elm-udiOjODk.js → elm-DLFl-jEZ.js} +1 -1
  20. package/dist/ui/assets/{erb-8ctXHMkS.js → erb-BJjsdB_Q.js} +1 -1
  21. package/dist/ui/assets/{git-rebase-P0V_qK1w.js → git-rebase-CqP5A2s0.js} +1 -1
  22. package/dist/ui/assets/{glimmer-js-CZ14ED7U.js → glimmer-js-B5Hh_7xe.js} +1 -1
  23. package/dist/ui/assets/{glimmer-ts-DtBSFP-o.js → glimmer-ts-oaeSAC8s.js} +1 -1
  24. package/dist/ui/assets/{glsl-BX2BkZJK.js → glsl-BylkOpcF.js} +1 -1
  25. package/dist/ui/assets/{graphql-Dj_cPv4w.js → graphql-DsVRvP6S.js} +1 -1
  26. package/dist/ui/assets/{hack-DGRkd8xn.js → hack-D4N5abT_.js} +1 -1
  27. package/dist/ui/assets/{haml-BAPSvujW.js → haml-CIAJQIox.js} +1 -1
  28. package/dist/ui/assets/{handlebars-PYksdULG.js → handlebars-DDEZyNXR.js} +1 -1
  29. package/dist/ui/assets/{heavy-payload-BjE0LSUi.js → heavy-payload-DafM8yjB.js} +1 -1
  30. package/dist/ui/assets/{html-derivative-C6q86ZrS.js → html-derivative-CZ91Y2d5.js} +1 -1
  31. package/dist/ui/assets/{html-DloAPGZ1.js → html-zDy-3lvy.js} +1 -1
  32. package/dist/ui/assets/{http-fPvYDvHA.js → http-ezaodZRf.js} +1 -1
  33. package/dist/ui/assets/{hurl-Dbir4WvE.js → hurl-BmL0o7TO.js} +1 -1
  34. package/dist/ui/assets/{java-DvkqXg3z.js → java-Q2C0vwbH.js} +1 -1
  35. package/dist/ui/assets/{javascript-TONTV6Ty.js → javascript-UDhBkHE3.js} +1 -1
  36. package/dist/ui/assets/{jinja-OM1F5Gct.js → jinja-BWRskcYS.js} +1 -1
  37. package/dist/ui/assets/{jison-CBttugts.js → jison-B18BY9Ds.js} +1 -1
  38. package/dist/ui/assets/{json-Db5oY-7J.js → json-DNj7MpRr.js} +1 -1
  39. package/dist/ui/assets/{jsx-DKeJCgsc.js → jsx-CVpHJ6cr.js} +1 -1
  40. package/dist/ui/assets/{julia-NlIPhXaL.js → julia-CJBNBS9c.js} +1 -1
  41. package/dist/ui/assets/{just-BXBJB-mX.js → just-Bmu2JCQT.js} +1 -1
  42. package/dist/ui/assets/{latex-CGK-6yDy.js → latex-BB7F93KY.js} +1 -1
  43. package/dist/ui/assets/{liquid-D7waQ0MY.js → liquid-Btnomc6E.js} +1 -1
  44. package/dist/ui/assets/{lua-C_kkt4h1.js → lua-D0FYT9Vo.js} +1 -1
  45. package/dist/ui/assets/{marko-BokXuCts.js → marko-MHLw0ZlK.js} +1 -1
  46. package/dist/ui/assets/{mdc-Bk0nnUJA.js → mdc-CSr-suGy.js} +1 -1
  47. package/dist/ui/assets/{nginx-jM6NF9TV.js → nginx-ur1l6E-g.js} +1 -1
  48. package/dist/ui/assets/{nim-hYsL-WKb.js → nim-B2k0IugA.js} +1 -1
  49. package/dist/ui/assets/{perl-DBI_cUHJ.js → perl-BqOHxFDn.js} +1 -1
  50. package/dist/ui/assets/{php-BX_aQhFl.js → php-DzPK6O1_.js} +1 -1
  51. package/dist/ui/assets/{pug-DwFAYy1e.js → pug-D169OZkr.js} +1 -1
  52. package/dist/ui/assets/{qml-eLSwQUKt.js → qml-BtemJTC4.js} +1 -1
  53. package/dist/ui/assets/{r-BMnQsMLf.js → r-BGVHu0bd.js} +1 -1
  54. package/dist/ui/assets/{razor-CgciiMyw.js → razor-Diisjg1d.js} +1 -1
  55. package/dist/ui/assets/{regexp-ZrURNrn5.js → regexp-B2F5vMZ6.js} +1 -1
  56. package/dist/ui/assets/{review-payload-HGkpJjjm.js → review-payload-B-3I3Oj_.js} +1 -1
  57. package/dist/ui/assets/{rst-EoAOr3M6.js → rst-BxpeZwuT.js} +1 -1
  58. package/dist/ui/assets/{ruby-Dqx5_Rqw.js → ruby-DrCq4riN.js} +1 -1
  59. package/dist/ui/assets/{sas-Bf6xPLSX.js → sas-CI_2W-1n.js} +1 -1
  60. package/dist/ui/assets/{scrollbar-DX4Ujotb.js → scrollbar-DHsDF5w-.js} +3 -3
  61. package/dist/ui/assets/{scss-DDpW3yTd.js → scss-BxtyHZ1p.js} +1 -1
  62. package/dist/ui/assets/{shellscript-DayAQ-W9.js → shellscript-BY_YYrHT.js} +1 -1
  63. package/dist/ui/assets/{shellsession-jPr_lSnA.js → shellsession-C6uBrLmA.js} +1 -1
  64. package/dist/ui/assets/{soy-X9adks8G.js → soy-BHM9gdPh.js} +1 -1
  65. package/dist/ui/assets/{sql-Bmt09AIR.js → sql-XmhfEGz_.js} +1 -1
  66. package/dist/ui/assets/{stata-DVkNpH8r.js → stata-BbHY7NEo.js} +1 -1
  67. package/dist/ui/assets/{surrealql-eEXzMb8j.js → surrealql-CD49DZ3E.js} +1 -1
  68. package/dist/ui/assets/{svelte-CFoBxZF3.js → svelte-CbYsEYJY.js} +1 -1
  69. package/dist/ui/assets/{templ-Da_324JF.js → templ-B5JoUxEB.js} +1 -1
  70. package/dist/ui/assets/{tex-HAzlmKJ0.js → tex-DZcOfTZK.js} +1 -1
  71. package/dist/ui/assets/{ts-tags-7iLrNH8p.js → ts-tags-BeU-xqc0.js} +1 -1
  72. package/dist/ui/assets/{tsx-gZaXf8PJ.js → tsx-V7W1xGl3.js} +1 -1
  73. package/dist/ui/assets/{twig-CLpQ7p1N.js → twig-MIaJo5BU.js} +1 -1
  74. package/dist/ui/assets/{typescript-B2WWCRI2.js → typescript-C0WESuVc.js} +1 -1
  75. package/dist/ui/assets/{vue-No_8Jygd.js → vue-eJHwc9ra.js} +1 -1
  76. package/dist/ui/assets/{vue-html-kHJg98Kk.js → vue-html-CLmBujTU.js} +1 -1
  77. package/dist/ui/assets/{vue-vine-Df4cEYaN.js → vue-vine-BP5kjIGb.js} +1 -1
  78. package/dist/ui/assets/{workspace-app-C_wsC6VL.js → workspace-app-C1qR5cAY.js} +3 -3
  79. package/dist/ui/assets/{xml-DUZ05dpG.js → xml-j7pmyP48.js} +1 -1
  80. package/dist/ui/assets/{xsl-Bwp3w2OF.js → xsl-5VFHbg6o.js} +1 -1
  81. package/dist/ui/assets/{yaml-IA_-TEM1.js → yaml-DxFAt-KU.js} +1 -1
  82. package/dist/ui/workspace-app.html +1 -1
  83. package/docs/chatgpt-coding-workflow.md +12 -1
  84. package/docs/configuration.md +3 -3
  85. package/docs/debugging.md +4 -4
  86. package/docs/security.md +15 -0
  87. package/package.json +2 -2
  88. package/scripts/debug/accept.mjs +41 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.3] - 2026-08-09
8
+
9
+ ### Changed
10
+
11
+ - Shell tools no longer carry a blanket prohibition against commands that modify files. `bash` and Codex `exec_command` may update ordinary project files when that is a natural part of the user's requested development task, including package managers, generators, and formatters.
12
+
13
+ ### Security
14
+
15
+ - The Agent shell contract continues to prohibit mutation of security- or privilege-sensitive operating-system files and credential material such as `/etc/sudoers`, `/etc/passwd`, `/etc/shadow`, authentication policy, and SSH private keys; configuration-file changes through shell require an explicit user request.
16
+
17
+ ## [0.2.2] - 2026-08-09
18
+
19
+ ### Added
20
+
21
+ - First-class `rename` and `delete` MCP tools for files and directories in workspace or OS-temp file roots, including direct availability in minimal, full, and Codex tool modes.
22
+
23
+ ### Security
24
+
25
+ - `rename` validates both source and destination canonical roots and refuses existing destinations; `delete` refuses allowed roots themselves and requires explicit `recursive: true` for non-empty directory trees.
26
+
7
27
  ## [0.2.1] - 2026-08-09
8
28
 
9
29
  ### Changed
@@ -0,0 +1,50 @@
1
+ import { lstat, realpath, rename, rm, rmdir, unlink } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { expandHomePath, resolveCanonicalAllowedPath } from "./roots.js";
4
+ export async function renamePath(input, context) {
5
+ const source = await resolveCanonicalAllowedPath(input.path, context.cwd, context.allowedRoots);
6
+ await assertNotAllowedRootItself(source, context.allowedRoots, input.path);
7
+ const destination = await resolveCanonicalAllowedPath(input.newPath, context.cwd, context.allowedRoots);
8
+ await assertDestinationMissing(destination, input.newPath);
9
+ await lstat(source);
10
+ await rename(source, destination);
11
+ return { path: input.path, newPath: input.newPath };
12
+ }
13
+ export async function deletePath(input, context) {
14
+ const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.allowedRoots);
15
+ await assertNotAllowedRootItself(path, context.allowedRoots, input.path);
16
+ const entry = await lstat(path);
17
+ const recursive = input.recursive ?? false;
18
+ if (entry.isDirectory()) {
19
+ if (recursive) {
20
+ await rm(path, { recursive: true, force: false });
21
+ }
22
+ else {
23
+ await rmdir(path);
24
+ }
25
+ }
26
+ else {
27
+ await unlink(path);
28
+ }
29
+ return { path: input.path, recursive };
30
+ }
31
+ async function assertDestinationMissing(path, inputPath) {
32
+ try {
33
+ await lstat(path);
34
+ }
35
+ catch (error) {
36
+ if (error.code === "ENOENT")
37
+ return;
38
+ throw error;
39
+ }
40
+ throw new Error(`Rename destination already exists: ${inputPath}`);
41
+ }
42
+ async function assertNotAllowedRootItself(path, allowedRoots, inputPath) {
43
+ const canonicalPath = await realpath(path);
44
+ for (const root of allowedRoots) {
45
+ const canonicalRoot = await realpath(resolve(expandHomePath(root)));
46
+ if (canonicalPath === canonicalRoot) {
47
+ throw new Error(`Cannot rename or delete an allowed root itself: ${inputPath}`);
48
+ }
49
+ }
50
+ }
@@ -4,11 +4,16 @@ export const toolNames = {
4
4
  read: "read",
5
5
  write: "write",
6
6
  edit: "edit",
7
+ rename: "rename",
8
+ delete: "delete",
7
9
  grep: "grep",
8
10
  glob: "glob",
9
11
  ls: "ls",
10
12
  shell: "bash",
11
13
  };
14
+ export function buildShellMutationPolicy() {
15
+ return "Shell commands may modify ordinary project files when that is a natural part of the user's requested development task. Never use shell commands to modify security- or privilege-sensitive operating-system files or credential material such as /etc/sudoers, /etc/passwd, /etc/shadow, PAM or authentication policy, SSH private keys, or equivalent privileged system files. Modify configuration files through shell only when the user's request explicitly calls for that configuration change; do not infer permission merely because changing configuration would be convenient.";
16
+ }
12
17
  export function buildServerInstructions(config, context = {}) {
13
18
  return joinInstructions(capabilityContractInstructions(config, context), selectedWorkflowInstructions(config), config.appendInstructions);
14
19
  }
@@ -19,24 +24,28 @@ export function buildToolDescriptions(config) {
19
24
  const shellSurface = config.toolMode === "minimal"
20
25
  ? ` In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled, so shell commands may be used for equivalent search and directory inspection.`
21
26
  : "";
27
+ const shellMutationPolicy = buildShellMutationPolicy();
22
28
  return {
23
29
  read: `Read a file inside an open workspace or the OS temp directory. Instruction files returned by ${toolNames.openWorkspace} and advertised skill files are also readable when applicable.${skillCapability} Call ${toolNames.openWorkspace} first and pass workspaceId.`,
24
30
  write: `Create or completely overwrite a file inside an open workspace or the OS temp directory. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
25
31
  edit: `Edit one file inside an open workspace or the OS temp directory by replacing exact text blocks. Each oldText must match a unique, non-overlapping region of the original file. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
32
+ rename: `Rename or move one file or directory inside an open workspace or the OS temp directory without overwriting an existing destination. Source and destination must both remain inside the permitted file roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
33
+ delete: `Delete one file or directory inside an open workspace or the OS temp directory. Non-empty directories require recursive=true. An allowed root itself cannot be deleted. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
26
34
  applyPatch: `Apply one Codex-style patch inside an open workspace or the OS temp directory. Supports adding, overwriting, updating, deleting, and moving files. Workspace paths must remain relative; absolute paths are accepted only inside the OS temp directory. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
27
- shell: `Run a shell command inside an open workspace.${shellSurface} Commands execute with the local user's authority; workspace filesystem containment does not make shell execution a sandbox. Do not use ${toolNames.shell} to create or modify project files; use ${toolNames.edit} or ${toolNames.write} for file changes. Call ${toolNames.openWorkspace} first and pass workspaceId. This capability should only be exposed behind strong authentication.`,
35
+ shell: `Run a shell command inside an open workspace.${shellSurface} Commands execute with the local user's authority; workspace filesystem containment does not make shell execution a sandbox. ${shellMutationPolicy} Call ${toolNames.openWorkspace} first and pass workspaceId. This capability should only be exposed behind strong authentication.`,
28
36
  shellCommand: "Shell command to run with the local user's authority.",
29
37
  };
30
38
  }
31
39
  function capabilityContractInstructions(config, context) {
32
40
  const workspaceLifecycle = config.toolMode === "codex"
33
41
  ? `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout and reuse its workspaceId. Only open mode=\"worktree\" when the user explicitly asks for isolated or parallel work. Managed worktrees use dedicated forgerelay/* branches, not detached HEADs. When work in a managed worktree is complete and verified, call ${toolNames.closeWorktree}; it commits remaining worktree changes, fast-forwards the original target branch only when safe, then removes the worktree and its branch. If the target branch diverged or the source checkout is dirty, closing is refused and the worktree is preserved.`
34
- : `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout and reuse its workspaceId for later file, search, edit, write, show-changes, and shell tools. Only open mode=\"worktree\" when the user explicitly asks for isolated or parallel work. Managed worktrees use dedicated forgerelay/* branches, not detached HEADs. When work in a managed worktree is complete and verified, call ${toolNames.closeWorktree}; it commits remaining worktree changes, fast-forwards the original target branch only when safe, then removes the worktree and its branch. If the target branch diverged or the source checkout is dirty, closing is refused and the worktree is preserved.`;
42
+ : `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout and reuse its workspaceId for later file, search, edit, write, rename, delete, show-changes, and shell tools. Only open mode=\"worktree\" when the user explicitly asks for isolated or parallel work. Managed worktrees use dedicated forgerelay/* branches, not detached HEADs. When work in a managed worktree is complete and verified, call ${toolNames.closeWorktree}; it commits remaining worktree changes, fast-forwards the original target branch only when safe, then removes the worktree and its branch. If the target branch diverged or the source checkout is dirty, closing is refused and the worktree is preserved.`;
35
43
  const agents = `Follow instructions returned by ${toolNames.openWorkspace}. Before working under a path listed in availableAgentsFiles, use ${toolNames.read} to inspect that instruction file and follow it.`;
36
44
  const skills = config.skillsEnabled
37
45
  ? `When ${toolNames.openWorkspace} returns available skills and a task matches a skill, use ${toolNames.read} to read that skill's path before proceeding. Skill paths may be outside the workspace, but ${toolNames.read} only permits advertised SKILL.md files and files under already-loaded skill directories.`
38
46
  : "";
39
47
  const toolSurface = toolSurfaceInstructions(config);
48
+ const shellMutationPolicy = buildShellMutationPolicy();
40
49
  const hooks = "When a ForgeRelay tool result reports Hook results, tell the user which meaningful hooks ran and whether they passed or blocked the operation. Do not claim the requested operation succeeded when a blocking hook prevented it.";
41
50
  const artifact = config.artifactsEnabled && context.artifactDownloadSupported
42
51
  ? "When the user supplies or generates a file that is not present on the ForgeRelay host, use download_artifact with its native file value, the existing workspace ID, and a suitable relative destination path chosen from the user's request and project structure. The tool refuses to overwrite an existing destination and returns the normalized workspace-relative path. Use normal workspace tools when explicit inspection, replacement, movement, renaming, or deletion is needed. Do not recreate binary files with write/edit calls or place signed URLs, native file objects, base64 content, or invented host paths in shell commands or logs."
@@ -44,11 +53,11 @@ function capabilityContractInstructions(config, context) {
44
53
  const showChanges = config.widgets === "changes"
45
54
  ? "If the turn successfully modifies files by creating, editing, overwriting, deleting, moving, or applying patches, call show_changes exactly once for that workspace after the final related file change and before your final response so the user can inspect the aggregate diff for that turn. Do not call it after every individual file change; do not skip it because individual file-change tools already returned diffs."
46
55
  : "";
47
- return joinInstructions(workspaceLifecycle, agents, skills, toolSurface, hooks, artifact, showChanges);
56
+ return joinInstructions(workspaceLifecycle, agents, skills, toolSurface, shellMutationPolicy, hooks, artifact, showChanges);
48
57
  }
49
58
  function toolSurfaceInstructions(config) {
50
59
  if (config.toolMode === "codex") {
51
- return `In codex tool mode, workspace file and command operations use ${toolNames.read}, apply_patch, exec_command, and write_stdin.`;
60
+ return `In codex tool mode, workspace file and command operations use ${toolNames.read}, ${toolNames.rename}, ${toolNames.delete}, apply_patch, exec_command, and write_stdin.`;
52
61
  }
53
62
  if (config.toolMode === "full") {
54
63
  return `In full tool mode, dedicated ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} inspection tools are available alongside the core workspace tools.`;
@@ -64,12 +73,12 @@ function selectedWorkflowInstructions(config) {
64
73
  }
65
74
  function defaultWorkflowInstructions(config) {
66
75
  if (config.toolMode === "codex") {
67
- return `Use ${toolNames.read} for direct file reads, apply_patch for all file modifications, exec_command for inspection, tests, builds, and other commands, and write_stdin to poll or interact with running processes.`;
76
+ return `Use ${toolNames.read} for direct file reads, ${toolNames.rename} and ${toolNames.delete} for direct path moves or removals, apply_patch for content modifications, exec_command for inspection, tests, builds, and other commands, and write_stdin to poll or interact with running processes.`;
68
77
  }
69
78
  const inspection = config.toolMode === "full"
70
79
  ? `Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection.`
71
80
  : `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
72
- return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted modifications, ${toolNames.write} only for new files or complete rewrites, and ${toolNames.shell} for tests, builds, git inspection, package scripts, and commands that are better executed by the shell.`, `Do not create or modify files with ${toolNames.shell}; avoid shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or any command whose purpose is to write project files.`);
81
+ return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted content modifications, ${toolNames.write} only for new files or complete rewrites, ${toolNames.rename} for path moves, ${toolNames.delete} for removals, and ${toolNames.shell} for tests, builds, git inspection, package scripts, generators, formatters, and commands that are better executed by the shell.`);
73
82
  }
74
83
  function joinInstructions(...parts) {
75
84
  return parts
package/dist/server.js CHANGED
@@ -14,10 +14,11 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@mode
14
14
  import express from "express";
15
15
  import * as z from "zod/v4";
16
16
  import { applyPatch } from "./apply-patch.js";
17
+ import { deletePath, renamePath } from "./file-mutations.js";
17
18
  import { isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
18
19
  import { loadConfig } from "./config.js";
19
20
  import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
20
- import { buildServerInstructions, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
21
+ import { buildServerInstructions, buildShellMutationPolicy, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
21
22
  import { createOpenAIIncomingArtifactAdapter, } from "./incoming-artifacts.js";
22
23
  import { logEvent, requestIp, requestPath, commandPreview, sessionIdPrefix, workspaceLogLabel, } from "./logger.js";
23
24
  import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, runShellTool, writeFileTool, } from "./pi-tools.js";
@@ -369,7 +370,7 @@ function toolResultIsError(result) {
369
370
  function registerCodexProcessTools(server, config, workspaces, processSessions, hooks) {
370
371
  registerAppTool(server, "exec_command", {
371
372
  title: "Execute command",
372
- description: "Run a command inside an open workspace. Returns its result when it exits during the yield window, otherwise returns a sessionId for write_stdin. Use this for file inspection, tests, builds, package scripts, and long-running processes. Call open_workspace first and pass workspaceId.",
373
+ description: `Run a command inside an open workspace. Returns its result when it exits during the yield window, otherwise returns a sessionId for write_stdin. Use this for file inspection, tests, builds, package scripts, generators, formatters, and long-running processes. ${buildShellMutationPolicy()} Call open_workspace first and pass workspaceId.`,
373
374
  inputSchema: {
374
375
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
375
376
  cmd: z.string().min(1).describe("Shell command to execute."),
@@ -1069,6 +1070,148 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1069
1070
  });
1070
1071
  });
1071
1072
  }
1073
+ registerAppTool(server, toolNames.rename, {
1074
+ title: "Rename path",
1075
+ description: toolDescriptions.rename,
1076
+ inputSchema: {
1077
+ workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
1078
+ path: z.string().describe("Source file or directory path relative to the workspace root, or absolute inside the OS temp directory."),
1079
+ newPath: z.string().describe("Destination path relative to the workspace root, or absolute inside the OS temp directory. The destination must not already exist."),
1080
+ },
1081
+ outputSchema: resultOutputSchema({
1082
+ status: z.literal("renamed"),
1083
+ path: z.string(),
1084
+ newPath: z.string(),
1085
+ }),
1086
+ ...toolWidgetDescriptorMeta(config, "edit"),
1087
+ annotations: EDIT_TOOL_ANNOTATIONS,
1088
+ }, async ({ workspaceId, path, newPath }, extra) => {
1089
+ const workspace = workspaces.getWorkspace(workspaceId);
1090
+ return runToolWithHooks(hooks, {
1091
+ tool: toolNames.rename,
1092
+ invocation: workspaceHookInvocation(workspace),
1093
+ payload: { path, newPath, paths: [path, newPath] },
1094
+ changedPaths: () => [path, newPath],
1095
+ operation: async () => {
1096
+ const startedAt = performance.now();
1097
+ try {
1098
+ await renamePath({ path, newPath }, {
1099
+ cwd: workspace.root,
1100
+ allowedRoots: workspaces.fileToolRoots(workspace),
1101
+ });
1102
+ const result = `Renamed ${path} to ${newPath}.`;
1103
+ const content = [textBlock(result)];
1104
+ logToolCall(config, {
1105
+ tool: toolNames.rename,
1106
+ ...workspaceLogContext(workspace, extra.sessionId),
1107
+ path: `${path} -> ${newPath}`,
1108
+ success: true,
1109
+ durationMs: Math.round(performance.now() - startedAt),
1110
+ });
1111
+ return {
1112
+ content,
1113
+ _meta: {
1114
+ tool: toolNames.rename,
1115
+ card: {
1116
+ workspaceId,
1117
+ path: newPath,
1118
+ summary: { previousPath: path },
1119
+ payload: { content },
1120
+ },
1121
+ },
1122
+ structuredContent: {
1123
+ result,
1124
+ status: "renamed",
1125
+ path,
1126
+ newPath,
1127
+ },
1128
+ };
1129
+ }
1130
+ catch (error) {
1131
+ logToolCall(config, {
1132
+ tool: toolNames.rename,
1133
+ ...workspaceLogContext(workspace, extra.sessionId),
1134
+ path: `${path} -> ${newPath}`,
1135
+ success: false,
1136
+ durationMs: Math.round(performance.now() - startedAt),
1137
+ error: error instanceof Error ? error.message : String(error),
1138
+ });
1139
+ throw error;
1140
+ }
1141
+ },
1142
+ });
1143
+ });
1144
+ registerAppTool(server, toolNames.delete, {
1145
+ title: "Delete path",
1146
+ description: toolDescriptions.delete,
1147
+ inputSchema: {
1148
+ workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
1149
+ path: z.string().describe("File or directory path relative to the workspace root, or absolute inside the OS temp directory."),
1150
+ recursive: z.boolean().optional().describe("Delete a non-empty directory tree. Defaults to false."),
1151
+ },
1152
+ outputSchema: resultOutputSchema({
1153
+ status: z.literal("deleted"),
1154
+ path: z.string(),
1155
+ recursive: z.boolean(),
1156
+ }),
1157
+ ...toolWidgetDescriptorMeta(config, "edit"),
1158
+ annotations: EDIT_TOOL_ANNOTATIONS,
1159
+ }, async ({ workspaceId, path, recursive }, extra) => {
1160
+ const workspace = workspaces.getWorkspace(workspaceId);
1161
+ return runToolWithHooks(hooks, {
1162
+ tool: toolNames.delete,
1163
+ invocation: workspaceHookInvocation(workspace),
1164
+ payload: { path, recursive: recursive ?? false },
1165
+ changedPaths: () => [path],
1166
+ operation: async () => {
1167
+ const startedAt = performance.now();
1168
+ try {
1169
+ const deleted = await deletePath({ path, recursive }, {
1170
+ cwd: workspace.root,
1171
+ allowedRoots: workspaces.fileToolRoots(workspace),
1172
+ });
1173
+ const result = `Deleted ${path}${deleted.recursive ? " recursively" : ""}.`;
1174
+ const content = [textBlock(result)];
1175
+ logToolCall(config, {
1176
+ tool: toolNames.delete,
1177
+ ...workspaceLogContext(workspace, extra.sessionId),
1178
+ path,
1179
+ success: true,
1180
+ durationMs: Math.round(performance.now() - startedAt),
1181
+ });
1182
+ return {
1183
+ content,
1184
+ _meta: {
1185
+ tool: toolNames.delete,
1186
+ card: {
1187
+ workspaceId,
1188
+ path,
1189
+ summary: { recursive: deleted.recursive },
1190
+ payload: { content },
1191
+ },
1192
+ },
1193
+ structuredContent: {
1194
+ result,
1195
+ status: "deleted",
1196
+ path,
1197
+ recursive: deleted.recursive,
1198
+ },
1199
+ };
1200
+ }
1201
+ catch (error) {
1202
+ logToolCall(config, {
1203
+ tool: toolNames.delete,
1204
+ ...workspaceLogContext(workspace, extra.sessionId),
1205
+ path,
1206
+ success: false,
1207
+ durationMs: Math.round(performance.now() - startedAt),
1208
+ error: error instanceof Error ? error.message : String(error),
1209
+ });
1210
+ throw error;
1211
+ }
1212
+ },
1213
+ });
1214
+ });
1072
1215
  if (config.toolMode === "codex") {
1073
1216
  registerAppTool(server, "apply_patch", {
1074
1217
  title: "Apply patch",