@akira-tl/forgerelay 0.9.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/dist/cli/init.js +72 -1
- package/dist/cli/shell/setup.js +99 -0
- package/dist/cli.js +43 -10
- package/dist/mcp/hooks/hooks.js +6 -3
- package/dist/mcp/process/process-platform.js +24 -26
- package/dist/mcp/process/process-sessions.js +6 -3
- package/dist/mcp/server/transport/http-server.js +4 -1
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +6 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +1 -1
- package/dist/mcp/server-instructions.js +10 -1
- package/dist/runtime/config/config.js +6 -0
- package/dist/runtime/instructions/shell-instructions.js +100 -0
- package/dist/runtime/security/runtime-privilege.js +120 -0
- package/dist/runtime/shell/command-shell-runtime.js +235 -0
- package/dist/ui/.vite/manifest.json +276 -276
- package/dist/ui/activity-panel-app.html +2 -2
- package/dist/ui/assets/{activity-panel-app-Sl1Jro8v.js → activity-panel-app-BkODhDLg.js} +1 -1
- package/dist/ui/assets/{activity-panel-app-7Xe0alhm.css → activity-panel-app-DkfJzTZ3.css} +1 -1
- package/dist/ui/assets/{angular-html-B_yn91wW.js → angular-html-DjuELMdF.js} +1 -1
- package/dist/ui/assets/{angular-ts-DSgtloKz.js → angular-ts-2EXVZIIX.js} +1 -1
- package/dist/ui/assets/{apl-NatWtF8X.js → apl-Bb4IeT5V.js} +1 -1
- package/dist/ui/assets/{astro-BYFEVBuN.js → astro-Bjavu3nT.js} +1 -1
- package/dist/ui/assets/{blade-DoO7yvMR.js → blade-DKW2uHZo.js} +1 -1
- package/dist/ui/assets/{c-Rl2hMCcg.js → c-oLeHmzbj.js} +1 -1
- package/dist/ui/assets/{cobol-ByC5mWiB.js → cobol-CcqFMr5T.js} +1 -1
- package/dist/ui/assets/{coffee-Dez77Qov.js → coffee-BA20zqRF.js} +1 -1
- package/dist/ui/assets/{cpp-DtaAQT9h.js → cpp-D750WmDa.js} +1 -1
- package/dist/ui/assets/{crystal-Bgmbf8ec.js → crystal-Cv5I6OMI.js} +1 -1
- package/dist/ui/assets/{css-C1tjBtVN.js → css-CjPPWPeH.js} +1 -1
- package/dist/ui/assets/{edge-DeH6u8Wg.js → edge-aYRuudEZ.js} +1 -1
- package/dist/ui/assets/{elixir-_H3KnmWT.js → elixir-ChLitmz7.js} +1 -1
- package/dist/ui/assets/{elm-CsCe3U28.js → elm-Fs7L7u36.js} +1 -1
- package/dist/ui/assets/{erb-CbM-L2g8.js → erb-B-qXHxN1.js} +1 -1
- package/dist/ui/assets/{git-rebase-D0s5pDh9.js → git-rebase-DOVN5OEg.js} +1 -1
- package/dist/ui/assets/{glimmer-js-BfOToq_A.js → glimmer-js-BFt_vCTW.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-CCUOkL2K.js → glimmer-ts-ufCHlR6C.js} +1 -1
- package/dist/ui/assets/{glsl-DH4ec9dE.js → glsl-Ex4JXGuC.js} +1 -1
- package/dist/ui/assets/{graphql-DilAMBoI.js → graphql-pQPXtBk5.js} +1 -1
- package/dist/ui/assets/{hack-BDkNDZlF.js → hack-B5hDguhd.js} +1 -1
- package/dist/ui/assets/{haml-D_QBp6K7.js → haml-BrNeI29U.js} +1 -1
- package/dist/ui/assets/{handlebars-DePUrTpC.js → handlebars-DCqTsL9-.js} +1 -1
- package/dist/ui/assets/{heavy-payload-DOwZpO6n.js → heavy-payload-DsmmHgvt.js} +3 -3
- package/dist/ui/assets/{html-By4dNnM4.js → html-D8k0CC9V.js} +1 -1
- package/dist/ui/assets/{html-derivative-DDw9Ef4Q.js → html-derivative-CC8flWmC.js} +1 -1
- package/dist/ui/assets/{http-B0QxWc9M.js → http-BYnWqNep.js} +1 -1
- package/dist/ui/assets/{hurl-vMQ4lda1.js → hurl-BND-z85s.js} +1 -1
- package/dist/ui/assets/{java-DYRe2lWG.js → java-Dug0oB9T.js} +1 -1
- package/dist/ui/assets/{javascript-CxyGqwcL.js → javascript-oDJ7WITC.js} +1 -1
- package/dist/ui/assets/{jinja-JUwV-fZt.js → jinja-Cv0t_F1U.js} +1 -1
- package/dist/ui/assets/{jison-7ZqCEvS8.js → jison-Car3MF55.js} +1 -1
- package/dist/ui/assets/{json-dbltgiiA.js → json-CEViCsf4.js} +1 -1
- package/dist/ui/assets/{jsx-oDopT7YQ.js → jsx-BfZ0zgSH.js} +1 -1
- package/dist/ui/assets/{julia-CkKiNSav.js → julia-CmX0b3Tn.js} +1 -1
- package/dist/ui/assets/{just-DuqCAKJ3.js → just-BuugLCVk.js} +1 -1
- package/dist/ui/assets/{latex-sudcRDSn.js → latex-FpTXqO03.js} +1 -1
- package/dist/ui/assets/{liquid-Di-08Js7.js → liquid-B37OzUFu.js} +1 -1
- package/dist/ui/assets/{lua-YlqdMoV0.js → lua-BHjcb8oq.js} +1 -1
- package/dist/ui/assets/{marko-CJRNyJDb.js → marko-CApj_fsH.js} +1 -1
- package/dist/ui/assets/{mdc-t12CzZHs.js → mdc-Ed5vQWq8.js} +1 -1
- package/dist/ui/assets/{nginx-57-b4XwT.js → nginx-u_rVEBox.js} +1 -1
- package/dist/ui/assets/{nim-BzrswYQa.js → nim-Ba9EdchT.js} +1 -1
- package/dist/ui/assets/{perl-D9JR_iIh.js → perl-BpU_6Wvz.js} +1 -1
- package/dist/ui/assets/{php-Py2gFHil.js → php-bKabofky.js} +1 -1
- package/dist/ui/assets/{pug-PzLLy0h1.js → pug-C_aLNkqe.js} +1 -1
- package/dist/ui/assets/{qml-By8fwgV4.js → qml-D9VisnRy.js} +1 -1
- package/dist/ui/assets/{r-CcezTbLq.js → r-CYqB1sLx.js} +1 -1
- package/dist/ui/assets/{razor-dG-vdhL_.js → razor-DNJKI8Rs.js} +1 -1
- package/dist/ui/assets/{regexp-BlsQ-N_8.js → regexp-DHrTHXkz.js} +1 -1
- package/dist/ui/assets/{rst-C2awDCat.js → rst-CbNdSFHv.js} +1 -1
- package/dist/ui/assets/{ruby-zA2W_49d.js → ruby-FyKG-pkF.js} +1 -1
- package/dist/ui/assets/{sas-D49bhyaN.js → sas-BbSPDl3X.js} +1 -1
- package/dist/ui/assets/{scss-BQb-cp01.js → scss-2nX9g6gD.js} +1 -1
- package/dist/ui/assets/{shellscript-Dga4H_Ni.js → shellscript-CmON6x2x.js} +1 -1
- package/dist/ui/assets/{shellsession-n3-5CqcF.js → shellsession-DtqIGlAX.js} +1 -1
- package/dist/ui/assets/{soy-XYBOf_BA.js → soy-BzUJMmJT.js} +1 -1
- package/dist/ui/assets/{sql-ET4u5Cpl.js → sql-CgVcFZmb.js} +1 -1
- package/dist/ui/assets/{stata-CLHcJHO_.js → stata-CNitlMot.js} +1 -1
- package/dist/ui/assets/{surrealql-BdjQ60T4.js → surrealql-BA8-xsRZ.js} +1 -1
- package/dist/ui/assets/{svelte-B2RPtKJe.js → svelte-El02yAxv.js} +1 -1
- package/dist/ui/assets/{templ-CrByPW_W.js → templ-DeUQ84aR.js} +1 -1
- package/dist/ui/assets/{tex-DZBkg363.js → tex-Cu-FqpPS.js} +1 -1
- package/dist/ui/assets/{ts-tags-DJR3Gphb.js → ts-tags-X42N6TGA.js} +1 -1
- package/dist/ui/assets/{tsx-CajzROyy.js → tsx-C1aPp9rY.js} +1 -1
- package/dist/ui/assets/{twig-BZjk5Zda.js → twig-Cj1Q4SQc.js} +1 -1
- package/dist/ui/assets/{typescript-B-EWToLj.js → typescript-Du9lVt1G.js} +1 -1
- package/dist/ui/assets/{vue-D_rHCCr8.js → vue-_uMGbFv8.js} +1 -1
- package/dist/ui/assets/{vue-html-CC4WlvVt.js → vue-html-D61naG-Q.js} +1 -1
- package/dist/ui/assets/{vue-vine-BgNMVgpk.js → vue-vine-CkFae4yV.js} +1 -1
- package/dist/ui/assets/{xml-BI86-WbH.js → xml-Cbwsc6ws.js} +1 -1
- package/dist/ui/assets/{xsl-DXeLZV4J.js → xsl-NyBx72XB.js} +1 -1
- package/dist/ui/assets/{yaml-60_PFlWa.js → yaml-Cgb6Uq9u.js} +1 -1
- package/dist/workspaces/context.js +32 -5
- package/dist/workspaces/presentation/workspace-presentation.js +1 -0
- package/dist/workspaces/sessions.js +2 -0
- package/dist/workspaces.js +2 -1
- package/docs/configuration.md +1 -1
- package/package.json +1 -1
- package/scripts/release/release-gate.test.mjs +1 -0
- package/scripts/release-proof.mjs +85 -9
- package/scripts/release-proof.test.mjs +79 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.10.0] - 2026-09-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added a first-class Command Shell Runtime identity with launch-shell/fallback selection, explicit pinned shell executable support, owner-visible diagnostics, and one shared shell contract for Agent commands and Hooks.
|
|
12
|
+
- Added editable ForgeRelay shell Instructions for PowerShell 7, Windows PowerShell 5.1, cmd, zsh, and fish. `forgerelay init` seeds exact-release-tag templates into the user config directory, preserves user edits, and projects loaded/disabled/unavailable state through the existing Workspace Instructions UI and context-delta path.
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
|
|
16
|
+
- ForgeRelay now refuses elevated root/Administrator/SYSTEM-equivalent runtime authority by default across supported platforms. Intentional elevated startup requires the invocation-scoped `--allow-elevated` acknowledgement and always exposes a mandatory Agent safety notice.
|
|
17
|
+
|
|
7
18
|
## [0.9.4] - 2026-09-04
|
|
8
19
|
|
|
9
20
|
### Added
|
package/README.md
CHANGED
|
@@ -222,7 +222,7 @@ Hook 是 ForgeRelay 的自动生命周期规则。首选方式是一个 Hook 一
|
|
|
222
222
|
}
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
-
命中 `BeforeTool`
|
|
225
|
+
命中 `BeforeTool` 后,普通 tag push 的 Hook 只快速验证 clean working tree(含 untracked)、tag 与 package version 一致,以及本地 tag 指向当前 HEAD;成功才继续原始 `git push`,失败则直接阻断。force release-tag push 始终拒绝。对于失败发布后显式执行的 `git push --delete origin vX.Y.Z`,Hook 仅在当前 HEAD 有有效 `release:verify` proof,且联网确认 npm 对应版本与 GitHub Release 都不存在时允许删除;任一不可撤回产物存在或远端状态无法确认都会拒绝。因此仅有 tag 时可以重建同版本,一旦 GitHub Release 或包仓库版本存在就进入不可变状态。tag 推送后由 GitHub Actions 的 Linux/macOS/Windows 矩阵执行权威验证,全部通过后才进入发布。Hook 结果会回到 Agent,Agent 应向用户说明重要 Hook 是否通过或阻断了操作。`report:false` 可以隐藏不重要的成功报告,但阻断失败始终可见。
|
|
226
226
|
|
|
227
227
|
旧的 inline `hooks` 和聚合 `hooks.json` 仍兼容;新配置建议都用独立 `hooks/*.json` 文件。
|
|
228
228
|
|
package/dist/cli/init.js
CHANGED
|
@@ -2,10 +2,13 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import * as prompts from "@clack/prompts";
|
|
3
3
|
import { publicEndpointUrl } from "../mcp/oauth/public-url.js";
|
|
4
4
|
import { expandHomePath } from "../mcp/filesystem/roots.js";
|
|
5
|
+
import { detectLauncherCommandShell, } from "../runtime/shell/command-shell-runtime.js";
|
|
6
|
+
import { seedShellInstructionFiles, shellInstructionFamiliesToSeed, } from "../runtime/instructions/shell-instructions.js";
|
|
5
7
|
import { installManagedLanguageServers, installedManagedLanguageServers, managedLanguageServerOptions, } from "../lsp/runtime/managed-language-servers.js";
|
|
6
8
|
import { generateInstanceId, generateOwnerToken, loadForgeRelayFiles, resolveSubagentsFlag, writeForgeRelayAuth, writeForgeRelayConfig, } from "../runtime/config/user-config.js";
|
|
7
9
|
import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, setupBindAddress, SetupCancelledError, textPrompt, validateHttpsProxyBaseUrls, validateLanClientFacingBaseUrls, validatePort, } from "./setup-support.js";
|
|
8
|
-
|
|
10
|
+
import { commandShellCompatibilityWarning, commandShellSetupOptions, customPinnedPreference, defaultCommandShellSetupChoice, followLauncherPreference, pinnedFamilyPreference, preservePinnedPreference, shellFamiliesForCustomSelection, } from "./shell/setup.js";
|
|
11
|
+
export async function runInit({ force, version }) {
|
|
9
12
|
const files = loadForgeRelayFiles();
|
|
10
13
|
if (!force && files.configExists && files.authExists) {
|
|
11
14
|
prompts.log.info(`ForgeRelay is already configured at ${files.dir}`);
|
|
@@ -116,6 +119,64 @@ export async function runInit({ force }) {
|
|
|
116
119
|
}
|
|
117
120
|
publicBaseUrl = compactPublicBaseUrlConfig(clientFacingBaseUrls);
|
|
118
121
|
}
|
|
122
|
+
const launcherShell = detectLauncherCommandShell();
|
|
123
|
+
const selectedShellChoice = await prompts.select({
|
|
124
|
+
message: "Which command shell should Agent commands and Hooks use?",
|
|
125
|
+
initialValue: defaultCommandShellSetupChoice(files.config.commandShell),
|
|
126
|
+
options: commandShellSetupOptions(process.platform, files.config.commandShell, launcherShell),
|
|
127
|
+
});
|
|
128
|
+
if (prompts.isCancel(selectedShellChoice))
|
|
129
|
+
throw new SetupCancelledError();
|
|
130
|
+
let commandShell;
|
|
131
|
+
const shellChoice = selectedShellChoice;
|
|
132
|
+
if (shellChoice === "follow-launcher") {
|
|
133
|
+
commandShell = followLauncherPreference(files.config.commandShell, launcherShell);
|
|
134
|
+
}
|
|
135
|
+
else if (shellChoice === "keep-pinned") {
|
|
136
|
+
commandShell = preservePinnedPreference(files.config.commandShell);
|
|
137
|
+
}
|
|
138
|
+
else if (shellChoice === "custom") {
|
|
139
|
+
const customFamily = await prompts.select({
|
|
140
|
+
message: "Which command language does the custom executable implement?",
|
|
141
|
+
options: shellFamiliesForCustomSelection(),
|
|
142
|
+
});
|
|
143
|
+
if (prompts.isCancel(customFamily))
|
|
144
|
+
throw new SetupCancelledError();
|
|
145
|
+
const executable = await textPrompt({
|
|
146
|
+
message: "What is the command-shell executable path?",
|
|
147
|
+
placeholder: process.platform === "win32" ? "D:\\Portable\\PowerShell\\pwsh.exe" : "/opt/custom/bin/zsh",
|
|
148
|
+
defaultValue: "",
|
|
149
|
+
validate: (value) => value?.trim() ? undefined : "Enter an executable path.",
|
|
150
|
+
});
|
|
151
|
+
commandShell = customPinnedPreference(customFamily, executable);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
commandShell = pinnedFamilyPreference(shellChoice);
|
|
155
|
+
}
|
|
156
|
+
const shellWarning = commandShellCompatibilityWarning(commandShell.family);
|
|
157
|
+
if (shellWarning)
|
|
158
|
+
prompts.note(shellWarning, "Command shell compatibility");
|
|
159
|
+
const shellInstructionFamilies = shellInstructionFamiliesToSeed(process.platform, commandShell.family);
|
|
160
|
+
let shellInstructions = files.config.shellInstructions;
|
|
161
|
+
let shellInstructionSeedResults = [];
|
|
162
|
+
if (shellInstructionFamilies.length > 0) {
|
|
163
|
+
const shellInstructionsAnswer = await prompts.confirm({
|
|
164
|
+
message: "Load ForgeRelay-provided extra Instructions for this command shell?",
|
|
165
|
+
initialValue: files.config.shellInstructions !== false,
|
|
166
|
+
});
|
|
167
|
+
if (prompts.isCancel(shellInstructionsAnswer))
|
|
168
|
+
throw new SetupCancelledError();
|
|
169
|
+
shellInstructions = shellInstructionsAnswer === true;
|
|
170
|
+
shellInstructionSeedResults = await seedShellInstructionFiles({
|
|
171
|
+
configDir: files.dir,
|
|
172
|
+
version,
|
|
173
|
+
families: shellInstructionFamilies,
|
|
174
|
+
});
|
|
175
|
+
const failedShellInstructions = shellInstructionSeedResults.filter((result) => result.status === "failed");
|
|
176
|
+
if (failedShellInstructions.length > 0) {
|
|
177
|
+
prompts.note(failedShellInstructions.map((result) => `${result.family}: ${result.error ?? "download failed"}\n${result.sourceUrl}`).join("\n\n"), "Shell Instructions unavailable");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
119
180
|
const installedManaged = installedManagedLanguageServers(files.dir);
|
|
120
181
|
const selectedManaged = await prompts.multiselect({
|
|
121
182
|
message: "Which Language Servers should ForgeRelay manage with npm?",
|
|
@@ -150,6 +211,7 @@ export async function runInit({ force }) {
|
|
|
150
211
|
}
|
|
151
212
|
}
|
|
152
213
|
const config = {
|
|
214
|
+
...files.config,
|
|
153
215
|
host,
|
|
154
216
|
port,
|
|
155
217
|
allowedRoots,
|
|
@@ -158,6 +220,8 @@ export async function runInit({ force }) {
|
|
|
158
220
|
trustedProxies,
|
|
159
221
|
workflowInstructions: files.config.workflowInstructions,
|
|
160
222
|
appendInstructions: files.config.appendInstructions,
|
|
223
|
+
commandShell,
|
|
224
|
+
shellInstructions,
|
|
161
225
|
subagents: resolveSubagentsFlag(files.config),
|
|
162
226
|
languageServers: files.config.languageServers,
|
|
163
227
|
allowAgentLanguageServerInstall,
|
|
@@ -173,6 +237,13 @@ export async function runInit({ force }) {
|
|
|
173
237
|
`Config: ${configPath}`,
|
|
174
238
|
`Auth: ${authPath}`,
|
|
175
239
|
`Bind: http://${config.host}:${config.port}`,
|
|
240
|
+
`Command shell: ${commandShell.mode} ${commandShell.family} (${commandShell.executable})`,
|
|
241
|
+
...(shellInstructionFamilies.length > 0
|
|
242
|
+
? [
|
|
243
|
+
`Shell Instructions: ${shellInstructions ? "enabled" : "disabled"}`,
|
|
244
|
+
...shellInstructionSeedResults.map((result) => `Shell Instructions ${result.family}: ${result.status} (${result.path})`),
|
|
245
|
+
]
|
|
246
|
+
: []),
|
|
176
247
|
...clientFacingBaseUrls.map((baseUrl, index) => `${index === 0 ? "Client-facing MCP URL" : "Client-facing MCP alias"}: ${publicEndpointUrl(baseUrl, "mcp").toString()}`),
|
|
177
248
|
];
|
|
178
249
|
if (networkMode === "ssh") {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { defaultCommandShellSelection, normalizeCommandShellSelection, } from "../../runtime/shell/command-shell-runtime.js";
|
|
2
|
+
export function commandShellSetupOptions(platform, current, launcher) {
|
|
3
|
+
const options = [];
|
|
4
|
+
if (current?.mode === "pinned") {
|
|
5
|
+
options.push({
|
|
6
|
+
value: "keep-pinned",
|
|
7
|
+
label: `Keep pinned ${current.family}`,
|
|
8
|
+
hint: current.executable,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
options.push({
|
|
12
|
+
value: "follow-launcher",
|
|
13
|
+
label: "Follow the shell used to launch ForgeRelay",
|
|
14
|
+
hint: launcher
|
|
15
|
+
? `Detected ${launcher.family ?? "shell"}: ${launcher.executable}`
|
|
16
|
+
: "Use the recorded fallback when no launcher shell can be identified.",
|
|
17
|
+
});
|
|
18
|
+
const families = platform === "win32"
|
|
19
|
+
? ["pwsh", "powershell", "cmd", "bash"]
|
|
20
|
+
: ["bash", "zsh", "fish", "sh"];
|
|
21
|
+
for (const family of families) {
|
|
22
|
+
options.push({
|
|
23
|
+
value: family,
|
|
24
|
+
label: shellFamilyLabel(family),
|
|
25
|
+
hint: family === "bash" && platform !== "win32"
|
|
26
|
+
? "Recommended compatibility path"
|
|
27
|
+
: undefined,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
options.push({
|
|
31
|
+
value: "custom",
|
|
32
|
+
label: "Custom executable path",
|
|
33
|
+
hint: "Pin a portable/green install or a shell-development test binary.",
|
|
34
|
+
});
|
|
35
|
+
return options;
|
|
36
|
+
}
|
|
37
|
+
export function defaultCommandShellSetupChoice(current) {
|
|
38
|
+
return current?.mode === "pinned" ? "keep-pinned" : "follow-launcher";
|
|
39
|
+
}
|
|
40
|
+
export function followLauncherPreference(current, launcher, platform = process.platform, environment = process.env) {
|
|
41
|
+
const fallback = launcher
|
|
42
|
+
?? (current?.mode === "follow-launcher" ? current : undefined)
|
|
43
|
+
?? defaultCommandShellSelection(platform === "win32" ? "cmd" : "bash", platform, environment);
|
|
44
|
+
const normalized = normalizeCommandShellSelection(fallback, platform, environment, true);
|
|
45
|
+
const family = normalized.family ?? fallback.family;
|
|
46
|
+
if (!family)
|
|
47
|
+
throw new Error(`Unable to identify command-shell family for ${normalized.executable}.`);
|
|
48
|
+
return {
|
|
49
|
+
mode: "follow-launcher",
|
|
50
|
+
family,
|
|
51
|
+
executable: normalized.executable,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function pinnedFamilyPreference(family, platform = process.platform, environment = process.env) {
|
|
55
|
+
const normalized = normalizeCommandShellSelection(defaultCommandShellSelection(family, platform, environment), platform, environment, true);
|
|
56
|
+
return {
|
|
57
|
+
mode: "pinned",
|
|
58
|
+
family,
|
|
59
|
+
executable: normalized.executable,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function customPinnedPreference(family, executable, platform = process.platform, environment = process.env) {
|
|
63
|
+
const normalized = normalizeCommandShellSelection({ family, executable }, platform, environment, true);
|
|
64
|
+
return {
|
|
65
|
+
mode: "pinned",
|
|
66
|
+
family,
|
|
67
|
+
executable: normalized.executable,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function preservePinnedPreference(current, platform = process.platform, environment = process.env) {
|
|
71
|
+
if (!current || current.mode !== "pinned") {
|
|
72
|
+
throw new Error("No pinned command-shell selection is available to preserve.");
|
|
73
|
+
}
|
|
74
|
+
return customPinnedPreference(current.family, current.executable, platform, environment);
|
|
75
|
+
}
|
|
76
|
+
export function commandShellCompatibilityWarning(family) {
|
|
77
|
+
if (family === "zsh") {
|
|
78
|
+
return "zsh is supported as an explicit command runtime, but Bash remains ForgeRelay's primary POSIX compatibility target. Agent commands and Hooks must use zsh syntax.";
|
|
79
|
+
}
|
|
80
|
+
if (family === "fish") {
|
|
81
|
+
return "fish may be selected explicitly, but ForgeRelay compatibility is less mature than Bash and native fish execution is not enabled until its runtime adapter is available. Agent commands and Hooks must use fish syntax when enabled.";
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
export function shellFamiliesForCustomSelection() {
|
|
86
|
+
return ["bash", "zsh", "fish", "sh", "pwsh", "powershell", "cmd"]
|
|
87
|
+
.map((family) => ({ value: family, label: shellFamilyLabel(family) }));
|
|
88
|
+
}
|
|
89
|
+
function shellFamilyLabel(family) {
|
|
90
|
+
switch (family) {
|
|
91
|
+
case "bash": return "Bash";
|
|
92
|
+
case "zsh": return "zsh";
|
|
93
|
+
case "fish": return "fish";
|
|
94
|
+
case "sh": return "POSIX sh";
|
|
95
|
+
case "pwsh": return "PowerShell 7 (pwsh)";
|
|
96
|
+
case "powershell": return "Windows PowerShell 5.1";
|
|
97
|
+
case "cmd": return "cmd.exe";
|
|
98
|
+
}
|
|
99
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -21,21 +21,31 @@ import { parseSubagentRunArgs } from "./subagents/cli-target.js";
|
|
|
21
21
|
import { ensureForgeRelayInstanceId, loadForgeRelayFiles, removeForgeRelayRemote, renameForgeRelayRemote, writeForgeRelayConfig, writeForgeRelayRemote, } from "./runtime/config/user-config.js";
|
|
22
22
|
import { shutdownHttpServer } from "./mcp/server/transport/server-shutdown.js";
|
|
23
23
|
import { publicEndpointUrl } from "./mcp/oauth/public-url.js";
|
|
24
|
+
import { assertRuntimePrivilegeAllowed, detectRuntimePrivilege, elevatedRuntimeWarning, formatRuntimePrivilege, } from "./runtime/security/runtime-privilege.js";
|
|
25
|
+
import { formatCommandShellRuntime } from "./runtime/shell/command-shell-runtime.js";
|
|
24
26
|
import { authenticateRemote, defaultRemoteAlias, isRemoteMcpUnauthorized, normalizeRemoteServiceTarget, refreshRemoteAuthentication, verifyRemoteMcp, } from "./workspaces/relay/auth/remote-auth.js";
|
|
25
27
|
import { defaultSshRouteAlias, parseSshRoute, readRemoteOwnerToken, withRemoteServiceEndpoint, } from "./workspaces/relay/transport/remote-transport.js";
|
|
26
28
|
import { assertSupportedNode, checkBashShell, checkGitAvailable, checkSqliteNative, nodeVersionStatus, normalizeOptionalPublicBaseUrl, } from "./cli/setup-support.js";
|
|
27
29
|
const require = createRequire(import.meta.url);
|
|
28
30
|
async function main(argv) {
|
|
29
31
|
assertSupportedNode();
|
|
30
|
-
const [rawCommand, ...
|
|
31
|
-
const
|
|
32
|
+
const [rawCommand, ...rest] = argv;
|
|
33
|
+
const defaultServeFlag = rawCommand === "--allow-elevated";
|
|
34
|
+
const command = defaultServeFlag ? "serve" : normalizeCommand(rawCommand);
|
|
35
|
+
const args = defaultServeFlag ? argv : rest;
|
|
32
36
|
switch (command) {
|
|
33
|
-
case "serve":
|
|
37
|
+
case "serve": {
|
|
38
|
+
const serveOptions = parseServeCommandArgs(args);
|
|
39
|
+
const runtimePrivilege = detectRuntimePrivilege();
|
|
40
|
+
assertRuntimePrivilegeAllowed(runtimePrivilege, serveOptions.allowElevated);
|
|
41
|
+
if (serveOptions.allowElevated)
|
|
42
|
+
console.warn(elevatedRuntimeWarning(runtimePrivilege));
|
|
34
43
|
await ensureConfigured();
|
|
35
|
-
await serve();
|
|
44
|
+
await serve(runtimePrivilege);
|
|
36
45
|
return;
|
|
46
|
+
}
|
|
37
47
|
case "init":
|
|
38
|
-
await runInit({ force: args.includes("--force") });
|
|
48
|
+
await runInit({ force: args.includes("--force"), version: installedForgeRelayVersion() });
|
|
39
49
|
return;
|
|
40
50
|
case "doctor":
|
|
41
51
|
await runDoctor();
|
|
@@ -74,6 +84,19 @@ function normalizeCommand(command) {
|
|
|
74
84
|
return "version";
|
|
75
85
|
throw new Error(`Unknown command: ${command}`);
|
|
76
86
|
}
|
|
87
|
+
function parseServeCommandArgs(args) {
|
|
88
|
+
let allowElevated = false;
|
|
89
|
+
for (const arg of args) {
|
|
90
|
+
if (arg === "--allow-elevated") {
|
|
91
|
+
if (allowElevated)
|
|
92
|
+
throw new Error("--allow-elevated may only be supplied once.");
|
|
93
|
+
allowElevated = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Unknown serve option: ${arg}`);
|
|
97
|
+
}
|
|
98
|
+
return { allowElevated };
|
|
99
|
+
}
|
|
77
100
|
async function ensureConfigured() {
|
|
78
101
|
const files = loadForgeRelayFiles();
|
|
79
102
|
if (files.configExists && files.authExists) {
|
|
@@ -94,9 +117,9 @@ async function ensureConfigured() {
|
|
|
94
117
|
"Or provide FORGERELAY_OAUTH_OWNER_TOKEN and FORGERELAY_ALLOWED_ROOTS.",
|
|
95
118
|
].join("\n"));
|
|
96
119
|
}
|
|
97
|
-
await runInit({ force: false });
|
|
120
|
+
await runInit({ force: false, version: installedForgeRelayVersion() });
|
|
98
121
|
}
|
|
99
|
-
async function serve() {
|
|
122
|
+
async function serve(runtimePrivilege) {
|
|
100
123
|
const sqliteStatus = checkSqliteNative();
|
|
101
124
|
if (sqliteStatus !== "ok") {
|
|
102
125
|
throw new Error([
|
|
@@ -109,6 +132,7 @@ async function serve() {
|
|
|
109
132
|
}
|
|
110
133
|
const { createServer } = await import("./server.js");
|
|
111
134
|
const config = loadConfig();
|
|
135
|
+
config.runtimePrivilege = runtimePrivilege;
|
|
112
136
|
const runtimeLease = acquireRuntimeLease(config.stateDir);
|
|
113
137
|
let server;
|
|
114
138
|
try {
|
|
@@ -128,6 +152,8 @@ async function serve() {
|
|
|
128
152
|
console.warn("warning: Host header allowlist is disabled because FORGERELAY_ALLOWED_HOSTS=*");
|
|
129
153
|
}
|
|
130
154
|
console.log("auth: Owner password approval required");
|
|
155
|
+
console.log(`command shell: ${formatCommandShellRuntime(config.commandShellRuntime)}`);
|
|
156
|
+
console.log(`runtime privilege: ${formatRuntimePrivilege(runtimePrivilege)}`);
|
|
131
157
|
console.log(`logging: ${config.logging.level} ${config.logging.format}`);
|
|
132
158
|
if (config.subagents) {
|
|
133
159
|
console.log(`subagent providers: ${formatSubagentProviderAvailabilitySummary(subagentProviders)}`);
|
|
@@ -325,6 +351,7 @@ async function runDoctor() {
|
|
|
325
351
|
console.log(`Node: ${process.version} (${nodeVersionStatus()})`);
|
|
326
352
|
console.log(`Node ABI: ${process.versions.modules}`);
|
|
327
353
|
console.log(`Platform: ${process.platform} ${process.arch}`);
|
|
354
|
+
console.log(`Runtime privilege: ${formatRuntimePrivilege(detectRuntimePrivilege())}`);
|
|
328
355
|
console.log(`Git: ${checkGitAvailable()}`);
|
|
329
356
|
console.log(`Bash shell: ${checkBashShell()}`);
|
|
330
357
|
console.log(`SQLite native dependency: ${checkSqliteNative()}`);
|
|
@@ -334,6 +361,7 @@ async function runDoctor() {
|
|
|
334
361
|
console.log(`Client-facing base URLs: ${config.publicBaseUrls.join(", ")}`);
|
|
335
362
|
console.log(`Client-facing base URL: ${config.publicBaseUrl}`);
|
|
336
363
|
console.log(`Client-facing MCP URL: ${publicEndpointUrl(config.publicBaseUrl, "mcp").toString()}`);
|
|
364
|
+
console.log(`Command shell: ${formatCommandShellRuntime(config.commandShellRuntime)}`);
|
|
337
365
|
console.log(`Tool mode: ${config.toolMode}`);
|
|
338
366
|
console.log(`Widgets: ${config.widgets}`);
|
|
339
367
|
console.log(`Trust proxy: ${config.proxyTrust === false ? "off" : config.proxyTrust.join(", ")}`);
|
|
@@ -377,6 +405,8 @@ function printHelp() {
|
|
|
377
405
|
"Usage:",
|
|
378
406
|
" forgerelay Run first-time setup if needed, then start the server",
|
|
379
407
|
" forgerelay serve Start the server",
|
|
408
|
+
" forgerelay serve --allow-elevated",
|
|
409
|
+
" Explicitly allow this invocation to run with elevated/unknown OS privilege",
|
|
380
410
|
" forgerelay init Create or update ~/.forgerelay/config.json and auth.json",
|
|
381
411
|
" forgerelay doctor Show config, runtime, and native dependency status",
|
|
382
412
|
" forgerelay config get Print persisted config",
|
|
@@ -570,12 +600,15 @@ function printAgentsHelp() {
|
|
|
570
600
|
" forgerelay agents show <id>",
|
|
571
601
|
].join("\n"));
|
|
572
602
|
}
|
|
573
|
-
function
|
|
603
|
+
function installedForgeRelayVersion() {
|
|
574
604
|
const packageJson = require("../package.json");
|
|
575
|
-
if (typeof packageJson.version !== "string") {
|
|
605
|
+
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
|
576
606
|
throw new Error("Unable to read ForgeRelay package version.");
|
|
577
607
|
}
|
|
578
|
-
|
|
608
|
+
return packageJson.version;
|
|
609
|
+
}
|
|
610
|
+
function printVersion() {
|
|
611
|
+
console.log(installedForgeRelayVersion());
|
|
579
612
|
}
|
|
580
613
|
main(process.argv.slice(2)).catch((error) => {
|
|
581
614
|
console.error(error instanceof Error ? error.message : String(error));
|
package/dist/mcp/hooks/hooks.js
CHANGED
|
@@ -2,7 +2,8 @@ import { readFile, readdir } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { performance } from "node:perf_hooks";
|
|
4
4
|
import { commandPreview, logEvent, workspaceLogLabel } from "../../runtime/logging/logger.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveCompatibilityCommandShellRuntime, snapshotCommandShellRuntime, } from "../../runtime/shell/command-shell-runtime.js";
|
|
6
|
+
import { resolveShellCommandForRuntime } from "../process/process-platform.js";
|
|
6
7
|
import { executeHookCommand, hookFailureOutput } from "./command-runner.js";
|
|
7
8
|
export const HOOK_EVENTS = [
|
|
8
9
|
"WorkspaceOpen",
|
|
@@ -212,11 +213,13 @@ export class HookRunner {
|
|
|
212
213
|
logging;
|
|
213
214
|
baseEnv;
|
|
214
215
|
resultDecorator;
|
|
215
|
-
|
|
216
|
+
commandShellRuntime;
|
|
217
|
+
constructor(hooks, logging, baseEnv = process.env, resultDecorator, commandShellRuntime) {
|
|
216
218
|
this.hooks = hooks;
|
|
217
219
|
this.logging = logging;
|
|
218
220
|
this.baseEnv = baseEnv;
|
|
219
221
|
this.resultDecorator = resultDecorator;
|
|
222
|
+
this.commandShellRuntime = snapshotCommandShellRuntime(commandShellRuntime ?? resolveCompatibilityCommandShellRuntime(process.platform, baseEnv));
|
|
220
223
|
}
|
|
221
224
|
decorateResult(workspaceId, result) {
|
|
222
225
|
return (this.resultDecorator?.(workspaceId, result) ?? result);
|
|
@@ -274,7 +277,7 @@ export class HookRunner {
|
|
|
274
277
|
async runHandler(event, handler, index, invocation, scope, signal) {
|
|
275
278
|
const startedAt = performance.now();
|
|
276
279
|
const name = handler.name ?? `${event} handler ${index + 1}`;
|
|
277
|
-
const shell =
|
|
280
|
+
const shell = resolveShellCommandForRuntime(handler.command, this.commandShellRuntime);
|
|
278
281
|
const detached = process.platform !== "win32";
|
|
279
282
|
const env = hookEnvironment(this.baseEnv, event, invocation);
|
|
280
283
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { resolveCompatibilityCommandShellRuntime, } from "../../runtime/shell/command-shell-runtime.js";
|
|
3
3
|
const defaultProcessTreeRuntime = {
|
|
4
4
|
platform: process.platform,
|
|
5
5
|
killGroup: (pid, signal) => process.kill(-pid, signal),
|
|
@@ -11,37 +11,35 @@ const defaultProcessTreeRuntime = {
|
|
|
11
11
|
return !result.error && result.status === 0;
|
|
12
12
|
},
|
|
13
13
|
};
|
|
14
|
-
const POSIX_SHELLS = new Set(["ash", "dash", "ksh", "sh"]);
|
|
15
14
|
export function resolveShellCommand(command, platform = process.platform, environment = process.env) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Match Node's native `spawn(command, { shell: cmd.exe })` quoting.
|
|
20
|
-
// cmd.exe /S applies special quote stripping, so the whole command must
|
|
21
|
-
// be wrapped even when the executable inside it is already quoted.
|
|
22
|
-
args: ["/d", "/s", "/c", `"${command}"`],
|
|
23
|
-
windowsVerbatimArguments: true,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
15
|
+
return resolveShellCommandForRuntime(command, resolveCompatibilityCommandShellRuntime(platform, environment));
|
|
16
|
+
}
|
|
17
|
+
export function resolveShellCommandForRuntime(command, runtime) {
|
|
26
18
|
// Agent and Hook commands must not source the user's interactive/login shell
|
|
27
19
|
// configuration. ForgeRelay already inherits PATH and other environment from
|
|
28
20
|
// the process that launched the server; re-running zsh/bash as a login shell
|
|
29
21
|
// can inject prompts, banners, aliases, plugins, or other user-only behavior.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
22
|
+
switch (runtime.family) {
|
|
23
|
+
case "cmd":
|
|
24
|
+
return {
|
|
25
|
+
executable: runtime.executable,
|
|
26
|
+
// Match Node's native `spawn(command, { shell: cmd.exe })` quoting.
|
|
27
|
+
// cmd.exe /S applies special quote stripping, so the whole command must
|
|
28
|
+
// be wrapped even when the executable inside it is already quoted.
|
|
29
|
+
args: ["/d", "/s", "/c", `"${command}"`],
|
|
30
|
+
windowsVerbatimArguments: true,
|
|
31
|
+
};
|
|
32
|
+
case "bash":
|
|
33
|
+
return { executable: runtime.executable, args: ["--noprofile", "--norc", "-c", command] };
|
|
34
|
+
case "zsh":
|
|
35
|
+
return { executable: runtime.executable, args: ["-f", "-c", command] };
|
|
36
|
+
case "sh":
|
|
37
|
+
return { executable: runtime.executable, args: ["-c", command] };
|
|
38
|
+
case "fish":
|
|
39
|
+
case "pwsh":
|
|
40
|
+
case "powershell":
|
|
41
|
+
throw new Error(`Command shell runtime ${runtime.family} is identified but native execution support is not enabled in this release stage. ForgeRelay will not silently execute the command through another shell.`);
|
|
43
42
|
}
|
|
44
|
-
return { executable: "/bin/sh", args: ["-c", command] };
|
|
45
43
|
}
|
|
46
44
|
export function terminateProcessTree(child, signal, detached, runtime = defaultProcessTreeRuntime) {
|
|
47
45
|
if (runtime.platform === "win32" && child.pid) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveShellCommandForRuntime, terminateProcessTree } from "./process-platform.js";
|
|
3
|
+
import { resolveCompatibilityCommandShellRuntime, snapshotCommandShellRuntime } from "../../runtime/shell/command-shell-runtime.js";
|
|
3
4
|
const DEFAULT_EXEC_YIELD_MS = 10_000;
|
|
4
5
|
const DEFAULT_INTERACTIVE_YIELD_MS = 250;
|
|
5
6
|
export const DEFAULT_POLL_YIELD_MS = 60_000;
|
|
@@ -210,6 +211,7 @@ export class ProcessManager {
|
|
|
210
211
|
maxStartYieldMs;
|
|
211
212
|
monotonicNow;
|
|
212
213
|
outputAudit;
|
|
214
|
+
commandShellRuntime;
|
|
213
215
|
nextProcessId = 1;
|
|
214
216
|
constructor(options = {}) {
|
|
215
217
|
this.maxBufferCharacters = options.maxBufferCharacters ?? DEFAULT_BUFFER_CHARACTERS;
|
|
@@ -227,6 +229,7 @@ export class ProcessManager {
|
|
|
227
229
|
this.maxStartYieldMs = options.maxStartYieldMs ?? MAX_START_YIELD_MS;
|
|
228
230
|
this.monotonicNow = options.monotonicNow ?? (() => performance.now());
|
|
229
231
|
this.outputAudit = options.outputAudit;
|
|
232
|
+
this.commandShellRuntime = snapshotCommandShellRuntime(options.commandShellRuntime ?? resolveCompatibilityCommandShellRuntime());
|
|
230
233
|
}
|
|
231
234
|
async start(input) {
|
|
232
235
|
input.signal?.throwIfAborted();
|
|
@@ -435,7 +438,7 @@ export class ProcessManager {
|
|
|
435
438
|
};
|
|
436
439
|
}
|
|
437
440
|
startPipe(processEntry, input) {
|
|
438
|
-
const shell =
|
|
441
|
+
const shell = resolveShellCommandForRuntime(input.command, this.commandShellRuntime);
|
|
439
442
|
const detached = process.platform !== "win32";
|
|
440
443
|
const child = spawn(shell.executable, shell.args, {
|
|
441
444
|
cwd: input.cwd,
|
|
@@ -467,7 +470,7 @@ export class ProcessManager {
|
|
|
467
470
|
catch {
|
|
468
471
|
throw new Error("PTY support requires the optional node-pty dependency.");
|
|
469
472
|
}
|
|
470
|
-
const shell =
|
|
473
|
+
const shell = resolveShellCommandForRuntime(input.command, this.commandShellRuntime);
|
|
471
474
|
let pty;
|
|
472
475
|
try {
|
|
473
476
|
pty = nodePty.spawn(shell.executable, shell.args, {
|
|
@@ -71,7 +71,10 @@ export function createHttpServer(config, options, createMcpServer) {
|
|
|
71
71
|
turnIdForConversation: (conversationScopeId, workspaceId) => activityQueries.currentTurnId(conversationScopeId, workspaceId),
|
|
72
72
|
});
|
|
73
73
|
const reviewCheckpoints = createReviewCheckpointManager();
|
|
74
|
-
const processSessions = new ProcessManager({
|
|
74
|
+
const processSessions = new ProcessManager({
|
|
75
|
+
outputAudit: bashOutputStore,
|
|
76
|
+
commandShellRuntime: config.commandShellRuntime,
|
|
77
|
+
});
|
|
75
78
|
const codeIntelligence = new CodeIntelligenceManager(config);
|
|
76
79
|
const subagentProviders = config.subagents
|
|
77
80
|
? getSubagentProviderAvailabilitySnapshot()
|
|
@@ -71,6 +71,10 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
|
|
|
71
71
|
const cardAvailableAgentsFiles = availableAgentsFiles.map((file) => ({
|
|
72
72
|
path: formatAgentsPath(file.path, workspace.root),
|
|
73
73
|
}));
|
|
74
|
+
const cardWorkspaceInstructions = workspace.workspaceInstructions.map((instruction) => ({
|
|
75
|
+
path: formatAgentsPath(instruction.path, workspace.root),
|
|
76
|
+
status: instruction.status,
|
|
77
|
+
}));
|
|
74
78
|
const bootstrapComponents = new Set(effectiveBootstrapContextComponents);
|
|
75
79
|
const visibleSkills = bootstrapComponents.has("skills") ? cardSkills : [];
|
|
76
80
|
const visibleSkillDiagnostics = bootstrapComponents.has("skillDiagnostics")
|
|
@@ -185,6 +189,7 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
|
|
|
185
189
|
capabilityCatalog,
|
|
186
190
|
agentsFiles: cardAgentsFiles,
|
|
187
191
|
availableAgentsFiles: cardAvailableAgentsFiles,
|
|
192
|
+
workspaceInstructions: cardWorkspaceInstructions,
|
|
188
193
|
skills: cardSkills,
|
|
189
194
|
agentProviders: cardAgentProviders,
|
|
190
195
|
agents: cardAgents,
|
|
@@ -193,6 +198,7 @@ export async function presentLocalWorkspaceOpen(options, input, contextData) {
|
|
|
193
198
|
mode: workspace.mode,
|
|
194
199
|
agentsFiles: cardAgentsFiles.length,
|
|
195
200
|
availableAgentsFiles: cardAvailableAgentsFiles.length,
|
|
201
|
+
workspaceInstructions: cardWorkspaceInstructions.length,
|
|
196
202
|
skills: cardSkills.length,
|
|
197
203
|
capabilities: capabilityCatalog.length,
|
|
198
204
|
agentProviders: cardAgentProviders.length,
|
|
@@ -26,7 +26,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
|
|
|
26
26
|
outputSchema: {
|
|
27
27
|
path: z.string(),
|
|
28
28
|
content: z.string(),
|
|
29
|
-
status: z.enum(["loaded", "available"]),
|
|
29
|
+
status: z.enum(["loaded", "available", "disabled"]),
|
|
30
30
|
},
|
|
31
31
|
_meta: { ui: { visibility: ["app"] } },
|
|
32
32
|
annotations: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandShellAgentInstruction } from "../runtime/shell/command-shell-runtime.js";
|
|
1
2
|
export const toolNames = {
|
|
2
3
|
openWorkspace: "open_workspace",
|
|
3
4
|
closeWorkspace: "close_workspace",
|
|
@@ -14,7 +15,7 @@ export function buildShellMutationPolicy() {
|
|
|
14
15
|
return "Shell commands may modify ordinary project files when that is a natural part of the user's requested development task. They may also perform external device or hardware mutations when the user's current request explicitly asks for the actual device-changing operation, including firmware flashing or equivalent persistent device updates; do not infer such authorization from a check, audit, probe, backup, verification, dry-run, or build-only request. 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.";
|
|
15
16
|
}
|
|
16
17
|
export function buildServerInstructions(config) {
|
|
17
|
-
return joinInstructions(capabilityContractInstructions(config), selectedWorkflowInstructions(config), config.appendInstructions);
|
|
18
|
+
return joinInstructions(runtimePrivilegeInstructions(config), commandShellAgentInstruction(config.commandShellRuntime), capabilityContractInstructions(config), selectedWorkflowInstructions(config), config.appendInstructions);
|
|
18
19
|
}
|
|
19
20
|
export function buildToolDescriptions(config) {
|
|
20
21
|
const skillCapability = config.skillsEnabled
|
|
@@ -34,6 +35,14 @@ export function buildToolDescriptions(config) {
|
|
|
34
35
|
shellCommand: "Shell command to run with the local user's authority.",
|
|
35
36
|
};
|
|
36
37
|
}
|
|
38
|
+
function runtimePrivilegeInstructions(config) {
|
|
39
|
+
if (!config.runtimePrivilege || config.runtimePrivilege.level === "standard")
|
|
40
|
+
return "";
|
|
41
|
+
const status = config.runtimePrivilege.level === "elevated"
|
|
42
|
+
? "elevated operating-system privileges"
|
|
43
|
+
: "an unverified operating-system privilege level accepted through --allow-elevated";
|
|
44
|
+
return `Runtime security: ForgeRelay is running with ${status}. Agent and Hook shell commands inherit this authority. Do not perform system-wide, privileged, security-sensitive, or irreversible mutations unless the user's current request explicitly authorizes that exact change. This safety notice is mandatory and is not disabled by shell-guidance settings.`;
|
|
45
|
+
}
|
|
37
46
|
function capabilityContractInstructions(config) {
|
|
38
47
|
const staleWorkspacePolicy = config.toolMode === "codex"
|
|
39
48
|
? ""
|
|
@@ -4,6 +4,8 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import { expandHomePath } from "../../mcp/filesystem/roots.js";
|
|
5
5
|
import { mergeHookConfigs, parseHookConfig } from "../../mcp/hooks/hooks.js";
|
|
6
6
|
import { forgerelayAgentsDir, forgerelaySkillsDir, generateInstanceId, loadForgeRelayFiles, } from "./user-config.js";
|
|
7
|
+
import { shellInstructionPath } from "../instructions/shell-instructions.js";
|
|
8
|
+
import { resolveConfiguredCommandShellRuntime, } from "../shell/command-shell-runtime.js";
|
|
7
9
|
const DEFAULT_OAUTH_ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
8
10
|
const DEFAULT_OAUTH_REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
9
11
|
const DEFAULT_ARTIFACT_MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
@@ -268,6 +270,7 @@ export function loadConfig(env = process.env) {
|
|
|
268
270
|
const publicDeployment = resolvePublicDeployment(env, files.config, host, port);
|
|
269
271
|
const publicBaseUrl = publicDeployment.canonicalBaseUrl;
|
|
270
272
|
const proxyTrust = resolveProxyTrust(env, files.config, host, publicBaseUrl);
|
|
273
|
+
const commandShellRuntime = resolveConfiguredCommandShellRuntime(files.config.commandShell, process.platform, env);
|
|
271
274
|
const derivedAllowedHosts = [
|
|
272
275
|
"localhost",
|
|
273
276
|
"127.0.0.1",
|
|
@@ -314,6 +317,9 @@ export function loadConfig(env = process.env) {
|
|
|
314
317
|
systemInstructionsPath: parseSystemInstructionsPath(productEnv(env, "SYSTEM_INSTRUCTIONS_PATH") ?? files.config.systemInstructionsPath),
|
|
315
318
|
hooks: mergeHookConfigs(parseHookConfig(files.config.hooks), parseHookConfig(files.hooks), files.hookFiles),
|
|
316
319
|
logging: parseLoggingConfig(env, proxyTrust !== false),
|
|
320
|
+
commandShellRuntime,
|
|
321
|
+
shellInstructionsEnabled: files.config.shellInstructions !== false,
|
|
322
|
+
shellInstructionPath: shellInstructionPath(files.dir, commandShellRuntime.family),
|
|
317
323
|
};
|
|
318
324
|
}
|
|
319
325
|
function numberConfigValue(value) {
|