@akira-tl/forgerelay 1.1.2 → 1.2.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 +38 -0
- package/README.md +14 -0
- package/dist/activity/runtime/mcp-query-tools.js +15 -5
- package/dist/cli/config/inspect.js +331 -0
- package/dist/cli/config/migrate.js +351 -0
- package/dist/cli/init/setup-config.js +41 -0
- package/dist/cli/init.js +128 -130
- package/dist/cli/maintenance.js +15 -5
- package/dist/cli/mcp/external-mcp.js +14 -10
- package/dist/cli/mcp/status.js +11 -6
- package/dist/cli.js +40 -7
- package/dist/lsp/language-server-config.js +66 -144
- package/dist/lsp/runtime/manager.js +35 -4
- package/dist/mcp/hooks/config.js +141 -0
- package/dist/mcp/hooks/external-mcp-transform.js +22 -13
- package/dist/mcp/hooks/hook-cli.js +62 -52
- package/dist/mcp/hooks/hooks.js +111 -85
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +8 -5
- package/dist/mcp/operations/external-mcp/external-mcp.js +9 -2
- package/dist/mcp/server/core/capability-support.js +7 -0
- package/dist/runtime/config/config.js +142 -64
- package/dist/runtime/config/definition/catalog.js +12 -0
- package/dist/runtime/config/definition/definition.js +115 -0
- package/dist/runtime/config/definition/external-mcp.js +58 -0
- package/dist/runtime/config/definition/general-config.js +258 -0
- package/dist/runtime/config/definition/language-servers.js +116 -0
- package/dist/runtime/config/definition/schema.js +113 -0
- package/dist/runtime/config/definition/types.js +2 -0
- package/dist/runtime/config/external-mcp-auth-store.js +105 -29
- package/dist/runtime/config/external-mcp-config.js +39 -0
- package/dist/runtime/config/external-mcp-registry.js +284 -106
- package/dist/runtime/config/inspection/live.js +77 -0
- package/dist/runtime/config/resolution/general.js +93 -0
- package/dist/runtime/config/resolution/hooks.js +315 -0
- package/dist/runtime/config/resolution/language-servers.js +237 -0
- package/dist/runtime/config/resolution/project-sources.js +159 -0
- package/dist/runtime/config/resolution/resolver.js +485 -0
- package/dist/runtime/config/resolution/types.js +1 -0
- package/dist/runtime/config/runtime/config-runtime.js +74 -0
- package/dist/runtime/config/runtime/source-refresh.js +263 -0
- package/dist/runtime/config/user-config.js +38 -31
- package/dist/runtime/security/project-execution-trust.js +34 -0
- package/dist/runtime/testing/server-fixture.js +9 -8
- package/dist/server.js +10 -6
- package/dist/subagents/profiles.js +336 -53
- package/dist/subagents/sessions/execution.js +6 -35
- package/dist/subagents/sessions/manager.js +14 -2
- package/dist/ui/.vite/manifest.json +275 -275
- package/dist/ui/activity-panel-app.html +1 -1
- package/dist/ui/assets/{activity-panel-app-Bw7ZFX2g.js → activity-panel-app-BeDiwd7D.js} +3 -3
- package/dist/ui/assets/{angular-html-DeUNP12X.js → angular-html-DuoCE7nn.js} +1 -1
- package/dist/ui/assets/{angular-ts-B3v8FKi_.js → angular-ts-D5tGk0hL.js} +1 -1
- package/dist/ui/assets/{apl-Bs3iQm4P.js → apl-lciOTPbw.js} +1 -1
- package/dist/ui/assets/{astro-D6GHecBY.js → astro-vJbqfNnJ.js} +1 -1
- package/dist/ui/assets/{blade-BMNbVOHv.js → blade-C7655Q4K.js} +1 -1
- package/dist/ui/assets/{c-BquuA6bv.js → c-DdHp1Jrw.js} +1 -1
- package/dist/ui/assets/{cobol-BuZ6kfps.js → cobol-DgvzQxZP.js} +1 -1
- package/dist/ui/assets/{coffee-DgC5uBzL.js → coffee-BIjVUOUd.js} +1 -1
- package/dist/ui/assets/{cpp-BdvWqCaB.js → cpp-CU8tGzvI.js} +1 -1
- package/dist/ui/assets/{crystal-0Q5qBvNI.js → crystal-CUQAwsg9.js} +1 -1
- package/dist/ui/assets/{css-Be_T8idh.js → css-DtMTqD3F.js} +1 -1
- package/dist/ui/assets/{edge-DaGLdgh_.js → edge-DoCX9RnL.js} +1 -1
- package/dist/ui/assets/{elixir-CMgiFIzB.js → elixir-DJxV9_U6.js} +1 -1
- package/dist/ui/assets/{elm-B-ufMlMC.js → elm-DIgWqK4r.js} +1 -1
- package/dist/ui/assets/{erb-CaGiOZM-.js → erb-BHxP0c_e.js} +1 -1
- package/dist/ui/assets/{git-rebase-CpwUWCzK.js → git-rebase-HbI_Vo8d.js} +1 -1
- package/dist/ui/assets/{glimmer-js-BRxccvnE.js → glimmer-js-CthY4Ty_.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-CgQbPzlT.js → glimmer-ts-B2-_A1Xz.js} +1 -1
- package/dist/ui/assets/{glsl-D7JmHFr0.js → glsl-D9V9gq98.js} +1 -1
- package/dist/ui/assets/{graphql-C8n0eUBK.js → graphql-CddeAaS3.js} +1 -1
- package/dist/ui/assets/{hack-lVN7KVQv.js → hack-D_loGkOT.js} +1 -1
- package/dist/ui/assets/{haml-h18lG0pS.js → haml-BoWDczd0.js} +1 -1
- package/dist/ui/assets/{handlebars-lRFjRPfj.js → handlebars-ZoSOvw9y.js} +1 -1
- package/dist/ui/assets/{heavy-payload-Cqx9MOkH.js → heavy-payload-DO34LwoZ.js} +3 -3
- package/dist/ui/assets/{html-DtILqTE1.js → html-CE-obXVo.js} +1 -1
- package/dist/ui/assets/{html-derivative-CSTSmPup.js → html-derivative-Cd-smAbW.js} +1 -1
- package/dist/ui/assets/{http-DNI8fsbY.js → http-CZrkbrSl.js} +1 -1
- package/dist/ui/assets/{hurl-DgRi60JG.js → hurl-CxTuxBt1.js} +1 -1
- package/dist/ui/assets/{java-CW9pZ5bh.js → java-BshLjCat.js} +1 -1
- package/dist/ui/assets/{javascript-Cvg2UrWL.js → javascript-D2pxQTiX.js} +1 -1
- package/dist/ui/assets/{jinja-CZw_S1FB.js → jinja-kbfPP7-F.js} +1 -1
- package/dist/ui/assets/{jison-C56u_FZv.js → jison-C2mt9l1M.js} +1 -1
- package/dist/ui/assets/{json-DW2UTv1q.js → json-DU_rg_AU.js} +1 -1
- package/dist/ui/assets/{jsx-Zj5gobdD.js → jsx-D_8aQMlx.js} +1 -1
- package/dist/ui/assets/{julia-Cvcrre-L.js → julia-BmefTaVV.js} +1 -1
- package/dist/ui/assets/{just-8CZ2u6Tj.js → just-DXV8vk1S.js} +1 -1
- package/dist/ui/assets/{latex-D289AXWG.js → latex-Cjm2MvGW.js} +1 -1
- package/dist/ui/assets/{liquid-CFEX3SkZ.js → liquid-DyQDlo0u.js} +1 -1
- package/dist/ui/assets/{lua-BKpJ0ybO.js → lua-B2A1vzzy.js} +1 -1
- package/dist/ui/assets/{marko-D9maZ0a6.js → marko-B9vOkXOU.js} +1 -1
- package/dist/ui/assets/{mdc-rM9l0qWR.js → mdc-D42m6spU.js} +1 -1
- package/dist/ui/assets/{nginx-C9w6p2C9.js → nginx-TpfVxSUY.js} +1 -1
- package/dist/ui/assets/{nim-BRePHk0J.js → nim-CJr5T1FK.js} +1 -1
- package/dist/ui/assets/{perl-CskTmiPQ.js → perl-cJZcsdm9.js} +1 -1
- package/dist/ui/assets/{php-DCU_FvNI.js → php-CSZL5KRT.js} +1 -1
- package/dist/ui/assets/{pug-XLaVxTvv.js → pug-B1eMKzuA.js} +1 -1
- package/dist/ui/assets/{qml-CaONKSl-.js → qml-Dl2_7zc9.js} +1 -1
- package/dist/ui/assets/{r-BCVR0ZE3.js → r-F0aaO9gG.js} +1 -1
- package/dist/ui/assets/{razor-DDnGXxnU.js → razor-BPnkSMCw.js} +1 -1
- package/dist/ui/assets/{regexp-DWGTYYVx.js → regexp-BnGHMFbv.js} +1 -1
- package/dist/ui/assets/{rst-Dz3GWvQm.js → rst-B3CO7WYO.js} +1 -1
- package/dist/ui/assets/{ruby-DC3jGtMY.js → ruby-B6VMzT1T.js} +1 -1
- package/dist/ui/assets/{sas-7mNpvsjZ.js → sas-Df2lHVuU.js} +1 -1
- package/dist/ui/assets/{scss-uBsoCJVy.js → scss-4XxI6sgu.js} +1 -1
- package/dist/ui/assets/{shellscript-BCN9N_Jf.js → shellscript-DYASCN4r.js} +1 -1
- package/dist/ui/assets/{shellsession-Hsw46VXV.js → shellsession-Br6AGJO_.js} +1 -1
- package/dist/ui/assets/{soy-D6KV69HU.js → soy-p3ZNq1We.js} +1 -1
- package/dist/ui/assets/{sql-CVzVQkJh.js → sql-CAdv2OXL.js} +1 -1
- package/dist/ui/assets/{stata-BwUjXNhI.js → stata-Dx7yUbG_.js} +1 -1
- package/dist/ui/assets/{surrealql-CgloWqRa.js → surrealql-6YsE8HQa.js} +1 -1
- package/dist/ui/assets/{svelte-DcC3n6IC.js → svelte-YgzedwpD.js} +1 -1
- package/dist/ui/assets/{templ-DOFfnWTJ.js → templ-Bahqx3uD.js} +1 -1
- package/dist/ui/assets/{tex-ln89VPaX.js → tex-o6fTQjkH.js} +1 -1
- package/dist/ui/assets/{ts-tags-CWMzCstH.js → ts-tags-BO-Co0Ju.js} +1 -1
- package/dist/ui/assets/{tsx-DgzfiGeK.js → tsx-DtQe5AoH.js} +1 -1
- package/dist/ui/assets/{twig-Bi2itZL9.js → twig-BcUs6Kqc.js} +1 -1
- package/dist/ui/assets/{typescript-CQ24f4zW.js → typescript-BPtM8h4u.js} +1 -1
- package/dist/ui/assets/{vue-BS8jE5u6.js → vue-5KkGUzJJ.js} +1 -1
- package/dist/ui/assets/{vue-html-o38xqhyB.js → vue-html-Df_Xfsz9.js} +1 -1
- package/dist/ui/assets/{vue-vine-D2NvKv50.js → vue-vine-hUIe_u_Y.js} +1 -1
- package/dist/ui/assets/{xml-_44rx9_2.js → xml-C3Jy7as5.js} +1 -1
- package/dist/ui/assets/{xsl-B5QWQPoi.js → xsl-BnF9OuFo.js} +1 -1
- package/dist/ui/assets/{yaml-CKL2pEWe.js → yaml-DyrFgCou.js} +1 -1
- package/dist/workspaces/conversation-test-support.js +3 -11
- package/dist/workspaces/sessions.js +2 -0
- package/dist/workspaces/state/project-context.js +221 -0
- package/dist/workspaces.js +3 -1
- package/docs/chatgpt-coding-workflow.md +8 -3
- package/docs/configuration.md +99 -41
- package/docs/gotchas.md +24 -5
- package/docs/security.md +14 -3
- package/docs/setup.md +12 -7
- package/package.json +12 -3
- package/schemas/v1/config.project-local.schema.json +14 -0
- package/schemas/v1/config.project.schema.json +14 -0
- package/schemas/v1/config.user.schema.json +538 -0
- package/schemas/v1/hooks.project-local.schema.json +118 -0
- package/schemas/v1/hooks.project.schema.json +118 -0
- package/schemas/v1/hooks.user.schema.json +118 -0
- package/schemas/v1/language-servers.project-local.schema.json +230 -0
- package/schemas/v1/language-servers.project.schema.json +230 -0
- package/schemas/v1/language-servers.user.schema.json +230 -0
- package/schemas/v1/mcp.project-local.schema.json +143 -0
- package/schemas/v1/mcp.project.schema.json +143 -0
- package/schemas/v1/mcp.user.schema.json +143 -0
- package/scripts/ci/architecture.mjs +54 -4
- package/scripts/ci/cmd-acceptance.mjs +9 -9
- package/scripts/ci/config-v2-product-acceptance.mjs +639 -0
- package/scripts/ci/gates/acceptance-artifact.mjs +66 -0
- package/scripts/ci/gates/core-contract.mjs +27 -0
- package/scripts/ci/gates/platform-tests.mjs +38 -0
- package/scripts/ci/gates/prepare-windows-product.mjs +57 -0
- package/scripts/ci/gates/test-shard.mjs +71 -0
- package/scripts/ci/powershell51-acceptance.mjs +9 -9
- package/scripts/ci/pwsh-acceptance.mjs +9 -9
- package/scripts/ci/verify.mjs +2 -0
- package/scripts/ci/windows-product-acceptance.mjs +9 -10
- package/scripts/config/schema-text.mjs +4 -0
- package/scripts/config/schema-text.test.mjs +18 -0
- package/scripts/config/schema.mjs +74 -0
- package/scripts/lsp/interop.mjs +4 -1
- package/scripts/release/pack.mjs +64 -4
- package/scripts/release/release-gate.test.mjs +93 -27
- package/scripts/release-parity.mjs +19 -2
- package/templates/instructions/cmd.md +12 -0
- package/templates/instructions/fish.md +11 -0
- package/templates/instructions/powershell.md +14 -0
- package/templates/instructions/pwsh.md +14 -0
- package/templates/instructions/zsh.md +11 -0
- package/dist/runtime/instructions/powershell-skill.js +0 -65
|
@@ -2,25 +2,18 @@ import { isIP } from "node:net";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { expandHomePath } from "../../mcp/filesystem/roots.js";
|
|
5
|
-
import {
|
|
6
|
-
import { forgerelayAgentsDir, forgerelaySkillsDir, generateInstanceId, loadForgeRelayFiles, } from "./user-config.js";
|
|
7
|
-
import { parseExternalMcpServers, } from "./external-mcp-config.js";
|
|
5
|
+
import { forgerelaySkillsDir, generateInstanceId, loadForgeRelayFiles, } from "./user-config.js";
|
|
8
6
|
import { DEFAULT_MEDIA_MAX_BYTES } from "../../mcp/operations/media-content.js";
|
|
9
7
|
import { shellInstructionPath } from "../instructions/shell-instructions.js";
|
|
10
8
|
import { resolveConfiguredCommandShellRuntime, } from "../shell/command-shell-runtime.js";
|
|
9
|
+
import { generalConfigDefinition } from "./definition/general-config.js";
|
|
10
|
+
import { resolveGeneralConfig } from "./resolution/general.js";
|
|
11
|
+
import { assertConfigResolutionValid } from "./resolution/resolver.js";
|
|
12
|
+
import { ConfigRuntime } from "./runtime/config-runtime.js";
|
|
11
13
|
const DEFAULT_OAUTH_ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
12
14
|
const DEFAULT_OAUTH_REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
13
15
|
const DEFAULT_ARTIFACT_MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
14
16
|
const DEFAULT_TASK_REMINDER_INTERVAL = 30;
|
|
15
|
-
function parsePort(value) {
|
|
16
|
-
if (value === undefined || value === "")
|
|
17
|
-
return 7676;
|
|
18
|
-
const port = Number(value);
|
|
19
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
20
|
-
throw new Error(`Invalid PORT: ${value}`);
|
|
21
|
-
}
|
|
22
|
-
return port;
|
|
23
|
-
}
|
|
24
17
|
function parseAllowedRoots(value) {
|
|
25
18
|
if (Array.isArray(value)) {
|
|
26
19
|
const roots = value.map((entry) => entry.trim()).filter(Boolean);
|
|
@@ -114,15 +107,6 @@ function parsePositiveInteger(value, fallback, name, max = Number.MAX_SAFE_INTEG
|
|
|
114
107
|
}
|
|
115
108
|
return parsed;
|
|
116
109
|
}
|
|
117
|
-
function parseNonNegativeInteger(value, fallback, name) {
|
|
118
|
-
if (value === undefined || value === "")
|
|
119
|
-
return fallback;
|
|
120
|
-
const parsed = Number(value);
|
|
121
|
-
if (!Number.isInteger(parsed) || parsed < 0 || parsed > Number.MAX_SAFE_INTEGER) {
|
|
122
|
-
throw new Error(`Invalid ${name}: ${value}`);
|
|
123
|
-
}
|
|
124
|
-
return parsed;
|
|
125
|
-
}
|
|
126
110
|
function parseLoggingConfig(env, trustProxy) {
|
|
127
111
|
const format = parseLogFormat(productEnv(env, "LOG_FORMAT"));
|
|
128
112
|
const requests = productEnv(env, "LOG_REQUESTS");
|
|
@@ -148,8 +132,7 @@ function resolveProxyTrust(env, config, host, publicBaseUrl) {
|
|
|
148
132
|
}
|
|
149
133
|
return ["loopback"];
|
|
150
134
|
}
|
|
151
|
-
const
|
|
152
|
-
const explicitTrustedProxies = parseTrustedProxies(envTrustedProxies === undefined ? config.trustedProxies : envTrustedProxies);
|
|
135
|
+
const explicitTrustedProxies = parseTrustedProxies(config.trustedProxies);
|
|
153
136
|
if (explicitTrustedProxies !== undefined)
|
|
154
137
|
return explicitTrustedProxies;
|
|
155
138
|
return isLoopbackHost(host) && !isLoopbackHost(new URL(publicBaseUrl).hostname)
|
|
@@ -253,81 +236,176 @@ function parsePublicBaseUrls(value, fallback) {
|
|
|
253
236
|
const normalized = (raw ?? fallback).map((entry) => parsePublicBaseUrl(entry));
|
|
254
237
|
return Array.from(new Set(normalized));
|
|
255
238
|
}
|
|
256
|
-
function resolvePublicDeployment(
|
|
239
|
+
function resolvePublicDeployment(configuredValue, host, port) {
|
|
257
240
|
const localBaseUrls = [parsePublicBaseUrl(localPublicBaseUrl(host, port))];
|
|
258
|
-
const
|
|
259
|
-
const baseUrls = envBaseUrl !== undefined
|
|
260
|
-
? parsePublicBaseUrls(envBaseUrl, localBaseUrls)
|
|
261
|
-
: parsePublicBaseUrls(fileConfig.publicBaseUrl, localBaseUrls);
|
|
241
|
+
const baseUrls = parsePublicBaseUrls(configuredValue, localBaseUrls);
|
|
262
242
|
return {
|
|
263
243
|
baseUrls,
|
|
264
244
|
canonicalBaseUrl: baseUrls[0],
|
|
265
245
|
};
|
|
266
246
|
}
|
|
267
|
-
export function loadConfig(env = process.env) {
|
|
247
|
+
export function loadConfig(env = process.env, options = {}) {
|
|
248
|
+
const configRuntime = new ConfigRuntime();
|
|
249
|
+
const runtimeEnvironment = generalConfigRuntimeEnvironment(env);
|
|
250
|
+
configRuntime.captureResolutionInputs(generalConfigDefinition.domain, {
|
|
251
|
+
environment: runtimeEnvironment,
|
|
252
|
+
...(options.runtimeOverrides ? { cli: options.runtimeOverrides } : {}),
|
|
253
|
+
});
|
|
268
254
|
const files = loadForgeRelayFiles(env);
|
|
255
|
+
const generalResolution = resolveGeneralConfig({
|
|
256
|
+
env,
|
|
257
|
+
...(options.runtimeOverrides ? { cli: options.runtimeOverrides } : {}),
|
|
258
|
+
...(files.configExists ? { user: files.config, userSourcePath: files.configPath } : {}),
|
|
259
|
+
...(options.projectConfig !== undefined
|
|
260
|
+
? { project: options.projectConfig, projectSourcePath: options.projectConfigPath }
|
|
261
|
+
: {}),
|
|
262
|
+
...(options.projectLocalConfig !== undefined
|
|
263
|
+
? { projectLocal: options.projectLocalConfig, projectLocalSourcePath: options.projectLocalConfigPath }
|
|
264
|
+
: {}),
|
|
265
|
+
});
|
|
266
|
+
assertConfigResolutionValid(generalResolution);
|
|
267
|
+
const config = generalResolution.values;
|
|
268
|
+
refreshGeneralUserConfigSource(configRuntime, files.configPath, runtimeEnvironment);
|
|
269
269
|
const instanceId = files.auth.instanceId?.trim() || generateInstanceId();
|
|
270
|
-
const host =
|
|
271
|
-
const port =
|
|
272
|
-
const publicDeployment = resolvePublicDeployment(
|
|
270
|
+
const host = config.host ?? "127.0.0.1";
|
|
271
|
+
const port = config.port ?? 7676;
|
|
272
|
+
const publicDeployment = resolvePublicDeployment(config.publicBaseUrl, host, port);
|
|
273
273
|
const publicBaseUrl = publicDeployment.canonicalBaseUrl;
|
|
274
|
-
const proxyTrust = resolveProxyTrust(env,
|
|
275
|
-
const commandShellRuntime = resolveConfiguredCommandShellRuntime(
|
|
274
|
+
const proxyTrust = resolveProxyTrust(env, config, host, publicBaseUrl);
|
|
275
|
+
const commandShellRuntime = resolveConfiguredCommandShellRuntime(config.commandShell, process.platform, env);
|
|
276
276
|
const derivedAllowedHosts = [
|
|
277
277
|
"localhost",
|
|
278
278
|
"127.0.0.1",
|
|
279
279
|
"::1",
|
|
280
280
|
host,
|
|
281
281
|
...publicDeployment.baseUrls.map((baseUrl) => new URL(baseUrl).hostname),
|
|
282
|
-
...(
|
|
282
|
+
...(config.allowedHosts ?? []),
|
|
283
283
|
];
|
|
284
|
-
|
|
284
|
+
const serverConfig = {
|
|
285
|
+
configRuntime,
|
|
285
286
|
instanceId,
|
|
286
287
|
configDir: files.dir,
|
|
287
288
|
host,
|
|
288
289
|
port,
|
|
289
290
|
oauth: parseOAuthConfig(env, files.auth.ownerToken),
|
|
290
|
-
allowedRoots: parseAllowedRoots(
|
|
291
|
-
allowedHosts: parseAllowedHosts(
|
|
291
|
+
allowedRoots: parseAllowedRoots(config.allowedRoots),
|
|
292
|
+
allowedHosts: parseAllowedHosts(config.allowedHosts, derivedAllowedHosts),
|
|
292
293
|
publicBaseUrl,
|
|
293
294
|
publicBaseUrls: publicDeployment.baseUrls,
|
|
294
295
|
proxyTrust,
|
|
295
296
|
toolMode: parseToolMode(env),
|
|
296
|
-
workflowInstructions: parseWorkflowInstructions(
|
|
297
|
-
appendInstructions: parseAppendInstructions(
|
|
297
|
+
workflowInstructions: parseWorkflowInstructions(undefined, config.workflowInstructions),
|
|
298
|
+
appendInstructions: parseAppendInstructions(undefined, config.appendInstructions),
|
|
298
299
|
widgets: parseWidgetMode(productEnv(env, "WIDGETS")),
|
|
299
|
-
activityPanelExpanded:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
: parseBoolean(productEnv(env, "ARTIFACTS")),
|
|
307
|
-
artifactMaxFileBytes: parsePositiveInteger(productEnv(env, "ARTIFACT_MAX_FILE_BYTES") ?? numberConfigValue(files.config.artifactMaxFileBytes), DEFAULT_ARTIFACT_MAX_FILE_BYTES, "FORGERELAY_ARTIFACT_MAX_FILE_BYTES"),
|
|
308
|
-
mediaMaxBytes: parsePositiveInteger(productEnv(env, "MEDIA_MAX_BYTES") ?? numberConfigValue(files.config.mediaMaxBytes), DEFAULT_MEDIA_MAX_BYTES, "FORGERELAY_MEDIA_MAX_BYTES"),
|
|
309
|
-
taskReminderInterval: parseNonNegativeInteger(productEnv(env, "TASK_REMINDER_INTERVAL") ?? numberConfigValue(files.config.taskReminderInterval), DEFAULT_TASK_REMINDER_INTERVAL, "FORGERELAY_TASK_REMINDER_INTERVAL"),
|
|
300
|
+
activityPanelExpanded: config.activityPanelExpanded === true,
|
|
301
|
+
stateDir: resolve(expandHomePath(config.stateDir ?? defaultStateDir())),
|
|
302
|
+
worktreeRoot: resolve(expandHomePath(config.worktreeRoot ?? defaultWorktreeRoot())),
|
|
303
|
+
artifactsEnabled: config.artifactsEnabled === true,
|
|
304
|
+
artifactMaxFileBytes: config.artifactMaxFileBytes ?? DEFAULT_ARTIFACT_MAX_FILE_BYTES,
|
|
305
|
+
mediaMaxBytes: config.mediaMaxBytes ?? DEFAULT_MEDIA_MAX_BYTES,
|
|
306
|
+
taskReminderInterval: config.taskReminderInterval ?? DEFAULT_TASK_REMINDER_INTERVAL,
|
|
310
307
|
skillsEnabled: productEnv(env, "SKILLS") === undefined ? true : parseBoolean(productEnv(env, "SKILLS")),
|
|
311
308
|
skillPaths: parsePathList(productEnv(env, "SKILL_PATHS")),
|
|
312
309
|
configSkillsDir: forgerelaySkillsDir(env),
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
languageServers: files.config.languageServers ?? {},
|
|
318
|
-
allowAgentLanguageServerInstall: files.config.allowAgentLanguageServerInstall === true,
|
|
319
|
-
mcpServers: parseExternalMcpServers(files.config.mcpServers),
|
|
320
|
-
agentDir: resolve(expandHomePath(productEnv(env, "AGENT_DIR") ?? files.config.agentDir ?? defaultAgentDir())),
|
|
321
|
-
systemInstructionsPath: parseSystemInstructionsPath(productEnv(env, "SYSTEM_INSTRUCTIONS_PATH") ?? files.config.systemInstructionsPath),
|
|
322
|
-
hooks: mergeHookConfigs(parseHookConfig(files.config.hooks), parseHookConfig(files.hooks), files.hookFiles),
|
|
310
|
+
subagents: config.subagents === true,
|
|
311
|
+
allowAgentLanguageServerInstall: config.allowAgentLanguageServerInstall === true,
|
|
312
|
+
agentDir: resolve(expandHomePath(config.agentDir ?? defaultAgentDir())),
|
|
313
|
+
systemInstructionsPath: parseSystemInstructionsPath(config.systemInstructionsPath),
|
|
323
314
|
logging: parseLoggingConfig(env, proxyTrust !== false),
|
|
324
315
|
commandShellRuntime,
|
|
325
|
-
shellInstructionsEnabled:
|
|
316
|
+
shellInstructionsEnabled: config.shellInstructions !== false,
|
|
326
317
|
shellInstructionPath: shellInstructionPath(files.dir, commandShellRuntime.family),
|
|
327
318
|
};
|
|
319
|
+
configRuntime.captureApplied(generalConfigDefinition, restartRequiredGeneralValues(config, runtimeEnvironment));
|
|
320
|
+
return serverConfig;
|
|
321
|
+
}
|
|
322
|
+
export function liveGeneralConfigState(config) {
|
|
323
|
+
const inputs = config.configRuntime.resolutionInputsFor(generalConfigDefinition.domain);
|
|
324
|
+
const configPath = join(config.configDir, "config.json");
|
|
325
|
+
const refreshed = refreshGeneralUserConfigSource(config.configRuntime, configPath, inputs.environment);
|
|
326
|
+
const resolution = resolveGeneralConfig({
|
|
327
|
+
env: inputs.environment,
|
|
328
|
+
...(inputs.cli ? { cli: inputs.cli } : {}),
|
|
329
|
+
...(refreshed.value !== undefined
|
|
330
|
+
? { user: refreshed.value, userSourcePath: configPath }
|
|
331
|
+
: {}),
|
|
332
|
+
});
|
|
333
|
+
const configured = resolution.values;
|
|
334
|
+
return {
|
|
335
|
+
applied: config.configRuntime.snapshotApplied(generalConfigDefinition, restartRequiredGeneralValues(configured, inputs.environment)),
|
|
336
|
+
...(refreshed.status.state === "invalid" && refreshed.issue
|
|
337
|
+
? {
|
|
338
|
+
source: {
|
|
339
|
+
state: "invalid",
|
|
340
|
+
usingLastKnownGood: refreshed.status.usingLastKnownGood,
|
|
341
|
+
message: refreshed.issue.message,
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
: {}),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function refreshGeneralUserConfigSource(runtime, configPath, environment) {
|
|
348
|
+
return runtime.sources.refreshFile({
|
|
349
|
+
key: `general:user:${configPath}`,
|
|
350
|
+
path: configPath,
|
|
351
|
+
parse: (raw) => JSON.parse(raw),
|
|
352
|
+
parseIssue: {
|
|
353
|
+
code: "invalid_source",
|
|
354
|
+
message: "General configuration is not valid JSON.",
|
|
355
|
+
},
|
|
356
|
+
readIssue: {
|
|
357
|
+
code: "invalid_source",
|
|
358
|
+
message: "General configuration could not be read.",
|
|
359
|
+
},
|
|
360
|
+
validate: (value) => {
|
|
361
|
+
const validation = resolveGeneralConfig({
|
|
362
|
+
env: environment,
|
|
363
|
+
user: value,
|
|
364
|
+
userSourcePath: configPath,
|
|
365
|
+
});
|
|
366
|
+
const error = validation.diagnostics.find((diagnostic) => diagnostic.severity === "error");
|
|
367
|
+
return error
|
|
368
|
+
? { code: error.code === "missing_environment" ? "missing_environment" : "invalid_source", message: error.message }
|
|
369
|
+
: undefined;
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function generalConfigRuntimeEnvironment(env) {
|
|
374
|
+
const names = new Set(["FORGERELAY_TRUST_PROXY"]);
|
|
375
|
+
for (const field of Object.values(generalConfigDefinition.fields)) {
|
|
376
|
+
const configured = field.runtimeOverride?.env;
|
|
377
|
+
if (!configured)
|
|
378
|
+
continue;
|
|
379
|
+
for (const name of Array.isArray(configured) ? configured : [configured])
|
|
380
|
+
names.add(name);
|
|
381
|
+
}
|
|
382
|
+
return Object.fromEntries([...names].flatMap((name) => env[name] === undefined ? [] : [[name, env[name]]]));
|
|
328
383
|
}
|
|
329
|
-
function
|
|
330
|
-
|
|
384
|
+
function restartRequiredGeneralValues(configured, env) {
|
|
385
|
+
const host = configured.host ?? "127.0.0.1";
|
|
386
|
+
const port = configured.port ?? 7676;
|
|
387
|
+
const publicDeployment = resolvePublicDeployment(configured.publicBaseUrl, host, port);
|
|
388
|
+
const proxyTrust = resolveProxyTrust(env, configured, host, publicDeployment.canonicalBaseUrl);
|
|
389
|
+
const derivedAllowedHosts = [
|
|
390
|
+
"localhost",
|
|
391
|
+
"127.0.0.1",
|
|
392
|
+
"::1",
|
|
393
|
+
host,
|
|
394
|
+
...publicDeployment.baseUrls.map((baseUrl) => new URL(baseUrl).hostname),
|
|
395
|
+
...(configured.allowedHosts ?? []),
|
|
396
|
+
];
|
|
397
|
+
return {
|
|
398
|
+
host,
|
|
399
|
+
port,
|
|
400
|
+
allowedRoots: parseAllowedRoots(configured.allowedRoots),
|
|
401
|
+
publicBaseUrl: publicDeployment.baseUrls,
|
|
402
|
+
allowedHosts: parseAllowedHosts(configured.allowedHosts, derivedAllowedHosts),
|
|
403
|
+
trustedProxies: proxyTrust === false ? [] : proxyTrust,
|
|
404
|
+
stateDir: resolve(expandHomePath(configured.stateDir ?? defaultStateDir())),
|
|
405
|
+
worktreeRoot: resolve(expandHomePath(configured.worktreeRoot ?? defaultWorktreeRoot())),
|
|
406
|
+
agentDir: resolve(expandHomePath(configured.agentDir ?? defaultAgentDir())),
|
|
407
|
+
commandShell: configured.commandShell,
|
|
408
|
+
};
|
|
331
409
|
}
|
|
332
410
|
function parsePublicBaseUrl(value) {
|
|
333
411
|
const parsed = new URL(value);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { externalMcpConfigDefinition } from "./external-mcp.js";
|
|
2
|
+
import { generalConfigDefinition } from "./general-config.js";
|
|
3
|
+
import { languageServersConfigDefinition } from "./language-servers.js";
|
|
4
|
+
import { hooksConfigDefinition } from "../../../mcp/hooks/config.js";
|
|
5
|
+
import { subagentProfilesConfigDefinition } from "../../../subagents/profiles.js";
|
|
6
|
+
export const CONFIG_DEFINITION_CATALOG = [
|
|
7
|
+
generalConfigDefinition,
|
|
8
|
+
externalMcpConfigDefinition,
|
|
9
|
+
languageServersConfigDefinition,
|
|
10
|
+
hooksConfigDefinition,
|
|
11
|
+
subagentProfilesConfigDefinition,
|
|
12
|
+
];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { CONFIG_FILE_SCOPES, } from "./types.js";
|
|
3
|
+
const DOMAIN_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
4
|
+
const FILE_SCOPE_SET = new Set(CONFIG_FILE_SCOPES);
|
|
5
|
+
export function defineConfigDomain(definition) {
|
|
6
|
+
if (!DOMAIN_PATTERN.test(definition.domain)) {
|
|
7
|
+
throw new Error(`Invalid config domain name: ${definition.domain}`);
|
|
8
|
+
}
|
|
9
|
+
if (!definition.title.trim()) {
|
|
10
|
+
throw new Error(`Config domain ${definition.domain} requires a title.`);
|
|
11
|
+
}
|
|
12
|
+
if (!definition.description.trim()) {
|
|
13
|
+
throw new Error(`Config domain ${definition.domain} requires a description.`);
|
|
14
|
+
}
|
|
15
|
+
for (const [name, field] of Object.entries(definition.fields)) {
|
|
16
|
+
validateFieldDefinition(definition.domain, name, field);
|
|
17
|
+
}
|
|
18
|
+
validateFileShape(definition);
|
|
19
|
+
return definition;
|
|
20
|
+
}
|
|
21
|
+
export function configSourceSchema(definition, scope) {
|
|
22
|
+
if (!FILE_SCOPE_SET.has(scope))
|
|
23
|
+
throw new Error(`Scope ${scope} is not a file-backed config scope.`);
|
|
24
|
+
const shape = {
|
|
25
|
+
$schema: z.string().optional().describe("Editor-only JSON Schema URL. Ignored by ForgeRelay resolution."),
|
|
26
|
+
};
|
|
27
|
+
for (const [name, field] of Object.entries(definition.fields)) {
|
|
28
|
+
if (!field.legalScopes.includes(scope))
|
|
29
|
+
continue;
|
|
30
|
+
const schema = field.required ? field.schema : field.schema.optional();
|
|
31
|
+
shape[name] = schema.describe(field.description);
|
|
32
|
+
}
|
|
33
|
+
return z.object(shape).strict();
|
|
34
|
+
}
|
|
35
|
+
export function normalizeConfigSourceShape(definition, scope, value, entryKey) {
|
|
36
|
+
const fileShape = definition.fileShape;
|
|
37
|
+
if (!fileShape || !FILE_SCOPE_SET.has(scope))
|
|
38
|
+
return value;
|
|
39
|
+
if (fileShape.kind === "keyed-root") {
|
|
40
|
+
if (!isRecord(value))
|
|
41
|
+
return value;
|
|
42
|
+
const { $schema, ...entries } = value;
|
|
43
|
+
return {
|
|
44
|
+
...($schema === undefined ? {} : { $schema }),
|
|
45
|
+
[fileShape.field]: entries,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (fileShape.kind === "keyed-entry") {
|
|
49
|
+
const key = entryKey?.trim();
|
|
50
|
+
if (!key)
|
|
51
|
+
throw new Error(`Config domain ${definition.domain} keyed-entry source requires an entry key.`);
|
|
52
|
+
const parsed = fileShape.fileSchema.parse(value);
|
|
53
|
+
if (!isRecord(parsed)) {
|
|
54
|
+
throw new Error(`Config domain ${definition.domain} keyed-entry source must be an object.`);
|
|
55
|
+
}
|
|
56
|
+
const { $schema, ...entry } = parsed;
|
|
57
|
+
const normalized = fileShape.normalizeEntry ? fileShape.normalizeEntry(entry) : entry;
|
|
58
|
+
return {
|
|
59
|
+
...($schema === undefined ? {} : { $schema }),
|
|
60
|
+
[fileShape.field]: { [key]: normalized },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
export function parseConfigSource(definition, scope, value, entryKey) {
|
|
66
|
+
return configSourceSchema(definition, scope).parse(normalizeConfigSourceShape(definition, scope, value, entryKey));
|
|
67
|
+
}
|
|
68
|
+
export function resolveExecutionEffect(field, value, logicalPath) {
|
|
69
|
+
return typeof field.executionEffect === "function"
|
|
70
|
+
? field.executionEffect(value, logicalPath)
|
|
71
|
+
: field.executionEffect;
|
|
72
|
+
}
|
|
73
|
+
function validateFileShape(definition) {
|
|
74
|
+
const fileShape = definition.fileShape;
|
|
75
|
+
if (!fileShape || fileShape.kind === "object")
|
|
76
|
+
return;
|
|
77
|
+
const rootField = definition.fields[fileShape.field];
|
|
78
|
+
if (!rootField) {
|
|
79
|
+
throw new Error(`Config domain ${definition.domain} keyed file field ${fileShape.field} is not defined.`);
|
|
80
|
+
}
|
|
81
|
+
if (rootField.merge !== "keyed") {
|
|
82
|
+
throw new Error(`Config domain ${definition.domain} keyed file field ${fileShape.field} must use keyed merge.`);
|
|
83
|
+
}
|
|
84
|
+
const otherFileFields = Object.entries(definition.fields).filter(([name, field]) => name !== fileShape.field &&
|
|
85
|
+
field.legalScopes.some((scope) => FILE_SCOPE_SET.has(scope)));
|
|
86
|
+
if (otherFileFields.length > 0) {
|
|
87
|
+
throw new Error(`Config domain ${definition.domain} keyed file shape cannot expose additional file-backed fields.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function isRecord(value) {
|
|
91
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
92
|
+
}
|
|
93
|
+
function validateFieldDefinition(domain, name, field) {
|
|
94
|
+
if (name === "$schema") {
|
|
95
|
+
throw new Error(`Config domain ${domain} cannot define reserved field $schema.`);
|
|
96
|
+
}
|
|
97
|
+
if (!name.trim())
|
|
98
|
+
throw new Error(`Config domain ${domain} contains an empty field name.`);
|
|
99
|
+
if (!field.description.trim())
|
|
100
|
+
throw new Error(`Config field ${domain}.${name} requires a description.`);
|
|
101
|
+
if (field.legalScopes.length === 0)
|
|
102
|
+
throw new Error(`Config field ${domain}.${name} requires at least one legal scope.`);
|
|
103
|
+
if (new Set(field.legalScopes).size !== field.legalScopes.length) {
|
|
104
|
+
throw new Error(`Config field ${domain}.${name} contains duplicate legal scopes.`);
|
|
105
|
+
}
|
|
106
|
+
if (field.builtIn.kind !== "none" && !field.legalScopes.includes("built-in")) {
|
|
107
|
+
throw new Error(`Config field ${domain}.${name} declares a built-in default without built-in scope.`);
|
|
108
|
+
}
|
|
109
|
+
if (field.runtimeOverride && !field.legalScopes.includes("runtime")) {
|
|
110
|
+
throw new Error(`Config field ${domain}.${name} declares a runtime override without runtime scope.`);
|
|
111
|
+
}
|
|
112
|
+
if (field.interpolateValue && field.interpolation !== "env") {
|
|
113
|
+
throw new Error(`Config field ${domain}.${name} declares a custom interpolator without env interpolation.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { externalMcpStandaloneServersSchema, } from "../external-mcp-config.js";
|
|
2
|
+
import { defineConfigDomain } from "./definition.js";
|
|
3
|
+
const MCP_SCOPES = ["project-local", "project", "user"];
|
|
4
|
+
const ENV_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
5
|
+
export const externalMcpConfigDefinition = defineConfigDomain({
|
|
6
|
+
domain: "mcp",
|
|
7
|
+
title: "ForgeRelay External MCP configuration",
|
|
8
|
+
description: "Canonical External MCP server configuration. OAuth credentials remain in the machine-private mcp-auth.json store.",
|
|
9
|
+
fields: {
|
|
10
|
+
servers: {
|
|
11
|
+
schema: externalMcpStandaloneServersSchema,
|
|
12
|
+
description: "External MCP servers keyed by stable server name.",
|
|
13
|
+
required: true,
|
|
14
|
+
legalScopes: MCP_SCOPES,
|
|
15
|
+
merge: "keyed",
|
|
16
|
+
reload: "hot",
|
|
17
|
+
sensitivity: "sensitive",
|
|
18
|
+
interpolation: "env",
|
|
19
|
+
interpolateValue: interpolateExternalMcpSecrets,
|
|
20
|
+
builtIn: { kind: "none" },
|
|
21
|
+
executionEffect: (value) => isRecord(value) && value.transport === "stdio" ? "process" : "none",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
function interpolateExternalMcpSecrets(value, environment) {
|
|
26
|
+
if (!isRecord(value))
|
|
27
|
+
return value;
|
|
28
|
+
return Object.fromEntries(Object.entries(value).map(([name, entry]) => {
|
|
29
|
+
if (!isRecord(entry))
|
|
30
|
+
return [name, entry];
|
|
31
|
+
const next = { ...entry };
|
|
32
|
+
if (isRecord(entry.env))
|
|
33
|
+
next.env = interpolateStringRecord(entry.env, environment);
|
|
34
|
+
if (isRecord(entry.headers))
|
|
35
|
+
next.headers = interpolateStringRecord(entry.headers, environment);
|
|
36
|
+
return [name, next];
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function interpolateStringRecord(value, environment) {
|
|
40
|
+
return Object.fromEntries(Object.entries(value).map(([name, entry]) => [
|
|
41
|
+
name,
|
|
42
|
+
typeof entry === "string" ? interpolateString(entry, environment) : entry,
|
|
43
|
+
]));
|
|
44
|
+
}
|
|
45
|
+
function interpolateString(value, environment) {
|
|
46
|
+
return value.replace(ENV_REFERENCE, (_match, name) => {
|
|
47
|
+
const resolved = environment[name];
|
|
48
|
+
if (resolved !== undefined)
|
|
49
|
+
return resolved;
|
|
50
|
+
const error = new Error(`Required environment variable ${name} is not available.`);
|
|
51
|
+
error.code = "missing_environment";
|
|
52
|
+
error.variable = name;
|
|
53
|
+
throw error;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
|
+
}
|