@akira-tl/forgerelay 0.9.4 → 0.10.1
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 +18 -0
- package/README.md +4 -3
- package/capabilities/shell-processes/GUIDE.md +1 -1
- package/dist/cli/init.js +86 -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 +34 -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/powershell-skill.js +65 -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 +278 -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/docs/gotchas.md +5 -3
- package/package.json +3 -2
- package/scripts/ci/pwsh-acceptance.mjs +308 -0
- package/scripts/ci/verify.mjs +1 -0
- 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,24 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.10.1] - 2026-09-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added native PowerShell 7 (`pwsh`) execution for Agent commands, Hooks, and PTY sessions with one versioned Command Shell Runtime, no-profile invocation, PowerShell-native command semantics, and Windows packaged-shim acceptance in cloud CI.
|
|
12
|
+
- Added an exact-release-tag PowerShell guidance Skill seeded into the ForgeRelay config Skills directory without bundling prompt templates into the npm package or overwriting user edits.
|
|
13
|
+
|
|
14
|
+
## [0.10.0] - 2026-09-05
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- 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.
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
7
25
|
## [0.9.4] - 2026-09-04
|
|
8
26
|
|
|
9
27
|
### 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
|
|
|
@@ -295,8 +295,9 @@ See [Security Model](docs/security.md) for the full boundary and threat model.
|
|
|
295
295
|
| --- | --- | --- |
|
|
296
296
|
| Linux | Supported | Requires Node, npm, Git, and Bash. |
|
|
297
297
|
| macOS | Supported | Requires Node, npm, Git, and Bash. |
|
|
298
|
-
| Windows with
|
|
299
|
-
| Windows
|
|
298
|
+
| Windows with PowerShell 7 (`pwsh`) | Supported | Agent commands and Hooks use one native `pwsh` runtime without loading the user profile. |
|
|
299
|
+
| Windows with Git Bash, WSL, MSYS2, or Cygwin Bash | Supported | Bash remains available as a compatibility path. |
|
|
300
|
+
| Windows PowerShell 5.1 (`powershell.exe`) or `cmd.exe` only | Not supported yet | These runtimes are separate follow-up stages. |
|
|
300
301
|
|
|
301
302
|
You can check the local runtime with:
|
|
302
303
|
|
|
@@ -48,6 +48,6 @@ PTY 依赖 optional `node-pty`。缺少该依赖时 ForgeRelay 会明确报错
|
|
|
48
48
|
|
|
49
49
|
## Platform notes
|
|
50
50
|
|
|
51
|
-
ForgeRelay
|
|
51
|
+
ForgeRelay 在 Windows 上支持 PowerShell 7 (`pwsh`) 作为原生 Command Shell Runtime;非 TTY Agent 命令与 Hooks 使用同一个 `pwsh`,禁止加载用户 profile,PTY 也复用同一 executable。公共工具名仍保留为 `bash` 以兼容 Host contract,实际命令语言以 ForgeRelay 报告的 Command Shell Runtime 为准。Windows PowerShell 5.1 (`powershell.exe`) 与 `cmd.exe` 仍属于后续 runtime 阶段;Git Bash、WSL、MSYS2 或 Cygwin Bash 仍可作为 Bash 兼容路径。用 `forgerelay doctor` 检查当前 shell identity、version 与 executable。
|
|
52
52
|
|
|
53
53
|
Shell 可以作为用户开发任务的一部分修改普通项目文件,但始终受 ForgeRelay core mutation/safety contract 约束。涉及 privileged OS files、credentials、configuration 或外部硬件持久写入时,不要用本指南替代 core authorization/safety 规则。
|
package/dist/cli/init.js
CHANGED
|
@@ -2,10 +2,14 @@ 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";
|
|
7
|
+
import { seedPowerShellSkill, } from "../runtime/instructions/powershell-skill.js";
|
|
5
8
|
import { installManagedLanguageServers, installedManagedLanguageServers, managedLanguageServerOptions, } from "../lsp/runtime/managed-language-servers.js";
|
|
6
9
|
import { generateInstanceId, generateOwnerToken, loadForgeRelayFiles, resolveSubagentsFlag, writeForgeRelayAuth, writeForgeRelayConfig, } from "../runtime/config/user-config.js";
|
|
7
10
|
import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, setupBindAddress, SetupCancelledError, textPrompt, validateHttpsProxyBaseUrls, validateLanClientFacingBaseUrls, validatePort, } from "./setup-support.js";
|
|
8
|
-
|
|
11
|
+
import { commandShellCompatibilityWarning, commandShellSetupOptions, customPinnedPreference, defaultCommandShellSetupChoice, followLauncherPreference, pinnedFamilyPreference, preservePinnedPreference, shellFamiliesForCustomSelection, } from "./shell/setup.js";
|
|
12
|
+
export async function runInit({ force, version }) {
|
|
9
13
|
const files = loadForgeRelayFiles();
|
|
10
14
|
if (!force && files.configExists && files.authExists) {
|
|
11
15
|
prompts.log.info(`ForgeRelay is already configured at ${files.dir}`);
|
|
@@ -116,6 +120,74 @@ export async function runInit({ force }) {
|
|
|
116
120
|
}
|
|
117
121
|
publicBaseUrl = compactPublicBaseUrlConfig(clientFacingBaseUrls);
|
|
118
122
|
}
|
|
123
|
+
const launcherShell = detectLauncherCommandShell();
|
|
124
|
+
const selectedShellChoice = await prompts.select({
|
|
125
|
+
message: "Which command shell should Agent commands and Hooks use?",
|
|
126
|
+
initialValue: defaultCommandShellSetupChoice(files.config.commandShell),
|
|
127
|
+
options: commandShellSetupOptions(process.platform, files.config.commandShell, launcherShell),
|
|
128
|
+
});
|
|
129
|
+
if (prompts.isCancel(selectedShellChoice))
|
|
130
|
+
throw new SetupCancelledError();
|
|
131
|
+
let commandShell;
|
|
132
|
+
const shellChoice = selectedShellChoice;
|
|
133
|
+
if (shellChoice === "follow-launcher") {
|
|
134
|
+
commandShell = followLauncherPreference(files.config.commandShell, launcherShell);
|
|
135
|
+
}
|
|
136
|
+
else if (shellChoice === "keep-pinned") {
|
|
137
|
+
commandShell = preservePinnedPreference(files.config.commandShell);
|
|
138
|
+
}
|
|
139
|
+
else if (shellChoice === "custom") {
|
|
140
|
+
const customFamily = await prompts.select({
|
|
141
|
+
message: "Which command language does the custom executable implement?",
|
|
142
|
+
options: shellFamiliesForCustomSelection(),
|
|
143
|
+
});
|
|
144
|
+
if (prompts.isCancel(customFamily))
|
|
145
|
+
throw new SetupCancelledError();
|
|
146
|
+
const executable = await textPrompt({
|
|
147
|
+
message: "What is the command-shell executable path?",
|
|
148
|
+
placeholder: process.platform === "win32" ? "D:\\Portable\\PowerShell\\pwsh.exe" : "/opt/custom/bin/zsh",
|
|
149
|
+
defaultValue: "",
|
|
150
|
+
validate: (value) => value?.trim() ? undefined : "Enter an executable path.",
|
|
151
|
+
});
|
|
152
|
+
commandShell = customPinnedPreference(customFamily, executable);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
commandShell = pinnedFamilyPreference(shellChoice);
|
|
156
|
+
}
|
|
157
|
+
const shellWarning = commandShellCompatibilityWarning(commandShell.family);
|
|
158
|
+
if (shellWarning)
|
|
159
|
+
prompts.note(shellWarning, "Command shell compatibility");
|
|
160
|
+
const shellInstructionFamilies = shellInstructionFamiliesToSeed(process.platform, commandShell.family);
|
|
161
|
+
let shellInstructions = files.config.shellInstructions;
|
|
162
|
+
let shellInstructionSeedResults = [];
|
|
163
|
+
let powerShellSkillSeedResult;
|
|
164
|
+
if (shellInstructionFamilies.length > 0) {
|
|
165
|
+
const shellInstructionsAnswer = await prompts.confirm({
|
|
166
|
+
message: "Load ForgeRelay-provided extra Instructions for this command shell?",
|
|
167
|
+
initialValue: files.config.shellInstructions !== false,
|
|
168
|
+
});
|
|
169
|
+
if (prompts.isCancel(shellInstructionsAnswer))
|
|
170
|
+
throw new SetupCancelledError();
|
|
171
|
+
shellInstructions = shellInstructionsAnswer === true;
|
|
172
|
+
shellInstructionSeedResults = await seedShellInstructionFiles({
|
|
173
|
+
configDir: files.dir,
|
|
174
|
+
version,
|
|
175
|
+
families: shellInstructionFamilies,
|
|
176
|
+
});
|
|
177
|
+
const failedShellInstructions = shellInstructionSeedResults.filter((result) => result.status === "failed");
|
|
178
|
+
if (failedShellInstructions.length > 0) {
|
|
179
|
+
prompts.note(failedShellInstructions.map((result) => `${result.family}: ${result.error ?? "download failed"}\n${result.sourceUrl}`).join("\n\n"), "Shell Instructions unavailable");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (commandShell.family === "pwsh") {
|
|
183
|
+
powerShellSkillSeedResult = await seedPowerShellSkill({
|
|
184
|
+
configDir: files.dir,
|
|
185
|
+
version,
|
|
186
|
+
});
|
|
187
|
+
if (powerShellSkillSeedResult.status === "failed") {
|
|
188
|
+
prompts.note(`${powerShellSkillSeedResult.error ?? "download failed"}\n${powerShellSkillSeedResult.sourceUrl}`, "PowerShell Skill unavailable");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
119
191
|
const installedManaged = installedManagedLanguageServers(files.dir);
|
|
120
192
|
const selectedManaged = await prompts.multiselect({
|
|
121
193
|
message: "Which Language Servers should ForgeRelay manage with npm?",
|
|
@@ -150,6 +222,7 @@ export async function runInit({ force }) {
|
|
|
150
222
|
}
|
|
151
223
|
}
|
|
152
224
|
const config = {
|
|
225
|
+
...files.config,
|
|
153
226
|
host,
|
|
154
227
|
port,
|
|
155
228
|
allowedRoots,
|
|
@@ -158,6 +231,8 @@ export async function runInit({ force }) {
|
|
|
158
231
|
trustedProxies,
|
|
159
232
|
workflowInstructions: files.config.workflowInstructions,
|
|
160
233
|
appendInstructions: files.config.appendInstructions,
|
|
234
|
+
commandShell,
|
|
235
|
+
shellInstructions,
|
|
161
236
|
subagents: resolveSubagentsFlag(files.config),
|
|
162
237
|
languageServers: files.config.languageServers,
|
|
163
238
|
allowAgentLanguageServerInstall,
|
|
@@ -173,6 +248,16 @@ export async function runInit({ force }) {
|
|
|
173
248
|
`Config: ${configPath}`,
|
|
174
249
|
`Auth: ${authPath}`,
|
|
175
250
|
`Bind: http://${config.host}:${config.port}`,
|
|
251
|
+
`Command shell: ${commandShell.mode} ${commandShell.family} (${commandShell.executable})`,
|
|
252
|
+
...(shellInstructionFamilies.length > 0
|
|
253
|
+
? [
|
|
254
|
+
`Shell Instructions: ${shellInstructions ? "enabled" : "disabled"}`,
|
|
255
|
+
...shellInstructionSeedResults.map((result) => `Shell Instructions ${result.family}: ${result.status} (${result.path})`),
|
|
256
|
+
]
|
|
257
|
+
: []),
|
|
258
|
+
...(powerShellSkillSeedResult
|
|
259
|
+
? [`PowerShell Skill: ${powerShellSkillSeedResult.status} (${powerShellSkillSeedResult.path})`]
|
|
260
|
+
: []),
|
|
176
261
|
...clientFacingBaseUrls.map((baseUrl, index) => `${index === 0 ? "Client-facing MCP URL" : "Client-facing MCP alias"}: ${publicEndpointUrl(baseUrl, "mcp").toString()}`),
|
|
177
262
|
];
|
|
178
263
|
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,45 @@ 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, options = {}) {
|
|
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 "pwsh":
|
|
39
|
+
return {
|
|
40
|
+
executable: runtime.executable,
|
|
41
|
+
args: [
|
|
42
|
+
"-NoLogo",
|
|
43
|
+
"-NoProfile",
|
|
44
|
+
...(options.interactive ? [] : ["-NonInteractive"]),
|
|
45
|
+
"-Command",
|
|
46
|
+
command,
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
case "fish":
|
|
50
|
+
case "powershell":
|
|
51
|
+
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
52
|
}
|
|
44
|
-
return { executable: "/bin/sh", args: ["-c", command] };
|
|
45
53
|
}
|
|
46
54
|
export function terminateProcessTree(child, signal, detached, runtime = defaultProcessTreeRuntime) {
|
|
47
55
|
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, { interactive: true });
|
|
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: {
|