@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 +10 -0
- package/dist/mcp/server-instructions.js +8 -3
- package/dist/server.js +2 -2
- package/docs/chatgpt-coding-workflow.md +8 -0
- package/docs/debugging.md +2 -2
- package/docs/security.md +15 -0
- package/package.json +1 -1
- package/scripts/debug/accept.mjs +9 -0
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.
|
|
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
|
|
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:
|
|
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
package/scripts/debug/accept.mjs
CHANGED
|
@@ -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", {
|