@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
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
import { configSourceSchema, normalizeConfigSourceShape, resolveExecutionEffect, } from "../definition/definition.js";
|
|
4
|
+
import { configSchemaId } from "../definition/schema.js";
|
|
5
|
+
const SCOPE_RANK = {
|
|
6
|
+
"built-in": 0,
|
|
7
|
+
user: 1,
|
|
8
|
+
project: 2,
|
|
9
|
+
"project-local": 3,
|
|
10
|
+
runtime: 4,
|
|
11
|
+
};
|
|
12
|
+
class MissingEnvironmentError extends Error {
|
|
13
|
+
name;
|
|
14
|
+
constructor(name) {
|
|
15
|
+
super(`Required environment variable ${name} is not available.`);
|
|
16
|
+
this.name = name;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function resolveConfigDomain(input) {
|
|
20
|
+
const environment = input.environment ?? process.env;
|
|
21
|
+
const diagnostics = [];
|
|
22
|
+
const prepared = [];
|
|
23
|
+
for (const source of input.sources) {
|
|
24
|
+
const reference = sourceReference(source);
|
|
25
|
+
const schemaDiagnostic = sourceSchemaDiagnostic(input.definition, source, reference);
|
|
26
|
+
if (schemaDiagnostic)
|
|
27
|
+
diagnostics.push(schemaDiagnostic);
|
|
28
|
+
if (source.deprecation)
|
|
29
|
+
diagnostics.push(sourceDeprecationDiagnostic(reference, source.deprecation));
|
|
30
|
+
if (source.error) {
|
|
31
|
+
diagnostics.push({
|
|
32
|
+
severity: "error",
|
|
33
|
+
code: source.error.code,
|
|
34
|
+
source: reference,
|
|
35
|
+
message: source.error.message,
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const configured = sourceObject(source.normalized
|
|
41
|
+
? source.value
|
|
42
|
+
: normalizeConfigSourceShape(input.definition, source.scope, source.value, source.entryKey));
|
|
43
|
+
const interpolated = interpolateSource(input.definition, source.scope, configured, environment);
|
|
44
|
+
const parsed = parseSource(input.definition, source.scope, interpolated);
|
|
45
|
+
prepared.push({
|
|
46
|
+
input: source,
|
|
47
|
+
reference,
|
|
48
|
+
values: stripSchemaMetadata(parsed),
|
|
49
|
+
configuredValues: stripSchemaMetadata(configured),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
diagnostics.push(diagnosticForSourceError(reference, error));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const builtInReference = {
|
|
57
|
+
id: `built-in:${input.definition.domain}`,
|
|
58
|
+
scope: "built-in",
|
|
59
|
+
kind: "built-in",
|
|
60
|
+
priority: 0,
|
|
61
|
+
};
|
|
62
|
+
const shadowBarriers = sourceShadowBarriers(input.sources);
|
|
63
|
+
const entries = {};
|
|
64
|
+
const values = {};
|
|
65
|
+
for (const [name, field] of Object.entries(input.definition.fields)) {
|
|
66
|
+
if (field.merge === "keyed") {
|
|
67
|
+
resolveKeyedField({
|
|
68
|
+
definition: input.definition,
|
|
69
|
+
name,
|
|
70
|
+
field,
|
|
71
|
+
prepared,
|
|
72
|
+
builtInReference,
|
|
73
|
+
shadowBarriers,
|
|
74
|
+
entries,
|
|
75
|
+
values,
|
|
76
|
+
diagnostics,
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
resolveReplaceField({
|
|
81
|
+
definition: input.definition,
|
|
82
|
+
name,
|
|
83
|
+
field,
|
|
84
|
+
prepared,
|
|
85
|
+
builtInReference,
|
|
86
|
+
shadowBarriers,
|
|
87
|
+
entries,
|
|
88
|
+
values,
|
|
89
|
+
diagnostics,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const sources = [
|
|
93
|
+
...input.sources.map(sourceReference),
|
|
94
|
+
builtInReference,
|
|
95
|
+
].sort(compareSourceReferences);
|
|
96
|
+
return {
|
|
97
|
+
domain: input.definition.domain,
|
|
98
|
+
values,
|
|
99
|
+
entries,
|
|
100
|
+
sources,
|
|
101
|
+
diagnostics,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function assertConfigResolutionValid(resolution) {
|
|
105
|
+
const errors = resolution.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
106
|
+
if (errors.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
const details = errors.map((diagnostic) => {
|
|
109
|
+
const location = diagnostic.source.location ?? diagnostic.source.id;
|
|
110
|
+
return `${location}: ${diagnostic.message}`;
|
|
111
|
+
});
|
|
112
|
+
throw new Error(`Invalid ForgeRelay ${resolution.domain} configuration: ${details.join("; ")}`);
|
|
113
|
+
}
|
|
114
|
+
function resolveReplaceField(input) {
|
|
115
|
+
const logicalPath = `${input.definition.domain}.${input.name}`;
|
|
116
|
+
const candidates = input.prepared
|
|
117
|
+
.filter((source) => Object.prototype.hasOwnProperty.call(source.values, input.name))
|
|
118
|
+
.map((source) => ({
|
|
119
|
+
source: source.reference,
|
|
120
|
+
value: source.values[input.name],
|
|
121
|
+
hasValue: true,
|
|
122
|
+
configuredValue: source.configuredValues[input.name],
|
|
123
|
+
}));
|
|
124
|
+
appendBuiltInCandidate(candidates, input.field, input.builtInReference);
|
|
125
|
+
candidates.sort(compareCandidates);
|
|
126
|
+
const winner = candidates.find((candidate) => !candidateIsSourceShadowed(candidate, input.shadowBarriers, input.name));
|
|
127
|
+
if (!winner)
|
|
128
|
+
return;
|
|
129
|
+
input.entries[input.name] = {
|
|
130
|
+
logicalPath,
|
|
131
|
+
effective: provenanceFor(input.field, winner, logicalPath),
|
|
132
|
+
shadowed: candidates
|
|
133
|
+
.filter((candidate) => candidate !== winner)
|
|
134
|
+
.map((candidate) => ({
|
|
135
|
+
...provenanceFor(input.field, candidate, logicalPath),
|
|
136
|
+
reason: candidateIsSourceShadowed(candidate, input.shadowBarriers, input.name)
|
|
137
|
+
? "source-shadowed"
|
|
138
|
+
: shadowReason(winner, candidate),
|
|
139
|
+
})),
|
|
140
|
+
};
|
|
141
|
+
if (winner.hasValue)
|
|
142
|
+
input.values[input.name] = winner.value;
|
|
143
|
+
appendFieldDeprecations(input.diagnostics, input.field, candidates, logicalPath);
|
|
144
|
+
}
|
|
145
|
+
function resolveKeyedField(input) {
|
|
146
|
+
const sourceValues = input.prepared
|
|
147
|
+
.filter((source) => Object.prototype.hasOwnProperty.call(source.values, input.name))
|
|
148
|
+
.map((source) => ({
|
|
149
|
+
source,
|
|
150
|
+
value: keyedObject(source.values[input.name], `${input.definition.domain}.${input.name}`),
|
|
151
|
+
configured: keyedObject(source.configuredValues[input.name], `${input.definition.domain}.${input.name}`),
|
|
152
|
+
}));
|
|
153
|
+
let fieldPresent = sourceValues.length > 0;
|
|
154
|
+
let builtInValue;
|
|
155
|
+
if (input.field.builtIn.kind === "literal") {
|
|
156
|
+
builtInValue = keyedObject(input.field.builtIn.value, `${input.definition.domain}.${input.name}`);
|
|
157
|
+
fieldPresent = true;
|
|
158
|
+
}
|
|
159
|
+
const keys = new Set();
|
|
160
|
+
for (const source of sourceValues)
|
|
161
|
+
for (const key of Object.keys(source.value))
|
|
162
|
+
keys.add(key);
|
|
163
|
+
for (const key of Object.keys(builtInValue ?? {}))
|
|
164
|
+
keys.add(key);
|
|
165
|
+
const merged = {};
|
|
166
|
+
for (const key of [...keys].sort()) {
|
|
167
|
+
const logicalPath = `${input.definition.domain}.${input.name}.${key}`;
|
|
168
|
+
const candidates = sourceValues
|
|
169
|
+
.filter(({ value }) => Object.prototype.hasOwnProperty.call(value, key))
|
|
170
|
+
.map(({ source, value, configured }) => ({
|
|
171
|
+
source: source.reference,
|
|
172
|
+
value: value[key],
|
|
173
|
+
hasValue: true,
|
|
174
|
+
configuredValue: configured[key],
|
|
175
|
+
}));
|
|
176
|
+
if (builtInValue && Object.prototype.hasOwnProperty.call(builtInValue, key)) {
|
|
177
|
+
candidates.push({
|
|
178
|
+
source: input.builtInReference,
|
|
179
|
+
value: builtInValue[key],
|
|
180
|
+
hasValue: true,
|
|
181
|
+
configuredValue: builtInValue[key],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
candidates.sort(compareCandidates);
|
|
185
|
+
const entryPath = `${input.name}.${key}`;
|
|
186
|
+
const winner = candidates.find((candidate) => !candidateIsSourceShadowed(candidate, input.shadowBarriers, entryPath));
|
|
187
|
+
if (!winner)
|
|
188
|
+
continue;
|
|
189
|
+
const tombstone = isDisabledTombstone(winner.value);
|
|
190
|
+
input.entries[`${input.name}.${key}`] = {
|
|
191
|
+
logicalPath,
|
|
192
|
+
effective: provenanceFor(input.field, winner, logicalPath),
|
|
193
|
+
shadowed: candidates
|
|
194
|
+
.filter((candidate) => candidate !== winner)
|
|
195
|
+
.map((candidate) => ({
|
|
196
|
+
...provenanceFor(input.field, candidate, logicalPath),
|
|
197
|
+
reason: candidateIsSourceShadowed(candidate, input.shadowBarriers, entryPath)
|
|
198
|
+
? "source-shadowed"
|
|
199
|
+
: shadowReason(winner, candidate),
|
|
200
|
+
})),
|
|
201
|
+
...(tombstone ? { tombstone: true } : {}),
|
|
202
|
+
};
|
|
203
|
+
if (!tombstone)
|
|
204
|
+
merged[key] = winner.value;
|
|
205
|
+
appendFieldDeprecations(input.diagnostics, input.field, candidates, logicalPath);
|
|
206
|
+
}
|
|
207
|
+
if (fieldPresent)
|
|
208
|
+
input.values[input.name] = merged;
|
|
209
|
+
}
|
|
210
|
+
function appendBuiltInCandidate(candidates, field, builtInReference) {
|
|
211
|
+
if (field.builtIn.kind === "literal") {
|
|
212
|
+
candidates.push({
|
|
213
|
+
source: builtInReference,
|
|
214
|
+
value: field.builtIn.value,
|
|
215
|
+
hasValue: true,
|
|
216
|
+
configuredValue: field.builtIn.value,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else if (field.builtIn.kind === "computed") {
|
|
220
|
+
candidates.push({
|
|
221
|
+
source: builtInReference,
|
|
222
|
+
value: undefined,
|
|
223
|
+
hasValue: false,
|
|
224
|
+
configuredValue: `<computed: ${field.builtIn.description}>`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function sourceShadowBarriers(sources) {
|
|
229
|
+
const scope = new Map();
|
|
230
|
+
const entries = new Map();
|
|
231
|
+
for (const source of sources) {
|
|
232
|
+
if (source.shadowsLowerPriority) {
|
|
233
|
+
scope.set(source.scope, Math.max(scope.get(source.scope) ?? Number.NEGATIVE_INFINITY, source.priority));
|
|
234
|
+
}
|
|
235
|
+
for (const entryPath of source.shadowsLowerPriorityKeys ?? []) {
|
|
236
|
+
const key = shadowBarrierKey(source.scope, entryPath);
|
|
237
|
+
entries.set(key, Math.max(entries.get(key) ?? Number.NEGATIVE_INFINITY, source.priority));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { scope, entries };
|
|
241
|
+
}
|
|
242
|
+
function candidateIsSourceShadowed(candidate, barriers, entryPath) {
|
|
243
|
+
const scopeBarrier = barriers.scope.get(candidate.source.scope);
|
|
244
|
+
const entryBarrier = barriers.entries.get(shadowBarrierKey(candidate.source.scope, entryPath));
|
|
245
|
+
const barrier = Math.max(scopeBarrier ?? Number.NEGATIVE_INFINITY, entryBarrier ?? Number.NEGATIVE_INFINITY);
|
|
246
|
+
return candidate.source.priority < barrier;
|
|
247
|
+
}
|
|
248
|
+
function shadowBarrierKey(scope, entryPath) {
|
|
249
|
+
return `${scope}\0${entryPath}`;
|
|
250
|
+
}
|
|
251
|
+
function parseSource(definition, scope, value) {
|
|
252
|
+
if (scope === "user" || scope === "project" || scope === "project-local") {
|
|
253
|
+
return configSourceSchema(definition, scope).parse(value);
|
|
254
|
+
}
|
|
255
|
+
if (scope !== "runtime") {
|
|
256
|
+
throw new Error("Built-in configuration is synthesized from Config Definition metadata.");
|
|
257
|
+
}
|
|
258
|
+
const shape = {};
|
|
259
|
+
for (const [name, field] of Object.entries(definition.fields)) {
|
|
260
|
+
if (!field.legalScopes.includes("runtime"))
|
|
261
|
+
continue;
|
|
262
|
+
shape[name] = field.required ? field.schema : field.schema.optional();
|
|
263
|
+
}
|
|
264
|
+
return z.object(shape).strict().parse(value);
|
|
265
|
+
}
|
|
266
|
+
function interpolateSource(definition, scope, configured, environment) {
|
|
267
|
+
const interpolated = { ...configured };
|
|
268
|
+
for (const [name, field] of Object.entries(definition.fields)) {
|
|
269
|
+
if (!field.legalScopes.includes(scope) || field.interpolation !== "env")
|
|
270
|
+
continue;
|
|
271
|
+
if (!Object.prototype.hasOwnProperty.call(interpolated, name))
|
|
272
|
+
continue;
|
|
273
|
+
interpolated[name] = field.interpolateValue
|
|
274
|
+
? field.interpolateValue(interpolated[name], environment)
|
|
275
|
+
: interpolateValue(interpolated[name], environment);
|
|
276
|
+
}
|
|
277
|
+
return interpolated;
|
|
278
|
+
}
|
|
279
|
+
function interpolateValue(value, environment) {
|
|
280
|
+
if (typeof value === "string") {
|
|
281
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
|
|
282
|
+
const resolved = environment[name];
|
|
283
|
+
if (resolved === undefined)
|
|
284
|
+
throw new MissingEnvironmentError(name);
|
|
285
|
+
return resolved;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (Array.isArray(value))
|
|
289
|
+
return value.map((entry) => interpolateValue(entry, environment));
|
|
290
|
+
if (isRecord(value)) {
|
|
291
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, interpolateValue(entry, environment)]));
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
function provenanceFor(field, candidate, logicalPath) {
|
|
296
|
+
const executionEffect = candidate.hasValue
|
|
297
|
+
? resolveExecutionEffect(field, candidate.value, logicalPath.split("."))
|
|
298
|
+
: typeof field.executionEffect === "string"
|
|
299
|
+
? field.executionEffect
|
|
300
|
+
: "none";
|
|
301
|
+
const provenance = {
|
|
302
|
+
source: candidate.source,
|
|
303
|
+
configuredValue: safeConfiguredValue(field, candidate.configuredValue),
|
|
304
|
+
effectiveValue: candidate.hasValue
|
|
305
|
+
? safeValue(field, candidate.value)
|
|
306
|
+
: candidate.configuredValue,
|
|
307
|
+
reload: field.reload,
|
|
308
|
+
sensitivity: field.sensitivity,
|
|
309
|
+
executionEffect,
|
|
310
|
+
};
|
|
311
|
+
if (executionEffect === "process" && candidate.hasValue) {
|
|
312
|
+
Object.defineProperty(provenance, "executionFingerprint", {
|
|
313
|
+
value: valueFingerprint({ logicalPath, value: candidate.value }),
|
|
314
|
+
enumerable: false,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return provenance;
|
|
318
|
+
}
|
|
319
|
+
function safeConfiguredValue(field, value) {
|
|
320
|
+
if (field.sensitivity !== "sensitive")
|
|
321
|
+
return structuredClone(value);
|
|
322
|
+
if (field.interpolation === "env")
|
|
323
|
+
return preserveEnvironmentReferences(value);
|
|
324
|
+
return "<redacted>";
|
|
325
|
+
}
|
|
326
|
+
function safeValue(field, value) {
|
|
327
|
+
if (field.sensitivity === "sensitive")
|
|
328
|
+
return "<redacted>";
|
|
329
|
+
return structuredClone(value);
|
|
330
|
+
}
|
|
331
|
+
function valueFingerprint(value) {
|
|
332
|
+
return createHash("sha256").update(stableJson(value)).digest("base64url");
|
|
333
|
+
}
|
|
334
|
+
function stableJson(value) {
|
|
335
|
+
return JSON.stringify(normalizeForFingerprint(value)) ?? "undefined";
|
|
336
|
+
}
|
|
337
|
+
function normalizeForFingerprint(value) {
|
|
338
|
+
if (Array.isArray(value))
|
|
339
|
+
return value.map(normalizeForFingerprint);
|
|
340
|
+
if (!isRecord(value))
|
|
341
|
+
return value;
|
|
342
|
+
return Object.fromEntries(Object.entries(value)
|
|
343
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
344
|
+
.map(([key, entry]) => [key, normalizeForFingerprint(entry)]));
|
|
345
|
+
}
|
|
346
|
+
function preserveEnvironmentReferences(value) {
|
|
347
|
+
if (typeof value === "string") {
|
|
348
|
+
return /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value) ? value : "<redacted>";
|
|
349
|
+
}
|
|
350
|
+
if (Array.isArray(value))
|
|
351
|
+
return value.map((entry) => preserveEnvironmentReferences(entry));
|
|
352
|
+
if (isRecord(value)) {
|
|
353
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, preserveEnvironmentReferences(entry)]));
|
|
354
|
+
}
|
|
355
|
+
return "<redacted>";
|
|
356
|
+
}
|
|
357
|
+
function appendFieldDeprecations(diagnostics, field, candidates, logicalPath) {
|
|
358
|
+
if (!field.deprecation)
|
|
359
|
+
return;
|
|
360
|
+
const seen = new Set();
|
|
361
|
+
for (const candidate of candidates) {
|
|
362
|
+
if (candidate.source.scope === "built-in" || seen.has(candidate.source.id))
|
|
363
|
+
continue;
|
|
364
|
+
seen.add(candidate.source.id);
|
|
365
|
+
diagnostics.push({
|
|
366
|
+
severity: "warning",
|
|
367
|
+
code: "deprecated_source",
|
|
368
|
+
source: candidate.source,
|
|
369
|
+
logicalPath,
|
|
370
|
+
message: `${logicalPath} is deprecated since ForgeRelay ${field.deprecation.since}` +
|
|
371
|
+
(field.deprecation.replacement ? `; use ${field.deprecation.replacement}.` : "."),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function sourceSchemaDiagnostic(definition, source, reference) {
|
|
376
|
+
if (source.kind !== "file" || definition.schemaOutput === "none")
|
|
377
|
+
return undefined;
|
|
378
|
+
if (source.scope !== "user" && source.scope !== "project" && source.scope !== "project-local")
|
|
379
|
+
return undefined;
|
|
380
|
+
if (!isRecord(source.value) || typeof source.value.$schema !== "string")
|
|
381
|
+
return undefined;
|
|
382
|
+
const expected = configSchemaId(definition, source.scope);
|
|
383
|
+
if (source.value.$schema === expected)
|
|
384
|
+
return undefined;
|
|
385
|
+
return {
|
|
386
|
+
severity: "warning",
|
|
387
|
+
code: "schema_mismatch",
|
|
388
|
+
source: reference,
|
|
389
|
+
message: `Configuration $schema does not match the current ${definition.domain} ${source.scope} schema; expected ${expected}.`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function sourceDeprecationDiagnostic(source, deprecation) {
|
|
393
|
+
return {
|
|
394
|
+
severity: "warning",
|
|
395
|
+
code: "deprecated_source",
|
|
396
|
+
source,
|
|
397
|
+
message: `Configuration source ${source.location ?? source.id} is deprecated since ForgeRelay ${deprecation.since}` +
|
|
398
|
+
(deprecation.replacement ? `; use ${deprecation.replacement}.` : "."),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function diagnosticForSourceError(source, error) {
|
|
402
|
+
const missingEnvironment = missingEnvironmentName(error);
|
|
403
|
+
if (missingEnvironment) {
|
|
404
|
+
return {
|
|
405
|
+
severity: "error",
|
|
406
|
+
code: "missing_environment",
|
|
407
|
+
source,
|
|
408
|
+
message: `Required environment variable ${missingEnvironment} is not available.`,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (error instanceof z.ZodError) {
|
|
412
|
+
return {
|
|
413
|
+
severity: "error",
|
|
414
|
+
code: "invalid_source",
|
|
415
|
+
source,
|
|
416
|
+
message: error.issues.map((issue) => {
|
|
417
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "config";
|
|
418
|
+
return `${path}: ${issue.message}`;
|
|
419
|
+
}).join("; "),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
severity: "error",
|
|
424
|
+
code: "invalid_source",
|
|
425
|
+
source,
|
|
426
|
+
message: error instanceof Error ? error.message : "Configuration source is invalid.",
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function missingEnvironmentName(error) {
|
|
430
|
+
if (error instanceof MissingEnvironmentError)
|
|
431
|
+
return error.name;
|
|
432
|
+
if (typeof error === "object" &&
|
|
433
|
+
error !== null &&
|
|
434
|
+
"code" in error &&
|
|
435
|
+
error.code === "missing_environment" &&
|
|
436
|
+
"variable" in error &&
|
|
437
|
+
typeof error.variable === "string") {
|
|
438
|
+
return error.variable;
|
|
439
|
+
}
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
function sourceObject(value) {
|
|
443
|
+
if (!isRecord(value))
|
|
444
|
+
throw new Error("Configuration source must be a JSON object.");
|
|
445
|
+
return value;
|
|
446
|
+
}
|
|
447
|
+
function keyedObject(value, logicalPath) {
|
|
448
|
+
if (!isRecord(value))
|
|
449
|
+
throw new Error(`${logicalPath} must resolve to a keyed object.`);
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
function isDisabledTombstone(value) {
|
|
453
|
+
return isRecord(value) && value.disabled === true;
|
|
454
|
+
}
|
|
455
|
+
function stripSchemaMetadata(value) {
|
|
456
|
+
const { $schema: _schema, ...rest } = value;
|
|
457
|
+
return rest;
|
|
458
|
+
}
|
|
459
|
+
function sourceReference(source) {
|
|
460
|
+
return {
|
|
461
|
+
id: source.id,
|
|
462
|
+
scope: source.scope,
|
|
463
|
+
kind: source.kind,
|
|
464
|
+
...(source.location ? { location: source.location } : {}),
|
|
465
|
+
priority: source.priority,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function compareCandidates(left, right) {
|
|
469
|
+
return compareSourceReferences(left.source, right.source);
|
|
470
|
+
}
|
|
471
|
+
function compareSourceReferences(left, right) {
|
|
472
|
+
const scope = SCOPE_RANK[right.scope] - SCOPE_RANK[left.scope];
|
|
473
|
+
if (scope !== 0)
|
|
474
|
+
return scope;
|
|
475
|
+
const priority = right.priority - left.priority;
|
|
476
|
+
if (priority !== 0)
|
|
477
|
+
return priority;
|
|
478
|
+
return left.id.localeCompare(right.id);
|
|
479
|
+
}
|
|
480
|
+
function shadowReason(winner, candidate) {
|
|
481
|
+
return winner.source.scope === candidate.source.scope ? "higher-priority" : "higher-scope";
|
|
482
|
+
}
|
|
483
|
+
function isRecord(value) {
|
|
484
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
485
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ConfigSourceRuntime } from "./source-refresh.js";
|
|
3
|
+
export class ConfigRuntime {
|
|
4
|
+
sources = new ConfigSourceRuntime();
|
|
5
|
+
applied = new Map();
|
|
6
|
+
resolutionInputs = new Map();
|
|
7
|
+
captureResolutionInputs(domain, inputs) {
|
|
8
|
+
this.resolutionInputs.set(domain, {
|
|
9
|
+
environment: { ...inputs.environment },
|
|
10
|
+
...(inputs.cli ? { cli: structuredClone(inputs.cli) } : {}),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
resolutionInputsFor(domain) {
|
|
14
|
+
const inputs = this.resolutionInputs.get(domain);
|
|
15
|
+
if (!inputs)
|
|
16
|
+
throw new Error(`No runtime resolution inputs are available for ${domain}.`);
|
|
17
|
+
return {
|
|
18
|
+
environment: { ...inputs.environment },
|
|
19
|
+
...(inputs.cli ? { cli: structuredClone(inputs.cli) } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
captureApplied(definition, values) {
|
|
23
|
+
const domain = new Map();
|
|
24
|
+
for (const [name, field] of Object.entries(definition.fields)) {
|
|
25
|
+
if (field.reload !== "restart-required")
|
|
26
|
+
continue;
|
|
27
|
+
domain.set(name, structuredClone(values[name]));
|
|
28
|
+
}
|
|
29
|
+
this.applied.set(definition.domain, domain);
|
|
30
|
+
}
|
|
31
|
+
snapshotApplied(definition, configuredValues) {
|
|
32
|
+
const startup = this.applied.get(definition.domain);
|
|
33
|
+
if (!startup) {
|
|
34
|
+
throw new Error(`No applied configuration snapshot is available for ${definition.domain}.`);
|
|
35
|
+
}
|
|
36
|
+
const fields = {};
|
|
37
|
+
let restartRequired = false;
|
|
38
|
+
for (const [name, field] of Object.entries(definition.fields)) {
|
|
39
|
+
if (field.reload !== "restart-required")
|
|
40
|
+
continue;
|
|
41
|
+
const appliedValue = startup.get(name);
|
|
42
|
+
const configuredValue = configuredValues[name];
|
|
43
|
+
const changed = valueFingerprint(appliedValue) !== valueFingerprint(configuredValue);
|
|
44
|
+
restartRequired ||= changed;
|
|
45
|
+
fields[name] = {
|
|
46
|
+
logicalPath: `${definition.domain}.${name}`,
|
|
47
|
+
configuredValue: safeValue(field.sensitivity, configuredValue),
|
|
48
|
+
appliedValue: safeValue(field.sensitivity, appliedValue),
|
|
49
|
+
restartRequired: changed,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return { domain: definition.domain, restartRequired, fields };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function safeValue(sensitivity, value) {
|
|
56
|
+
if (sensitivity === "sensitive" && value !== undefined)
|
|
57
|
+
return "<redacted>";
|
|
58
|
+
return structuredClone(value);
|
|
59
|
+
}
|
|
60
|
+
function valueFingerprint(value) {
|
|
61
|
+
return createHash("sha256").update(stableJson(value)).digest("base64url");
|
|
62
|
+
}
|
|
63
|
+
function stableJson(value) {
|
|
64
|
+
return JSON.stringify(normalize(value)) ?? "undefined";
|
|
65
|
+
}
|
|
66
|
+
function normalize(value) {
|
|
67
|
+
if (Array.isArray(value))
|
|
68
|
+
return value.map(normalize);
|
|
69
|
+
if (typeof value !== "object" || value === null)
|
|
70
|
+
return value;
|
|
71
|
+
return Object.fromEntries(Object.entries(value)
|
|
72
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
73
|
+
.map(([key, entry]) => [key, normalize(entry)]));
|
|
74
|
+
}
|