@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.
Files changed (170) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +14 -0
  3. package/dist/activity/runtime/mcp-query-tools.js +15 -5
  4. package/dist/cli/config/inspect.js +331 -0
  5. package/dist/cli/config/migrate.js +351 -0
  6. package/dist/cli/init/setup-config.js +41 -0
  7. package/dist/cli/init.js +128 -130
  8. package/dist/cli/maintenance.js +15 -5
  9. package/dist/cli/mcp/external-mcp.js +14 -10
  10. package/dist/cli/mcp/status.js +11 -6
  11. package/dist/cli.js +40 -7
  12. package/dist/lsp/language-server-config.js +66 -144
  13. package/dist/lsp/runtime/manager.js +35 -4
  14. package/dist/mcp/hooks/config.js +141 -0
  15. package/dist/mcp/hooks/external-mcp-transform.js +22 -13
  16. package/dist/mcp/hooks/hook-cli.js +62 -52
  17. package/dist/mcp/hooks/hooks.js +111 -85
  18. package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +8 -5
  19. package/dist/mcp/operations/external-mcp/external-mcp.js +9 -2
  20. package/dist/mcp/server/core/capability-support.js +7 -0
  21. package/dist/runtime/config/config.js +142 -64
  22. package/dist/runtime/config/definition/catalog.js +12 -0
  23. package/dist/runtime/config/definition/definition.js +115 -0
  24. package/dist/runtime/config/definition/external-mcp.js +58 -0
  25. package/dist/runtime/config/definition/general-config.js +258 -0
  26. package/dist/runtime/config/definition/language-servers.js +116 -0
  27. package/dist/runtime/config/definition/schema.js +113 -0
  28. package/dist/runtime/config/definition/types.js +2 -0
  29. package/dist/runtime/config/external-mcp-auth-store.js +105 -29
  30. package/dist/runtime/config/external-mcp-config.js +39 -0
  31. package/dist/runtime/config/external-mcp-registry.js +284 -106
  32. package/dist/runtime/config/inspection/live.js +77 -0
  33. package/dist/runtime/config/resolution/general.js +93 -0
  34. package/dist/runtime/config/resolution/hooks.js +315 -0
  35. package/dist/runtime/config/resolution/language-servers.js +237 -0
  36. package/dist/runtime/config/resolution/project-sources.js +159 -0
  37. package/dist/runtime/config/resolution/resolver.js +485 -0
  38. package/dist/runtime/config/resolution/types.js +1 -0
  39. package/dist/runtime/config/runtime/config-runtime.js +74 -0
  40. package/dist/runtime/config/runtime/source-refresh.js +263 -0
  41. package/dist/runtime/config/user-config.js +38 -31
  42. package/dist/runtime/security/project-execution-trust.js +34 -0
  43. package/dist/runtime/testing/server-fixture.js +9 -8
  44. package/dist/server.js +10 -6
  45. package/dist/subagents/profiles.js +336 -53
  46. package/dist/subagents/sessions/execution.js +6 -35
  47. package/dist/subagents/sessions/manager.js +14 -2
  48. package/dist/ui/.vite/manifest.json +275 -275
  49. package/dist/ui/activity-panel-app.html +1 -1
  50. package/dist/ui/assets/{activity-panel-app-Bw7ZFX2g.js → activity-panel-app-BeDiwd7D.js} +3 -3
  51. package/dist/ui/assets/{angular-html-DeUNP12X.js → angular-html-DuoCE7nn.js} +1 -1
  52. package/dist/ui/assets/{angular-ts-B3v8FKi_.js → angular-ts-D5tGk0hL.js} +1 -1
  53. package/dist/ui/assets/{apl-Bs3iQm4P.js → apl-lciOTPbw.js} +1 -1
  54. package/dist/ui/assets/{astro-D6GHecBY.js → astro-vJbqfNnJ.js} +1 -1
  55. package/dist/ui/assets/{blade-BMNbVOHv.js → blade-C7655Q4K.js} +1 -1
  56. package/dist/ui/assets/{c-BquuA6bv.js → c-DdHp1Jrw.js} +1 -1
  57. package/dist/ui/assets/{cobol-BuZ6kfps.js → cobol-DgvzQxZP.js} +1 -1
  58. package/dist/ui/assets/{coffee-DgC5uBzL.js → coffee-BIjVUOUd.js} +1 -1
  59. package/dist/ui/assets/{cpp-BdvWqCaB.js → cpp-CU8tGzvI.js} +1 -1
  60. package/dist/ui/assets/{crystal-0Q5qBvNI.js → crystal-CUQAwsg9.js} +1 -1
  61. package/dist/ui/assets/{css-Be_T8idh.js → css-DtMTqD3F.js} +1 -1
  62. package/dist/ui/assets/{edge-DaGLdgh_.js → edge-DoCX9RnL.js} +1 -1
  63. package/dist/ui/assets/{elixir-CMgiFIzB.js → elixir-DJxV9_U6.js} +1 -1
  64. package/dist/ui/assets/{elm-B-ufMlMC.js → elm-DIgWqK4r.js} +1 -1
  65. package/dist/ui/assets/{erb-CaGiOZM-.js → erb-BHxP0c_e.js} +1 -1
  66. package/dist/ui/assets/{git-rebase-CpwUWCzK.js → git-rebase-HbI_Vo8d.js} +1 -1
  67. package/dist/ui/assets/{glimmer-js-BRxccvnE.js → glimmer-js-CthY4Ty_.js} +1 -1
  68. package/dist/ui/assets/{glimmer-ts-CgQbPzlT.js → glimmer-ts-B2-_A1Xz.js} +1 -1
  69. package/dist/ui/assets/{glsl-D7JmHFr0.js → glsl-D9V9gq98.js} +1 -1
  70. package/dist/ui/assets/{graphql-C8n0eUBK.js → graphql-CddeAaS3.js} +1 -1
  71. package/dist/ui/assets/{hack-lVN7KVQv.js → hack-D_loGkOT.js} +1 -1
  72. package/dist/ui/assets/{haml-h18lG0pS.js → haml-BoWDczd0.js} +1 -1
  73. package/dist/ui/assets/{handlebars-lRFjRPfj.js → handlebars-ZoSOvw9y.js} +1 -1
  74. package/dist/ui/assets/{heavy-payload-Cqx9MOkH.js → heavy-payload-DO34LwoZ.js} +3 -3
  75. package/dist/ui/assets/{html-DtILqTE1.js → html-CE-obXVo.js} +1 -1
  76. package/dist/ui/assets/{html-derivative-CSTSmPup.js → html-derivative-Cd-smAbW.js} +1 -1
  77. package/dist/ui/assets/{http-DNI8fsbY.js → http-CZrkbrSl.js} +1 -1
  78. package/dist/ui/assets/{hurl-DgRi60JG.js → hurl-CxTuxBt1.js} +1 -1
  79. package/dist/ui/assets/{java-CW9pZ5bh.js → java-BshLjCat.js} +1 -1
  80. package/dist/ui/assets/{javascript-Cvg2UrWL.js → javascript-D2pxQTiX.js} +1 -1
  81. package/dist/ui/assets/{jinja-CZw_S1FB.js → jinja-kbfPP7-F.js} +1 -1
  82. package/dist/ui/assets/{jison-C56u_FZv.js → jison-C2mt9l1M.js} +1 -1
  83. package/dist/ui/assets/{json-DW2UTv1q.js → json-DU_rg_AU.js} +1 -1
  84. package/dist/ui/assets/{jsx-Zj5gobdD.js → jsx-D_8aQMlx.js} +1 -1
  85. package/dist/ui/assets/{julia-Cvcrre-L.js → julia-BmefTaVV.js} +1 -1
  86. package/dist/ui/assets/{just-8CZ2u6Tj.js → just-DXV8vk1S.js} +1 -1
  87. package/dist/ui/assets/{latex-D289AXWG.js → latex-Cjm2MvGW.js} +1 -1
  88. package/dist/ui/assets/{liquid-CFEX3SkZ.js → liquid-DyQDlo0u.js} +1 -1
  89. package/dist/ui/assets/{lua-BKpJ0ybO.js → lua-B2A1vzzy.js} +1 -1
  90. package/dist/ui/assets/{marko-D9maZ0a6.js → marko-B9vOkXOU.js} +1 -1
  91. package/dist/ui/assets/{mdc-rM9l0qWR.js → mdc-D42m6spU.js} +1 -1
  92. package/dist/ui/assets/{nginx-C9w6p2C9.js → nginx-TpfVxSUY.js} +1 -1
  93. package/dist/ui/assets/{nim-BRePHk0J.js → nim-CJr5T1FK.js} +1 -1
  94. package/dist/ui/assets/{perl-CskTmiPQ.js → perl-cJZcsdm9.js} +1 -1
  95. package/dist/ui/assets/{php-DCU_FvNI.js → php-CSZL5KRT.js} +1 -1
  96. package/dist/ui/assets/{pug-XLaVxTvv.js → pug-B1eMKzuA.js} +1 -1
  97. package/dist/ui/assets/{qml-CaONKSl-.js → qml-Dl2_7zc9.js} +1 -1
  98. package/dist/ui/assets/{r-BCVR0ZE3.js → r-F0aaO9gG.js} +1 -1
  99. package/dist/ui/assets/{razor-DDnGXxnU.js → razor-BPnkSMCw.js} +1 -1
  100. package/dist/ui/assets/{regexp-DWGTYYVx.js → regexp-BnGHMFbv.js} +1 -1
  101. package/dist/ui/assets/{rst-Dz3GWvQm.js → rst-B3CO7WYO.js} +1 -1
  102. package/dist/ui/assets/{ruby-DC3jGtMY.js → ruby-B6VMzT1T.js} +1 -1
  103. package/dist/ui/assets/{sas-7mNpvsjZ.js → sas-Df2lHVuU.js} +1 -1
  104. package/dist/ui/assets/{scss-uBsoCJVy.js → scss-4XxI6sgu.js} +1 -1
  105. package/dist/ui/assets/{shellscript-BCN9N_Jf.js → shellscript-DYASCN4r.js} +1 -1
  106. package/dist/ui/assets/{shellsession-Hsw46VXV.js → shellsession-Br6AGJO_.js} +1 -1
  107. package/dist/ui/assets/{soy-D6KV69HU.js → soy-p3ZNq1We.js} +1 -1
  108. package/dist/ui/assets/{sql-CVzVQkJh.js → sql-CAdv2OXL.js} +1 -1
  109. package/dist/ui/assets/{stata-BwUjXNhI.js → stata-Dx7yUbG_.js} +1 -1
  110. package/dist/ui/assets/{surrealql-CgloWqRa.js → surrealql-6YsE8HQa.js} +1 -1
  111. package/dist/ui/assets/{svelte-DcC3n6IC.js → svelte-YgzedwpD.js} +1 -1
  112. package/dist/ui/assets/{templ-DOFfnWTJ.js → templ-Bahqx3uD.js} +1 -1
  113. package/dist/ui/assets/{tex-ln89VPaX.js → tex-o6fTQjkH.js} +1 -1
  114. package/dist/ui/assets/{ts-tags-CWMzCstH.js → ts-tags-BO-Co0Ju.js} +1 -1
  115. package/dist/ui/assets/{tsx-DgzfiGeK.js → tsx-DtQe5AoH.js} +1 -1
  116. package/dist/ui/assets/{twig-Bi2itZL9.js → twig-BcUs6Kqc.js} +1 -1
  117. package/dist/ui/assets/{typescript-CQ24f4zW.js → typescript-BPtM8h4u.js} +1 -1
  118. package/dist/ui/assets/{vue-BS8jE5u6.js → vue-5KkGUzJJ.js} +1 -1
  119. package/dist/ui/assets/{vue-html-o38xqhyB.js → vue-html-Df_Xfsz9.js} +1 -1
  120. package/dist/ui/assets/{vue-vine-D2NvKv50.js → vue-vine-hUIe_u_Y.js} +1 -1
  121. package/dist/ui/assets/{xml-_44rx9_2.js → xml-C3Jy7as5.js} +1 -1
  122. package/dist/ui/assets/{xsl-B5QWQPoi.js → xsl-BnF9OuFo.js} +1 -1
  123. package/dist/ui/assets/{yaml-CKL2pEWe.js → yaml-DyrFgCou.js} +1 -1
  124. package/dist/workspaces/conversation-test-support.js +3 -11
  125. package/dist/workspaces/sessions.js +2 -0
  126. package/dist/workspaces/state/project-context.js +221 -0
  127. package/dist/workspaces.js +3 -1
  128. package/docs/chatgpt-coding-workflow.md +8 -3
  129. package/docs/configuration.md +99 -41
  130. package/docs/gotchas.md +24 -5
  131. package/docs/security.md +14 -3
  132. package/docs/setup.md +12 -7
  133. package/package.json +12 -3
  134. package/schemas/v1/config.project-local.schema.json +14 -0
  135. package/schemas/v1/config.project.schema.json +14 -0
  136. package/schemas/v1/config.user.schema.json +538 -0
  137. package/schemas/v1/hooks.project-local.schema.json +118 -0
  138. package/schemas/v1/hooks.project.schema.json +118 -0
  139. package/schemas/v1/hooks.user.schema.json +118 -0
  140. package/schemas/v1/language-servers.project-local.schema.json +230 -0
  141. package/schemas/v1/language-servers.project.schema.json +230 -0
  142. package/schemas/v1/language-servers.user.schema.json +230 -0
  143. package/schemas/v1/mcp.project-local.schema.json +143 -0
  144. package/schemas/v1/mcp.project.schema.json +143 -0
  145. package/schemas/v1/mcp.user.schema.json +143 -0
  146. package/scripts/ci/architecture.mjs +54 -4
  147. package/scripts/ci/cmd-acceptance.mjs +9 -9
  148. package/scripts/ci/config-v2-product-acceptance.mjs +639 -0
  149. package/scripts/ci/gates/acceptance-artifact.mjs +66 -0
  150. package/scripts/ci/gates/core-contract.mjs +27 -0
  151. package/scripts/ci/gates/platform-tests.mjs +38 -0
  152. package/scripts/ci/gates/prepare-windows-product.mjs +57 -0
  153. package/scripts/ci/gates/test-shard.mjs +71 -0
  154. package/scripts/ci/powershell51-acceptance.mjs +9 -9
  155. package/scripts/ci/pwsh-acceptance.mjs +9 -9
  156. package/scripts/ci/verify.mjs +2 -0
  157. package/scripts/ci/windows-product-acceptance.mjs +9 -10
  158. package/scripts/config/schema-text.mjs +4 -0
  159. package/scripts/config/schema-text.test.mjs +18 -0
  160. package/scripts/config/schema.mjs +74 -0
  161. package/scripts/lsp/interop.mjs +4 -1
  162. package/scripts/release/pack.mjs +64 -4
  163. package/scripts/release/release-gate.test.mjs +93 -27
  164. package/scripts/release-parity.mjs +19 -2
  165. package/templates/instructions/cmd.md +12 -0
  166. package/templates/instructions/fish.md +11 -0
  167. package/templates/instructions/powershell.md +14 -0
  168. package/templates/instructions/pwsh.md +14 -0
  169. package/templates/instructions/zsh.md +11 -0
  170. 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 { mergeHookConfigs, parseHookConfig } from "../../mcp/hooks/hooks.js";
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 envTrustedProxies = productEnv(env, "TRUSTED_PROXIES");
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(env, fileConfig, host, port) {
239
+ function resolvePublicDeployment(configuredValue, host, port) {
257
240
  const localBaseUrls = [parsePublicBaseUrl(localPublicBaseUrl(host, port))];
258
- const envBaseUrl = productEnv(env, "PUBLIC_BASE_URL");
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 = env.HOST ?? files.config.host ?? "127.0.0.1";
271
- const port = parsePort(env.PORT ?? files.config.port);
272
- const publicDeployment = resolvePublicDeployment(env, files.config, host, port);
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, files.config, host, publicBaseUrl);
275
- const commandShellRuntime = resolveConfiguredCommandShellRuntime(files.config.commandShell, process.platform, env);
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
- ...(files.config.allowedHosts ?? []),
282
+ ...(config.allowedHosts ?? []),
283
283
  ];
