@akira-tl/forgerelay 0.9.3 → 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 +22 -0
- package/README.md +10 -4
- package/dist/cli/init.js +111 -24
- package/dist/cli/maintenance-prune.js +479 -0
- package/dist/cli/maintenance-retention.js +93 -0
- package/dist/cli/maintenance.js +598 -0
- package/dist/cli/setup-support.js +21 -0
- package/dist/cli/shell/setup.js +99 -0
- package/dist/cli.js +72 -16
- package/dist/mcp/hooks/hooks.js +6 -3
- package/dist/mcp/oauth/public-url.js +3 -0
- package/dist/mcp/oauth/router.js +14 -9
- 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 +17 -9
- 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 +60 -6
- 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/runtime/state/runtime-lease.js +109 -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 +101 -14
- package/package.json +2 -2
- package/scripts/release/release-gate.test.mjs +1 -0
- package/scripts/release-proof.mjs +85 -9
- package/scripts/release-proof.test.mjs +79 -1
|
@@ -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
|
@@ -9,7 +9,9 @@ import { join, resolve } from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import * as prompts from "@clack/prompts";
|
|
11
11
|
import { loadConfig } from "./runtime/config/config.js";
|
|
12
|
+
import { acquireRuntimeLease } from "./runtime/state/runtime-lease.js";
|
|
12
13
|
import { runInit } from "./cli/init.js";
|
|
14
|
+
import { runMaintenanceCommand } from "./cli/maintenance.js";
|
|
13
15
|
import { runHooksCommand } from "./mcp/hooks/hook-cli.js";
|
|
14
16
|
import { executeSubagentSession } from "./subagents/sessions/execution.js";
|
|
15
17
|
import { SubagentDeliveryMailbox } from "./subagents/sessions/delivery-mailbox.js";
|
|
@@ -19,21 +21,31 @@ import { parseSubagentRunArgs } from "./subagents/cli-target.js";
|
|
|
19
21
|
import { ensureForgeRelayInstanceId, loadForgeRelayFiles, removeForgeRelayRemote, renameForgeRelayRemote, writeForgeRelayConfig, writeForgeRelayRemote, } from "./runtime/config/user-config.js";
|
|
20
22
|
import { shutdownHttpServer } from "./mcp/server/transport/server-shutdown.js";
|
|
21
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";
|
|
22
26
|
import { authenticateRemote, defaultRemoteAlias, isRemoteMcpUnauthorized, normalizeRemoteServiceTarget, refreshRemoteAuthentication, verifyRemoteMcp, } from "./workspaces/relay/auth/remote-auth.js";
|
|
23
27
|
import { defaultSshRouteAlias, parseSshRoute, readRemoteOwnerToken, withRemoteServiceEndpoint, } from "./workspaces/relay/transport/remote-transport.js";
|
|
24
28
|
import { assertSupportedNode, checkBashShell, checkGitAvailable, checkSqliteNative, nodeVersionStatus, normalizeOptionalPublicBaseUrl, } from "./cli/setup-support.js";
|
|
25
29
|
const require = createRequire(import.meta.url);
|
|
26
30
|
async function main(argv) {
|
|
27
31
|
assertSupportedNode();
|
|
28
|
-
const [rawCommand, ...
|
|
29
|
-
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;
|
|
30
36
|
switch (command) {
|
|
31
|
-
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));
|
|
32
43
|
await ensureConfigured();
|
|
33
|
-
await serve();
|
|
44
|
+
await serve(runtimePrivilege);
|
|
34
45
|
return;
|
|
46
|
+
}
|
|
35
47
|
case "init":
|
|
36
|
-
await runInit({ force: args.includes("--force") });
|
|
48
|
+
await runInit({ force: args.includes("--force"), version: installedForgeRelayVersion() });
|
|
37
49
|
return;
|
|
38
50
|
case "doctor":
|
|
39
51
|
await runDoctor();
|
|
@@ -50,6 +62,9 @@ async function main(argv) {
|
|
|
50
62
|
case "auth":
|
|
51
63
|
await runAuthCommand(args);
|
|
52
64
|
return;
|
|
65
|
+
case "maintenance":
|
|
66
|
+
runMaintenanceCommand(args);
|
|
67
|
+
return;
|
|
53
68
|
case "help":
|
|
54
69
|
printHelp();
|
|
55
70
|
return;
|
|
@@ -61,7 +76,7 @@ async function main(argv) {
|
|
|
61
76
|
function normalizeCommand(command) {
|
|
62
77
|
if (!command || command === "serve" || command === "start")
|
|
63
78
|
return "serve";
|
|
64
|
-
if (command === "init" || command === "doctor" || command === "config" || command === "hooks" || command === "agents" || command === "auth")
|
|
79
|
+
if (command === "init" || command === "doctor" || command === "config" || command === "hooks" || command === "agents" || command === "auth" || command === "maintenance")
|
|
65
80
|
return command;
|
|
66
81
|
if (command === "help" || command === "--help" || command === "-h")
|
|
67
82
|
return "help";
|
|
@@ -69,6 +84,19 @@ function normalizeCommand(command) {
|
|
|
69
84
|
return "version";
|
|
70
85
|
throw new Error(`Unknown command: ${command}`);
|
|
71
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
|
+
}
|
|
72
100
|
async function ensureConfigured() {
|
|
73
101
|
const files = loadForgeRelayFiles();
|
|
74
102
|
if (files.configExists && files.authExists) {
|
|
@@ -89,9 +117,9 @@ async function ensureConfigured() {
|
|
|
89
117
|
"Or provide FORGERELAY_OAUTH_OWNER_TOKEN and FORGERELAY_ALLOWED_ROOTS.",
|
|
90
118
|
].join("\n"));
|
|
91
119
|
}
|
|
92
|
-
await runInit({ force: false });
|
|
120
|
+
await runInit({ force: false, version: installedForgeRelayVersion() });
|
|
93
121
|
}
|
|
94
|
-
async function serve() {
|
|
122
|
+
async function serve(runtimePrivilege) {
|
|
95
123
|
const sqliteStatus = checkSqliteNative();
|
|
96
124
|
if (sqliteStatus !== "ok") {
|
|
97
125
|
throw new Error([
|
|
@@ -104,9 +132,19 @@ async function serve() {
|
|
|
104
132
|
}
|
|
105
133
|
const { createServer } = await import("./server.js");
|
|
106
134
|
const config = loadConfig();
|
|
107
|
-
|
|
135
|
+
config.runtimePrivilege = runtimePrivilege;
|
|
136
|
+
const runtimeLease = acquireRuntimeLease(config.stateDir);
|
|
137
|
+
let server;
|
|
138
|
+
try {
|
|
139
|
+
server = createServer(config);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
runtimeLease.release();
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
const { app, close, subagentProviders } = server;
|
|
108
146
|
const httpServer = app.listen(config.port, config.host, () => {
|
|
109
|
-
console.log(`forgerelay listening on http://${config.host}:${config.port}
|
|
147
|
+
console.log(`forgerelay listening on http://${config.host}:${config.port}${publicEndpointUrl(config.publicBaseUrl, "mcp").pathname}`);
|
|
110
148
|
console.log(`client-facing base url: ${config.publicBaseUrl}`);
|
|
111
149
|
console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
|
|
112
150
|
console.log(`allowed hosts: ${config.allowedHosts.join(", ")}`);
|
|
@@ -114,17 +152,27 @@ async function serve() {
|
|
|
114
152
|
console.warn("warning: Host header allowlist is disabled because FORGERELAY_ALLOWED_HOSTS=*");
|
|
115
153
|
}
|
|
116
154
|
console.log("auth: Owner password approval required");
|
|
155
|
+
console.log(`command shell: ${formatCommandShellRuntime(config.commandShellRuntime)}`);
|
|
156
|
+
console.log(`runtime privilege: ${formatRuntimePrivilege(runtimePrivilege)}`);
|
|
117
157
|
console.log(`logging: ${config.logging.level} ${config.logging.format}`);
|
|
118
158
|
if (config.subagents) {
|
|
119
159
|
console.log(`subagent providers: ${formatSubagentProviderAvailabilitySummary(subagentProviders)}`);
|
|
120
160
|
}
|
|
121
161
|
});
|
|
122
162
|
let shuttingDown = false;
|
|
163
|
+
const releaseRuntimeLease = () => runtimeLease.release();
|
|
164
|
+
process.once("exit", releaseRuntimeLease);
|
|
123
165
|
const shutdown = async () => {
|
|
124
166
|
if (shuttingDown)
|
|
125
167
|
return;
|
|
126
168
|
shuttingDown = true;
|
|
127
|
-
|
|
169
|
+
try {
|
|
170
|
+
await shutdownHttpServer(httpServer, close);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
process.removeListener("exit", releaseRuntimeLease);
|
|
174
|
+
runtimeLease.release();
|
|
175
|
+
}
|
|
128
176
|
process.exit(0);
|
|
129
177
|
};
|
|
130
178
|
const handleShutdown = () => {
|
|
@@ -303,18 +351,20 @@ async function runDoctor() {
|
|
|
303
351
|
console.log(`Node: ${process.version} (${nodeVersionStatus()})`);
|
|
304
352
|
console.log(`Node ABI: ${process.versions.modules}`);
|
|
305
353
|
console.log(`Platform: ${process.platform} ${process.arch}`);
|
|
354
|
+
console.log(`Runtime privilege: ${formatRuntimePrivilege(detectRuntimePrivilege())}`);
|
|
306
355
|
console.log(`Git: ${checkGitAvailable()}`);
|
|
307
356
|
console.log(`Bash shell: ${checkBashShell()}`);
|
|
308
357
|
console.log(`SQLite native dependency: ${checkSqliteNative()}`);
|
|
309
358
|
try {
|
|
310
359
|
const config = loadConfig();
|
|
311
|
-
console.log(`Bind MCP URL: http://${config.host}:${config.port}
|
|
360
|
+
console.log(`Bind MCP URL: http://${config.host}:${config.port}${publicEndpointUrl(config.publicBaseUrl, "mcp").pathname}`);
|
|
312
361
|
console.log(`Client-facing base URLs: ${config.publicBaseUrls.join(", ")}`);
|
|
313
362
|
console.log(`Client-facing base URL: ${config.publicBaseUrl}`);
|
|
314
363
|
console.log(`Client-facing MCP URL: ${publicEndpointUrl(config.publicBaseUrl, "mcp").toString()}`);
|
|
364
|
+
console.log(`Command shell: ${formatCommandShellRuntime(config.commandShellRuntime)}`);
|
|
315
365
|
console.log(`Tool mode: ${config.toolMode}`);
|
|
316
366
|
console.log(`Widgets: ${config.widgets}`);
|
|
317
|
-
console.log(`Trust proxy: ${config.
|
|
367
|
+
console.log(`Trust proxy: ${config.proxyTrust === false ? "off" : config.proxyTrust.join(", ")}`);
|
|
318
368
|
console.log(`Artifacts: ${config.artifactsEnabled ? "enabled" : "disabled"}`);
|
|
319
369
|
console.log(`Subagents: ${config.subagents ? "enabled" : "disabled"}`);
|
|
320
370
|
console.log(`Skills: ${config.skillsEnabled ? "enabled" : "disabled"}`);
|
|
@@ -355,6 +405,8 @@ function printHelp() {
|
|
|
355
405
|
"Usage:",
|
|
356
406
|
" forgerelay Run first-time setup if needed, then start the server",
|
|
357
407
|
" forgerelay serve Start the server",
|
|
408
|
+
" forgerelay serve --allow-elevated",
|
|
409
|
+
" Explicitly allow this invocation to run with elevated/unknown OS privilege",
|
|
358
410
|
" forgerelay init Create or update ~/.forgerelay/config.json and auth.json",
|
|
359
411
|
" forgerelay doctor Show config, runtime, and native dependency status",
|
|
360
412
|
" forgerelay config get Print persisted config",
|
|
@@ -370,6 +422,7 @@ function printHelp() {
|
|
|
370
422
|
" forgerelay auth test <alias>",
|
|
371
423
|
" forgerelay auth rename <old-alias> <new-alias>",
|
|
372
424
|
" forgerelay auth remove <alias>",
|
|
425
|
+
" forgerelay maintenance inspect [--json]",
|
|
373
426
|
" forgerelay -v, --version Print the installed version",
|
|
374
427
|
"",
|
|
375
428
|
"For temporary tunnels:",
|
|
@@ -547,12 +600,15 @@ function printAgentsHelp() {
|
|
|
547
600
|
" forgerelay agents show <id>",
|
|
548
601
|
].join("\n"));
|
|
549
602
|
}
|
|
550
|
-
function
|
|
603
|
+
function installedForgeRelayVersion() {
|
|
551
604
|
const packageJson = require("../package.json");
|
|
552
|
-
if (typeof packageJson.version !== "string") {
|
|
605
|
+
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
|
553
606
|
throw new Error("Unable to read ForgeRelay package version.");
|
|
554
607
|
}
|
|
555
|
-
|
|
608
|
+
return packageJson.version;
|
|
609
|
+
}
|
|
610
|
+
function printVersion() {
|
|
611
|
+
console.log(installedForgeRelayVersion());
|
|
556
612
|
}
|
|
557
613
|
main(process.argv.slice(2)).catch((error) => {
|
|
558
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 {
|
|
@@ -7,6 +7,9 @@ export function publicEndpointUrl(baseUrl, suffix) {
|
|
|
7
7
|
url.hash = "";
|
|
8
8
|
return url;
|
|
9
9
|
}
|
|
10
|
+
export function publicEndpointPaths(baseUrls, suffix) {
|
|
11
|
+
return Array.from(new Set(baseUrls.map((baseUrl) => publicEndpointUrl(baseUrl, suffix).pathname)));
|
|
12
|
+
}
|
|
10
13
|
export function oauthAuthorizationServerMetadataPath(issuerUrl) {
|
|
11
14
|
const issuer = new URL(issuerUrl instanceof URL ? issuerUrl.href : issuerUrl);
|
|
12
15
|
const issuerPath = issuer.pathname === "/" ? "" : issuer.pathname.replace(/\/+$/, "");
|
package/dist/mcp/oauth/router.js
CHANGED
|
@@ -5,9 +5,9 @@ import { tokenHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/tok
|
|
|
5
5
|
import { clientRegistrationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/register.js";
|
|
6
6
|
import { revocationHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/revoke.js";
|
|
7
7
|
import { metadataHandler } from "@modelcontextprotocol/sdk/server/auth/handlers/metadata.js";
|
|
8
|
-
import { oauthAuthorizationServerMetadataPath, publicEndpointUrl, } from "./public-url.js";
|
|
8
|
+
import { oauthAuthorizationServerMetadataPath, publicEndpointPaths, publicEndpointUrl, } from "./public-url.js";
|
|
9
9
|
export function createForgeRelayAuthRouter(options) {
|
|
10
|
-
const { provider, cliAuthenticationProvider, instanceId, issuerUrl, resourceServerUrl, scopesSupported, resourceName, } = options;
|
|
10
|
+
const { provider, cliAuthenticationProvider, instanceId, issuerUrl, resourceServerUrl, routeBaseUrls = [issuerUrl], scopesSupported, resourceName, } = options;
|
|
11
11
|
const authorizationEndpoint = publicEndpointUrl(issuerUrl, "authorize");
|
|
12
12
|
const tokenEndpoint = publicEndpointUrl(issuerUrl, "token");
|
|
13
13
|
const registrationEndpoint = provider.clientsStore.registerClient
|
|
@@ -16,6 +16,11 @@ export function createForgeRelayAuthRouter(options) {
|
|
|
16
16
|
const revocationEndpoint = provider.revokeToken
|
|
17
17
|
? publicEndpointUrl(issuerUrl, "revoke")
|
|
18
18
|
: undefined;
|
|
19
|
+
const cliAuthenticationPaths = publicEndpointPaths(routeBaseUrls, "auth/cli");
|
|
20
|
+
const authorizationPaths = publicEndpointPaths(routeBaseUrls, "authorize");
|
|
21
|
+
const tokenPaths = publicEndpointPaths(routeBaseUrls, "token");
|
|
22
|
+
const registrationPaths = publicEndpointPaths(routeBaseUrls, "register");
|
|
23
|
+
const revocationPaths = publicEndpointPaths(routeBaseUrls, "revoke");
|
|
19
24
|
const oauthMetadata = {
|
|
20
25
|
...createOAuthMetadata({
|
|
21
26
|
provider,
|
|
@@ -36,7 +41,7 @@ export function createForgeRelayAuthRouter(options) {
|
|
|
36
41
|
};
|
|
37
42
|
const router = express.Router();
|
|
38
43
|
if (cliAuthenticationProvider) {
|
|
39
|
-
router.post(
|
|
44
|
+
router.post(cliAuthenticationPaths, express.json({ limit: "4kb" }), (req, res) => {
|
|
40
45
|
const ownerToken = typeof req.body?.owner_token === "string" ? req.body.owner_token : undefined;
|
|
41
46
|
const refreshToken = typeof req.body?.refresh_token === "string" ? req.body.refresh_token : undefined;
|
|
42
47
|
if ((ownerToken ? 1 : 0) + (refreshToken ? 1 : 0) !== 1) {
|
|
@@ -55,13 +60,13 @@ export function createForgeRelayAuthRouter(options) {
|
|
|
55
60
|
res.status(200).json({ ...tokens, ...(instanceId ? { instance_id: instanceId } : {}) });
|
|
56
61
|
});
|
|
57
62
|
}
|
|
58
|
-
router.use(
|
|
59
|
-
router.use(
|
|
60
|
-
if (provider.clientsStore.registerClient) {
|
|
61
|
-
router.use(
|
|
63
|
+
router.use(authorizationPaths, authorizationHandler({ provider }));
|
|
64
|
+
router.use(tokenPaths, tokenHandler({ provider }));
|
|
65
|
+
if (provider.clientsStore.registerClient && registrationEndpoint) {
|
|
66
|
+
router.use(registrationPaths, clientRegistrationHandler({ clientsStore: provider.clientsStore }));
|
|
62
67
|
}
|
|
63
|
-
if (provider.revokeToken) {
|
|
64
|
-
router.use(
|
|
68
|
+
if (provider.revokeToken && revocationEndpoint) {
|
|
69
|
+
router.use(revocationPaths, revocationHandler({ provider }));
|
|
65
70
|
}
|
|
66
71
|
router.use(oauthAuthorizationServerMetadataPath(issuerUrl), metadataHandler(oauthMetadata));
|
|
67
72
|
router.use(new URL(getOAuthProtectedResourceMetadataUrl(resourceServerUrl)).pathname, metadataHandler(protectedResourceMetadata));
|
|
@@ -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, {
|
|
@@ -16,7 +16,7 @@ import { createOpenAIIncomingArtifactAdapter } from "../../artifacts/incoming-ar
|
|
|
16
16
|
import { logEvent, requestPath, transportSessionIdPrefix } from "../../../runtime/logging/logger.js";
|
|
17
17
|
import { SingleUserOAuthProvider } from "../../oauth/oauth-provider.js";
|
|
18
18
|
import { createForgeRelayAuthRouter } from "../../oauth/router.js";
|
|
19
|
-
import { publicEndpointUrl } from "../../oauth/public-url.js";
|
|
19
|
+
import { publicEndpointPaths, publicEndpointUrl } from "../../oauth/public-url.js";
|
|
20
20
|
import { McpTransportRegistry } from "./mcp-sessions.js";
|
|
21
21
|
import { ProcessManager } from "../../process/process-sessions.js";
|
|
22
22
|
import { createReviewCheckpointManager } from "../../../workspaces/review/review-checkpoints.js";
|
|
@@ -45,7 +45,11 @@ export function createHttpServer(config, options, createMcpServer) {
|
|
|
45
45
|
const transports = new McpTransportRegistry({
|
|
46
46
|
maxTransports: MAX_MCP_TRANSPORT_SESSIONS,
|
|
47
47
|
});
|
|
48
|
+
const routeBaseUrls = config.publicBaseUrls.map((baseUrl) => new URL(baseUrl));
|
|
48
49
|
const mcpUrl = publicEndpointUrl(config.publicBaseUrl, "mcp");
|
|
50
|
+
const mcpPaths = publicEndpointPaths(routeBaseUrls, "mcp");
|
|
51
|
+
const activityPanelAssetsPaths = publicEndpointPaths(routeBaseUrls, "mcp-app-assets");
|
|
52
|
+
const healthPaths = publicEndpointPaths(routeBaseUrls, "healthz");
|
|
49
53
|
const resourceServerUrl = resourceUrlFromServerUrl(mcpUrl);
|
|
50
54
|
const oauthProvider = new SingleUserOAuthProvider(config.oauth, mcpUrl, config.stateDir);
|
|
51
55
|
const bearerAuth = requireBearerAuth({
|
|
@@ -67,7 +71,10 @@ export function createHttpServer(config, options, createMcpServer) {
|
|
|
67
71
|
turnIdForConversation: (conversationScopeId, workspaceId) => activityQueries.currentTurnId(conversationScopeId, workspaceId),
|
|
68
72
|
});
|
|
69
73
|
const reviewCheckpoints = createReviewCheckpointManager();
|
|
70
|
-
const processSessions = new ProcessManager({
|
|
74
|
+
const processSessions = new ProcessManager({
|
|
75
|
+
outputAudit: bashOutputStore,
|
|
76
|
+
commandShellRuntime: config.commandShellRuntime,
|
|
77
|
+
});
|
|
71
78
|
const codeIntelligence = new CodeIntelligenceManager(config);
|
|
72
79
|
const subagentProviders = config.subagents
|
|
73
80
|
? getSubagentProviderAvailabilitySnapshot()
|
|
@@ -140,8 +147,8 @@ export function createHttpServer(config, options, createMcpServer) {
|
|
|
140
147
|
}, MCP_TRANSPORT_CLEANUP_INTERVAL_MS);
|
|
141
148
|
transportCleanupTimer.unref();
|
|
142
149
|
logRuntimeResources();
|
|
143
|
-
if (config.
|
|
144
|
-
app.set("trust proxy",
|
|
150
|
+
if (config.proxyTrust !== false) {
|
|
151
|
+
app.set("trust proxy", config.proxyTrust);
|
|
145
152
|
}
|
|
146
153
|
app.use((req, res, next) => {
|
|
147
154
|
const requestId = randomUUID();
|
|
@@ -151,7 +158,7 @@ export function createHttpServer(config, options, createMcpServer) {
|
|
|
151
158
|
const path = requestPath(req);
|
|
152
159
|
if (!config.logging.requests)
|
|
153
160
|
return;
|
|
154
|
-
if (!config.logging.assets && path.startsWith(
|
|
161
|
+
if (!config.logging.assets && activityPanelAssetsPaths.some((assetPath) => path.startsWith(assetPath)))
|
|
155
162
|
return;
|
|
156
163
|
logEvent(config.logging, "info", "http_request", {
|
|
157
164
|
requestId,
|
|
@@ -170,23 +177,24 @@ export function createHttpServer(config, options, createMcpServer) {
|
|
|
170
177
|
instanceId: config.instanceId,
|
|
171
178
|
issuerUrl: new URL(config.publicBaseUrl),
|
|
172
179
|
resourceServerUrl,
|
|
180
|
+
routeBaseUrls,
|
|
173
181
|
scopesSupported: config.oauth.scopes,
|
|
174
182
|
resourceName: "ForgeRelay",
|
|
175
183
|
}));
|
|
176
|
-
app.options(
|
|
184
|
+
app.options(activityPanelAssetsPaths.map((assetPath) => `${assetPath}/{*asset}`), (_req, res) => {
|
|
177
185
|
setActivityPanelAssetHeaders(res);
|
|
178
186
|
res.sendStatus(204);
|
|
179
187
|
});
|
|
180
|
-
app.use(
|
|
188
|
+
app.use(activityPanelAssetsPaths, express.static(activityPanelAssetDirectory(), {
|
|
181
189
|
immutable: true,
|
|
182
190
|
maxAge: "1y",
|
|
183
191
|
fallthrough: false,
|
|
184
192
|
setHeaders: setActivityPanelAssetHeaders,
|
|
185
193
|
}));
|
|
186
|
-
app.get(
|
|
194
|
+
app.get(healthPaths, (_req, res) => {
|
|
187
195
|
res.json({ ok: true, name: "forgerelay" });
|
|
188
196
|
});
|
|
189
|
-
app.all(
|
|
197
|
+
app.all(mcpPaths, async (req, res) => {
|
|
190
198
|
const requestId = res.locals.requestId;
|
|
191
199
|
const transportSessionId = req.header("mcp-session-id");
|
|
192
200
|
const initializeRequest = req.method === "POST" && isInitializeRequest(req.body);
|
|
@@ -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: {
|