@akira-tl/forgerelay 0.2.2 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ 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
+
7
17
  ## [0.2.2] - 2026-08-09
8
18
 
9
19
  ### Added
@@ -11,6 +11,9 @@ export const toolNames = {
11
11
  ls: "ls",
12
12
  shell: "bash",
13
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
+ }
14
17
  export function buildServerInstructions(config, context = {}) {
15
18
  return joinInstructions(capabilityContractInstructions(config, context), selectedWorkflowInstructions(config), config.appendInstructions);
16
19
  }
@@ -21,6 +24,7 @@ export function buildToolDescriptions(config) {
21
24
  const shellSurface = config.toolMode === "minimal"
22
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.`
23
26
  : "";
27
+ const shellMutationPolicy = buildShellMutationPolicy();
24
28
  return {
25
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.`,
26
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.`,
@@ -28,7 +32,7 @@ export function buildToolDescriptions(config) {
28
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.`,
29
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.`,
30
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.`,
31
- 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, move, rename, or delete project files; use ${toolNames.edit}, ${toolNames.write}, ${toolNames.rename}, or ${toolNames.delete} 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.`,
32
36
  shellCommand: "Shell command to run with the local user's authority.",
33
37
  };
34
38
  }
@@ -41,6 +45,7 @@ function capabilityContractInstructions(config, context) {
41
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.`
42
46
  : "";
43
47
  const toolSurface = toolSurfaceInstructions(config);
48
+ const shellMutationPolicy = buildShellMutationPolicy();
44
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.";
45
50
  const artifact = config.artifactsEnabled && context.artifactDownloadSupported
46
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."
@@ -48,7 +53,7 @@ function capabilityContractInstructions(config, context) {
48
53
  const showChanges = config.widgets === "changes"
49
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."
50
55
  : "";
51
- return joinInstructions(workspaceLifecycle, agents, skills, toolSurface, hooks, artifact, showChanges);
56
+ return joinInstructions(workspaceLifecycle, agents, skills, toolSurface, shellMutationPolicy, hooks, artifact, showChanges);
52
57
  }
53
58
  function toolSurfaceInstructions(config) {
54
59
  if (config.toolMode === "codex") {
@@ -73,7 +78,7 @@ function defaultWorkflowInstructions(config) {
73
78
  const inspection = config.toolMode === "full"
74
79
  ? `Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection.`
75
80
  : `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
76
- 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, 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.`);
77
82
  }
78
83
  function joinInstructions(...parts) {
79
84
  return parts
package/dist/server.js CHANGED
@@ -18,7 +18,7 @@ import { deletePath, renamePath } from "./file-mutations.js";
18
18
  import { isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
19
19
  import { loadConfig } from "./config.js";
20
20
  import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
21
- import { buildServerInstructions, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
21
+ import { buildServerInstructions, buildShellMutationPolicy, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
22
22
  import { createOpenAIIncomingArtifactAdapter, } from "./incoming-artifacts.js";
23
23
  import { logEvent, requestIp, requestPath, commandPreview, sessionIdPrefix, workspaceLogLabel, } from "./logger.js";
24
24
  import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, runShellTool, writeFileTool, } from "./pi-tools.js";
@@ -370,7 +370,7 @@ function toolResultIsError(result) {
370
370
  function registerCodexProcessTools(server, config, workspaces, processSessions, hooks) {
371
371
  registerAppTool(server, "exec_command", {
372
372
  title: "Execute command",
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, 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.`,
374
374
  inputSchema: {
375
375
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
376
376
  cmd: z.string().min(1).describe("Shell command to execute."),
@@ -172,6 +172,14 @@ Experimental `FORGERELAY_TOOL_MODE=codex` provides a smaller Codex-shaped
172
172
  surface including direct `rename`/`delete` path mutations alongside `apply_patch`,
173
173
  `exec_command`, and `write_stdin`.
174
174
 
175
+ Shell commands are allowed to modify ordinary project files when that is a
176
+ natural part of the user's requested development task; ForgeRelay does not apply
177
+ a blanket ban to package managers, generators, formatters, or similar commands
178
+ that write files. The Agent contract still prohibits shell mutation of
179
+ security- or privilege-sensitive operating-system files and credential material,
180
+ and requires an explicit user request before changing configuration files
181
+ through `bash` or `exec_command`.
182
+
175
183
  ## Change review UI
176
184
 
177
185
  By default `FORGERELAY_WIDGETS=full` attaches ChatGPT Apps-compatible UI to the
package/docs/debugging.md CHANGED
@@ -58,8 +58,8 @@ The acceptance checks:
58
58
  2. OAuth protected-resource and authorization-server discovery;
59
59
  3. unauthenticated `/mcp` rejection;
60
60
  4. dynamic OAuth client registration, PKCE Owner-password approval, and access-token exchange;
61
- 5. MCP `initialize`, including package/server version consistency;
62
- 6. `tools/list` for the full debug tool surface;
61
+ 5. MCP `initialize`, including package/server version consistency and the shell mutation safety contract;
62
+ 6. `tools/list` for the full debug tool surface, including the non-blanket `bash` mutation policy;
63
63
  7. a real checkout workspace with `write`, `read`, `rename`, `delete`, `bash`, and a deliberate failed `edit`;
64
64
  8. OS temp-directory `write` → `read` → `edit` → `rename` → `delete` over the same real MCP session, plus rejection of an arbitrary path outside the workspace/temp roots;
65
65
  9. a temporary Git repository with managed worktree creation, file modification, and `close_worktree`;
package/docs/security.md CHANGED
@@ -97,6 +97,21 @@ repositories, local services, and other resources outside a narrow workspace
97
97
  sandbox, and MCP currently does not provide a clean universal interaction model
98
98
  for dynamically crossing such a boundary.
99
99
 
100
+ The Agent-facing shell contract therefore does not ban every command that can
101
+ change files. Commands may modify ordinary project files when that is a natural
102
+ part of the user's requested development task, including package managers,
103
+ generators, formatters, and similar tooling. The contract does prohibit shell
104
+ mutation of security- or privilege-sensitive operating-system files and
105
+ credential material such as `/etc/sudoers`, `/etc/passwd`, `/etc/shadow`, PAM or
106
+ authentication policy, SSH private keys, and equivalent privileged targets.
107
+ Configuration files may be changed through shell only when the user's request
108
+ explicitly calls for that configuration change rather than merely making it a
109
+ convenient implementation detail.
110
+
111
+ This is an Agent execution policy, not an OS-level sandbox or command parser.
112
+ Operators that need a stronger project-specific runtime gate can use blocking
113
+ `BeforeTool` Hooks around `bash` or `exec_command`.
114
+
100
115
  The security model is therefore based on:
101
116
 
102
117
  - strong authentication;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",
@@ -83,6 +83,11 @@ try {
83
83
  assert.equal(initialized.message.result.serverInfo.name, "forgerelay");
84
84
  assert.equal(initialized.message.result.serverInfo.title, "ForgeRelay");
85
85
  assert.equal(initialized.message.result.serverInfo.version, packageJson.version);
86
+ const serverInstructions = initialized.message.result.instructions ?? "";
87
+ assert.match(serverInstructions, /Shell commands may modify ordinary project files/);
88
+ assert.match(serverInstructions, /\/etc\/sudoers/);
89
+ assert.match(serverInstructions, /configuration files through shell only when the user's request explicitly calls for that configuration change/);
90
+ assert.doesNotMatch(serverInstructions, /Do not create or modify files with bash/);
86
91
  pass(
87
92
  "MCP initialize",
88
93
  JSON.stringify(initialized.message.result.serverInfo),
@@ -106,6 +111,10 @@ try {
106
111
  for (const expected of ["open_workspace", "close_worktree", "read", "write", "edit", "rename", "delete", "grep", "glob", "ls", "bash"]) {
107
112
  assert.ok(toolNames.includes(expected), `missing debug tool ${expected}`);
108
113
  }
114
+ const bashTool = tools.find((tool) => tool.name === "bash");
115
+ assert.match(bashTool?.description ?? "", /may modify ordinary project files/);
116
+ assert.match(bashTool?.description ?? "", /\/etc\/sudoers/);
117
+ assert.doesNotMatch(bashTool?.description ?? "", /Do not use bash to create, move, rename, or delete project files/);
109
118
  pass("MCP tools/list", `${toolNames.length} tools: ${toolNames.join(", ")}`);
110
119
 
111
120
  const opened = callTool(oauth.accessToken, sessionId, 3, "open_workspace", {