284
- return {
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(productEnv(env, "ALLOWED_ROOTS") ?? files.config.allowedRoots),
291
- allowedHosts: parseAllowedHosts(productEnv(env, "ALLOWED_HOSTS"), derivedAllowedHosts),
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(productEnv(env, "WORKFLOW_INSTRUCTIONS"), files.config.workflowInstructions),
297
- appendInstructions: parseAppendInstructions(productEnv(env, "APPEND_INSTRUCTIONS"), files.config.appendInstructions),
297
+ workflowInstructions: parseWorkflowInstructions(undefined, config.workflowInstructions),
298
+ appendInstructions: parseAppendInstructions(undefined, config.appendInstructions),
298
299
  widgets: parseWidgetMode(productEnv(env, "WIDGETS")),
299
- activityPanelExpanded: productEnv(env, "ACTIVITY_PANEL_EXPANDED") === undefined
300
- ? files.config.activityPanelExpanded === true
301
- : parseBoolean(productEnv(env, "ACTIVITY_PANEL_EXPANDED")),
302
- stateDir: resolve(expandHomePath(productEnv(env, "STATE_DIR") ?? files.config.stateDir ?? defaultStateDir())),
303
- worktreeRoot: resolve(expandHomePath(productEnv(env, "WORKTREE_ROOT") ?? files.config.worktreeRoot ?? defaultWorktreeRoot())),
304
- artifactsEnabled: productEnv(env, "ARTIFACTS") === undefined
305
- ? files.config.artifactsEnabled === true
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
- configAgentsDir: forgerelayAgentsDir(env),
314
- subagents: productEnv(env, "SUBAGENTS") === undefined
315
- ? files.config.subagents === true
316
- : parseBoolean(productEnv(env, "SUBAGENTS")),
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: files.config.shellInstructions !== false,
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 numberConfigValue(value) {
330
- return value === undefined ? undefined : String(value);
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
+ }