dsh-agent-team-ui-testing 0.1.1 → 0.1.2
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/LICENSE +21 -0
- package/README.md +170 -35
- package/annotation-capture.js +111 -0
- package/browser-safety.js +45 -1
- package/client.js +3709 -134
- package/context-menu.js +134 -0
- package/core/clock.js +88 -0
- package/core/config-store.js +566 -0
- package/core/config.js +892 -0
- package/core/contracts.js +240 -0
- package/core/errors.js +156 -0
- package/core/evidence.js +141 -0
- package/core/geometry.js +217 -0
- package/core/identity.js +180 -0
- package/core/index.js +24 -0
- package/core/p0.js +1101 -0
- package/core/readiness.js +175 -0
- package/core/registry.js +239 -0
- package/core/tab-access.js +1350 -0
- package/docs/adapters.md +160 -0
- package/docs/architecture.md +44 -9
- package/docs/capability-tiers.md +20 -4
- package/docs/capture-p0.md +39 -2
- package/docs/diagnose-reason-codes.md +157 -0
- package/docs/element-picker.md +24 -6
- package/docs/getting-started.md +47 -1
- package/docs/index.md +10 -7
- package/docs/multi-tab.md +55 -20
- package/docs/network-p0.md +2 -2
- package/docs/panel-context-menu.md +41 -0
- package/docs/t2-structured-gui.md +2 -2
- package/docs/t3-vision.md +6 -1
- package/docs/t4-dynamic-behavior.md +1 -0
- package/docs/tools.md +65 -7
- package/docs/verification.md +26 -2
- package/docs/viewport-control.md +21 -8
- package/index.js +3263 -251
- package/integration/cross-environment.js +1535 -0
- package/integration/diagnose.js +945 -0
- package/integration/tab-principal.js +493 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/network-p0.js +24 -7
- package/package.json +4 -3
- package/select-control.js +298 -0
- package/skills/ui-testing-common/SKILL.md +54 -0
- package/skills/ui-testing-fake/SKILL.md +43 -0
- package/skills/{ui-testing → ui-testing-web}/SKILL.md +64 -24
- package/skills/{ui-testing → ui-testing-web}/manifests/declared-capabilities.json +9 -2
- package/skills/{ui-testing → ui-testing-web}/references/contracts/capability.md +3 -3
- package/skills/{ui-testing → ui-testing-web}/references/contracts/evidence.md +1 -1
- package/skills/{ui-testing → ui-testing-web}/references/contracts/operation.md +1 -1
- package/skills/{ui-testing → ui-testing-web}/references/core/fallback.md +1 -1
- package/skills/{ui-testing → ui-testing-web}/references/core/page-identity.md +3 -2
- package/skills/{ui-testing → ui-testing-web}/references/core/selection.md +1 -1
- package/skills/{ui-testing → ui-testing-web}/references/core/tool-routing-and-limits.md +6 -3
- package/skills/{ui-testing → ui-testing-web}/references/platforms/index.md +1 -1
- package/skills/{ui-testing → ui-testing-web}/references/platforms/web.md +2 -2
- package/skills/ui-testing-web/references/scenarios/annotations.md +104 -0
- package/skills/{ui-testing → ui-testing-web}/references/scenarios/functional.md +1 -1
- package/skills/{ui-testing → ui-testing-web}/references/scenarios/visual-motion.md +1 -1
- package/skills/{ui-testing → ui-testing-web}/references/tiers/t1-programmatic.md +2 -2
- package/skills/{ui-testing → ui-testing-web}/references/tiers/t3-static-visual.md +2 -2
- package/skills/{ui-testing → ui-testing-web}/references/tiers/t4-dynamic-visual.md +2 -1
- package/targets/fake/adapter.js +1163 -0
- package/targets/fake/fixtures.js +177 -0
- package/targets/web/adapter.js +1231 -0
- package/targets/web/provider.js +415 -0
- package/skills/ui-testing/references/scenarios/annotations.md +0 -87
- /package/skills/{ui-testing → ui-testing-web}/references/core/dynamic-visual.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/references/core/network-and-state.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/references/core/report-template.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/references/core/reporting.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/references/core/safety.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/references/core/verdict-examples.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/references/core/verdict-gate.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/references/core/verification.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/references/core/workflow.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/references/tiers/t2-structured.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/templates/capability-note.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/templates/platform.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/templates/regression-case.md +0 -0
- /package/skills/{ui-testing → ui-testing-web}/templates/scenario.md +0 -0
package/core/config.js
ADDED
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration model and resolution chain (M0 §4).
|
|
3
|
+
*
|
|
4
|
+
* One authoritative chain: global -> workspace -> session, resolved into an immutable snapshot
|
|
5
|
+
* that every adapter consumes. Pure functions only; persistence lives in config-store.js.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
CONTRACT_VERSION,
|
|
9
|
+
ENVIRONMENTS,
|
|
10
|
+
ENVIRONMENT_CAPABILITIES,
|
|
11
|
+
ERROR_CODES,
|
|
12
|
+
WEB_PROVIDER_SUPPORT,
|
|
13
|
+
} from './contracts.js';
|
|
14
|
+
import { AdapterError } from './errors.js';
|
|
15
|
+
import { workspaceKeyOf } from './identity.js';
|
|
16
|
+
|
|
17
|
+
/** Persisted config schema version. */
|
|
18
|
+
export const CONFIG_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Web *runtime providers* (the browser that actually executes): one target environment (`web`),
|
|
22
|
+
* one provider per selection. This is a different axis from `environment` and from the P0
|
|
23
|
+
* `P0Provider` class — never conflate the names (see docs/internal/web-runtime-provider-contract.md §4.2).
|
|
24
|
+
*/
|
|
25
|
+
export const WEB_PROVIDERS = Object.freeze({
|
|
26
|
+
CHROME: 'chrome',
|
|
27
|
+
EDGE: 'edge',
|
|
28
|
+
FIREFOX: 'firefox',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** Known provider ids, in report order. Anything else is not a provider name. */
|
|
32
|
+
export const WEB_PROVIDER_IDS = Object.freeze([
|
|
33
|
+
WEB_PROVIDERS.CHROME,
|
|
34
|
+
WEB_PROVIDERS.EDGE,
|
|
35
|
+
WEB_PROVIDERS.FIREFOX,
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/** Provider used when a legacy config has no `provider` field at all (config v1 compatibility). */
|
|
39
|
+
export const DEFAULT_WEB_PROVIDER = WEB_PROVIDERS.CHROME;
|
|
40
|
+
|
|
41
|
+
/** Engineering support for one provider id (`supported|pending|unsupported`, null when unknown). */
|
|
42
|
+
export function webProviderSupport(provider) {
|
|
43
|
+
return Object.hasOwn(WEB_PROVIDER_SUPPORT, provider) ? WEB_PROVIDER_SUPPORT[provider] : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Providers the execution layer can currently drive — **derived** from `WEB_PROVIDER_SUPPORT`, so the
|
|
48
|
+
* config layer, registration, readiness and execution can never disagree. `edge` is `pending` today:
|
|
49
|
+
* its switch is storable, but it does not count as implemented and never executes.
|
|
50
|
+
*/
|
|
51
|
+
export const IMPLEMENTED_WEB_PROVIDERS = Object.freeze(
|
|
52
|
+
WEB_PROVIDER_IDS.filter((provider) => webProviderSupport(provider) === 'supported'),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/** True when a driver exists for this provider id. */
|
|
56
|
+
export function isImplementedWebProvider(provider) {
|
|
57
|
+
return webProviderSupport(provider) === 'supported';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Known environment metadata. `planned` environments are placeholders: they may be displayed but
|
|
62
|
+
* never enabled or selected in v1.
|
|
63
|
+
*/
|
|
64
|
+
export const KNOWN_ENVIRONMENTS = Object.freeze({
|
|
65
|
+
[ENVIRONMENTS.WEB]: Object.freeze({
|
|
66
|
+
environment: ENVIRONMENTS.WEB,
|
|
67
|
+
status: 'supported',
|
|
68
|
+
testOnly: false,
|
|
69
|
+
planned: false,
|
|
70
|
+
}),
|
|
71
|
+
[ENVIRONMENTS.FAKE]: Object.freeze({
|
|
72
|
+
environment: ENVIRONMENTS.FAKE,
|
|
73
|
+
status: 'supported',
|
|
74
|
+
testOnly: true,
|
|
75
|
+
planned: false,
|
|
76
|
+
}),
|
|
77
|
+
[ENVIRONMENTS.LINUX]: Object.freeze({
|
|
78
|
+
environment: ENVIRONMENTS.LINUX,
|
|
79
|
+
status: 'planned',
|
|
80
|
+
testOnly: false,
|
|
81
|
+
planned: true,
|
|
82
|
+
}),
|
|
83
|
+
[ENVIRONMENTS.ANDROID]: Object.freeze({
|
|
84
|
+
environment: ENVIRONMENTS.ANDROID,
|
|
85
|
+
status: 'planned',
|
|
86
|
+
testOnly: false,
|
|
87
|
+
planned: true,
|
|
88
|
+
}),
|
|
89
|
+
[ENVIRONMENTS.MINIPROGRAM]: Object.freeze({
|
|
90
|
+
environment: ENVIRONMENTS.MINIPROGRAM,
|
|
91
|
+
status: 'planned',
|
|
92
|
+
testOnly: false,
|
|
93
|
+
planned: true,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const PLANNED_ENVIRONMENTS = new Set(
|
|
98
|
+
Object.values(KNOWN_ENVIRONMENTS).filter((meta) => meta.planned).map((meta) => meta.environment),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
/** 该目标环境是否仍是"规划中"(v1 没有适配器,可启用但不可执行)。 */
|
|
102
|
+
export function isPlannedEnvironment(environment) {
|
|
103
|
+
return PLANNED_ENVIRONMENTS.has(environment);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function deepFreeze(value) {
|
|
107
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
108
|
+
Object.freeze(value);
|
|
109
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function clone(value) {
|
|
115
|
+
return structuredClone(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isPlainObject(value) {
|
|
119
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function invalid(message, details) {
|
|
123
|
+
return new AdapterError(ERROR_CODES.CONFIG_INVALID, message, { details });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertPlainObject(value, field) {
|
|
127
|
+
if (!isPlainObject(value)) throw invalid(`${field} must be an object`, { field });
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function assertRevision(value, field = 'revision') {
|
|
132
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
133
|
+
throw invalid(`${field} must be a non-negative integer`, { field, value: String(value) });
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Per-provider switches for a fresh web environment entry: chrome on, the others off, and no
|
|
140
|
+
* second copy of the "which providers are enabled" list (it is always derived).
|
|
141
|
+
*/
|
|
142
|
+
function defaultWebProviders() {
|
|
143
|
+
return {
|
|
144
|
+
[WEB_PROVIDERS.CHROME]: { enabled: true },
|
|
145
|
+
[WEB_PROVIDERS.EDGE]: { enabled: false },
|
|
146
|
+
[WEB_PROVIDERS.FIREFOX]: { enabled: false },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Default config: web is the only enabled environment, chrome the only enabled provider. */
|
|
151
|
+
export const DEFAULT_CONFIG = deepFreeze({
|
|
152
|
+
version: CONFIG_VERSION,
|
|
153
|
+
revision: 0,
|
|
154
|
+
global: {
|
|
155
|
+
environment: ENVIRONMENTS.WEB,
|
|
156
|
+
enabledEnvironments: [ENVIRONMENTS.WEB],
|
|
157
|
+
environments: {
|
|
158
|
+
[ENVIRONMENTS.WEB]: {
|
|
159
|
+
enabled: true,
|
|
160
|
+
targetId: null,
|
|
161
|
+
provider: DEFAULT_WEB_PROVIDER,
|
|
162
|
+
providers: {
|
|
163
|
+
[WEB_PROVIDERS.CHROME]: { enabled: true },
|
|
164
|
+
[WEB_PROVIDERS.EDGE]: { enabled: false },
|
|
165
|
+
[WEB_PROVIDERS.FIREFOX]: { enabled: false },
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
[ENVIRONMENTS.FAKE]: { enabled: false, targetId: null },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
workspaces: {},
|
|
172
|
+
sessions: {},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
function defaultEnvironmentEntry(environment) {
|
|
176
|
+
const entry = {
|
|
177
|
+
enabled: environment === ENVIRONMENTS.WEB,
|
|
178
|
+
targetId: null,
|
|
179
|
+
};
|
|
180
|
+
if (environment === ENVIRONMENTS.WEB) {
|
|
181
|
+
entry.provider = DEFAULT_WEB_PROVIDER;
|
|
182
|
+
entry.providers = defaultWebProviders();
|
|
183
|
+
}
|
|
184
|
+
return entry;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Merge provider maps per provider id: sibling providers are never replaced wholesale. */
|
|
188
|
+
function mergeProviderMaps(base, over) {
|
|
189
|
+
const out = {};
|
|
190
|
+
for (const [provider, entry] of Object.entries(base ?? {})) out[provider] = { ...entry };
|
|
191
|
+
for (const [provider, entry] of Object.entries(over ?? {})) {
|
|
192
|
+
out[provider] = { ...(out[provider] ?? {}), ...entry };
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* `allowPlanned: true` 只用于**启用**类写入(`environments.<id>.enabled`):
|
|
199
|
+
* v1 里 android / linux 等环境没有适配器,但允许用户先把开关打开——启用 ≠ 可执行,
|
|
200
|
+
* 可执行性由就绪六轴(adapter-not-registered / support=planned)如实表达。
|
|
201
|
+
* 选择"当前执行环境"仍然拒绝 planned(见 `environment` 的写入路径)。
|
|
202
|
+
*/
|
|
203
|
+
function normalizeEnvironmentName(environment, field, { allowTestEnvironments = true, allowPlanned = false } = {}) {
|
|
204
|
+
if (typeof environment !== 'string' || environment.length === 0) {
|
|
205
|
+
throw invalid(`${field} must be a non-empty string`, { field });
|
|
206
|
+
}
|
|
207
|
+
const meta = KNOWN_ENVIRONMENTS[environment];
|
|
208
|
+
if (!meta) throw invalid(`unknown environment: ${environment}`, { field, environment });
|
|
209
|
+
if (meta.planned && !allowPlanned) {
|
|
210
|
+
throw invalid(`environment ${environment} is planned and cannot be selected in v1`, {
|
|
211
|
+
field,
|
|
212
|
+
environment,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (meta.testOnly && !allowTestEnvironments) {
|
|
216
|
+
throw invalid(`environment ${environment} is test-only`, { field, environment });
|
|
217
|
+
}
|
|
218
|
+
return environment;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizeEnabledEnvironments(list, field, options) {
|
|
222
|
+
if (!Array.isArray(list)) throw invalid(`${field} must be an array`, { field });
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const item of list) {
|
|
225
|
+
const environment = normalizeEnvironmentName(item, field, options);
|
|
226
|
+
if (!out.includes(environment)) out.push(environment);
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Validate a provider name.
|
|
233
|
+
*
|
|
234
|
+
* Write path (`strict`, i.e. every patch path): only chrome/edge/firefox are accepted, anything else
|
|
235
|
+
* is CONFIG_INVALID with the offending field — never a silent fallback.
|
|
236
|
+
*
|
|
237
|
+
* Load path (`strict:false`): a *well-typed but unknown* name (e.g. `"safari"` written by a newer
|
|
238
|
+
* version) is **kept verbatim**. Throwing here would make the store treat the whole file as corrupt,
|
|
239
|
+
* replace it with defaults and discard every saved switch — far worse than carrying one unusable
|
|
240
|
+
* selection. The snapshot then reports `source.rejectedProviderReason = 'unknown-provider'`, which
|
|
241
|
+
* blocks execution instead of quietly switching browsers. A value that is not a non-empty string is
|
|
242
|
+
* a structural error on both paths.
|
|
243
|
+
*/
|
|
244
|
+
function normalizeWebProviderName(value, field, { allowNull = false, strict = true } = {}) {
|
|
245
|
+
if (allowNull && value === null) return null;
|
|
246
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
247
|
+
throw invalid(`unknown web provider: ${String(value)}`, {
|
|
248
|
+
field,
|
|
249
|
+
provider: value === undefined ? null : String(value),
|
|
250
|
+
allowed: [...WEB_PROVIDER_IDS],
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (WEB_PROVIDER_IDS.includes(value)) return value;
|
|
254
|
+
if (strict) {
|
|
255
|
+
throw invalid(`unknown web provider: ${value}`, {
|
|
256
|
+
field,
|
|
257
|
+
provider: value,
|
|
258
|
+
allowed: [...WEB_PROVIDER_IDS],
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const PROVIDER_ENTRY_KEYS = new Set(['enabled', 'executable', 'connection']);
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* One provider entry. `partial` keeps the patch sparse (missing key = unchanged); `strict` rejects
|
|
268
|
+
* unknown keys instead of dropping them (write path).
|
|
269
|
+
*/
|
|
270
|
+
function normalizeProviderEntry(raw, field, { partial = false, strict = false } = {}) {
|
|
271
|
+
const source = assertPlainObject(raw, field);
|
|
272
|
+
if (strict) {
|
|
273
|
+
for (const key of Object.keys(source)) {
|
|
274
|
+
if (!PROVIDER_ENTRY_KEYS.has(key)) {
|
|
275
|
+
throw invalid(`unknown provider patch key: ${field}.${key}`, { field: `${field}.${key}` });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const out = {};
|
|
280
|
+
if (source.enabled !== undefined) {
|
|
281
|
+
if (typeof source.enabled !== 'boolean') {
|
|
282
|
+
throw invalid(`${field}.enabled must be a boolean`, { field: `${field}.enabled` });
|
|
283
|
+
}
|
|
284
|
+
out.enabled = source.enabled;
|
|
285
|
+
}
|
|
286
|
+
if (source.executable !== undefined) {
|
|
287
|
+
if (source.executable !== null && (typeof source.executable !== 'string' || source.executable.length === 0)) {
|
|
288
|
+
throw invalid(`${field}.executable must be a non-empty string or null`, {
|
|
289
|
+
field: `${field}.executable`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
out.executable = source.executable;
|
|
293
|
+
}
|
|
294
|
+
if (source.connection !== undefined) {
|
|
295
|
+
out.connection = clone(assertPlainObject(source.connection, `${field}.connection`));
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Provider sub-map: per-provider deep shape, unknown ids dropped (load) or rejected (patch). */
|
|
301
|
+
function normalizeProvidersMap(raw, field, { partial = false, strict = false } = {}) {
|
|
302
|
+
const source = assertPlainObject(raw, field);
|
|
303
|
+
const out = {};
|
|
304
|
+
for (const [provider, entry] of Object.entries(source)) {
|
|
305
|
+
if (!WEB_PROVIDER_IDS.includes(provider)) {
|
|
306
|
+
if (strict) {
|
|
307
|
+
throw invalid(`unknown web provider: ${provider}`, {
|
|
308
|
+
field: `${field}.${provider}`,
|
|
309
|
+
provider,
|
|
310
|
+
allowed: [...WEB_PROVIDER_IDS],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
out[provider] = normalizeProviderEntry(entry, `${field}.${provider}`, { partial, strict });
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function normalizeEnvironmentEntry(raw, environment, { partial = false, strict = false } = {}) {
|
|
321
|
+
const field = `environments.${environment}`;
|
|
322
|
+
const source = assertPlainObject(raw, field);
|
|
323
|
+
const base = partial ? {} : defaultEnvironmentEntry(environment);
|
|
324
|
+
const out = { ...base };
|
|
325
|
+
if (source.enabled !== undefined) {
|
|
326
|
+
if (typeof source.enabled !== 'boolean') {
|
|
327
|
+
throw invalid(`${field}.enabled must be a boolean`, { field: `${field}.enabled` });
|
|
328
|
+
}
|
|
329
|
+
out.enabled = source.enabled;
|
|
330
|
+
}
|
|
331
|
+
if (source.targetId !== undefined) {
|
|
332
|
+
if (source.targetId !== null && typeof source.targetId !== 'string') {
|
|
333
|
+
throw invalid(`${field}.targetId must be a string or null`, { field: `${field}.targetId` });
|
|
334
|
+
}
|
|
335
|
+
out.targetId = source.targetId;
|
|
336
|
+
}
|
|
337
|
+
if (source.connection !== undefined) {
|
|
338
|
+
out.connection = clone(assertPlainObject(source.connection, `${field}.connection`));
|
|
339
|
+
}
|
|
340
|
+
const webOnly = environment === ENVIRONMENTS.WEB;
|
|
341
|
+
if (source.provider !== undefined) {
|
|
342
|
+
if (!webOnly) {
|
|
343
|
+
throw invalid(`${field}.provider is only supported for the web environment`, {
|
|
344
|
+
field: `${field}.provider`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
out.provider = normalizeWebProviderName(source.provider, `${field}.provider`, { strict });
|
|
348
|
+
}
|
|
349
|
+
if (source.providers !== undefined) {
|
|
350
|
+
if (!webOnly) {
|
|
351
|
+
throw invalid(`${field}.providers is only supported for the web environment`, {
|
|
352
|
+
field: `${field}.providers`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const provided = normalizeProvidersMap(source.providers, `${field}.providers`, { partial, strict });
|
|
356
|
+
out.providers = mergeProviderMaps(out.providers, provided);
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function normalizeEnvironmentMap(raw, field, { partial = false, strict = false } = {}) {
|
|
362
|
+
const source = assertPlainObject(raw, field);
|
|
363
|
+
const out = {};
|
|
364
|
+
for (const [environment, entry] of Object.entries(source)) {
|
|
365
|
+
// 未知环境丢弃(不猜);**planned 环境保留**——这一层是"每个环境的总开关",
|
|
366
|
+
// 「启用」不等于「可执行」:v1 里 android 等环境没有适配器,但用户可以先把它打开,
|
|
367
|
+
// 是否真的能跑由就绪六轴与"当前执行环境"的校验表达。
|
|
368
|
+
if (!Object.hasOwn(KNOWN_ENVIRONMENTS, environment)) continue;
|
|
369
|
+
out[environment] = normalizeEnvironmentEntry(entry, environment, { partial, strict });
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function normalizeGlobal(raw) {
|
|
375
|
+
const source = assertPlainObject(raw, 'global');
|
|
376
|
+
const environments = {
|
|
377
|
+
[ENVIRONMENTS.WEB]: { ...defaultEnvironmentEntry(ENVIRONMENTS.WEB) },
|
|
378
|
+
[ENVIRONMENTS.FAKE]: { ...defaultEnvironmentEntry(ENVIRONMENTS.FAKE) },
|
|
379
|
+
};
|
|
380
|
+
const provided = normalizeEnvironmentMap(source.environments ?? {}, 'global.environments');
|
|
381
|
+
for (const [environment, entry] of Object.entries(provided)) {
|
|
382
|
+
environments[environment] = { ...environments[environment], ...entry };
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
environment: source.environment === undefined
|
|
386
|
+
? ENVIRONMENTS.WEB
|
|
387
|
+
: normalizeEnvironmentName(source.environment, 'global.environment'),
|
|
388
|
+
enabledEnvironments: source.enabledEnvironments === undefined
|
|
389
|
+
? [ENVIRONMENTS.WEB]
|
|
390
|
+
: normalizeEnabledEnvironments(source.enabledEnvironments, 'global.enabledEnvironments'),
|
|
391
|
+
environments,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function normalizeWorkspaceEntry(raw, key) {
|
|
396
|
+
const source = assertPlainObject(raw, `workspaces.${key}`);
|
|
397
|
+
const out = {};
|
|
398
|
+
if (source.environment !== undefined) {
|
|
399
|
+
out.environment = normalizeEnvironmentName(source.environment, `workspaces.${key}.environment`);
|
|
400
|
+
}
|
|
401
|
+
if (source.enabledEnvironments !== undefined) {
|
|
402
|
+
out.enabledEnvironments = normalizeEnabledEnvironments(
|
|
403
|
+
source.enabledEnvironments,
|
|
404
|
+
`workspaces.${key}.enabledEnvironments`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (source.environments !== undefined) {
|
|
408
|
+
out.environments = normalizeEnvironmentMap(
|
|
409
|
+
source.environments,
|
|
410
|
+
`workspaces.${key}.environments`,
|
|
411
|
+
{ partial: true },
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
if (source.targetId !== undefined) {
|
|
415
|
+
if (source.targetId !== null && typeof source.targetId !== 'string') {
|
|
416
|
+
throw invalid(`workspaces.${key}.targetId must be a string or null`, {
|
|
417
|
+
field: `workspaces.${key}.targetId`,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
out.targetId = source.targetId;
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function normalizeSessionEntry(raw, key) {
|
|
426
|
+
const source = assertPlainObject(raw, `sessions.${key}`);
|
|
427
|
+
const out = {};
|
|
428
|
+
if (source.environment !== undefined) {
|
|
429
|
+
out.environment = normalizeEnvironmentName(source.environment, `sessions.${key}.environment`);
|
|
430
|
+
}
|
|
431
|
+
if (source.targetId !== undefined) {
|
|
432
|
+
if (source.targetId !== null && typeof source.targetId !== 'string') {
|
|
433
|
+
throw invalid(`sessions.${key}.targetId must be a string or null`, {
|
|
434
|
+
field: `sessions.${key}.targetId`,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
out.targetId = source.targetId;
|
|
438
|
+
}
|
|
439
|
+
if (source.parentSession !== undefined) {
|
|
440
|
+
if (source.parentSession !== null && typeof source.parentSession !== 'string') {
|
|
441
|
+
throw invalid(`sessions.${key}.parentSession must be a string or null`, {
|
|
442
|
+
field: `sessions.${key}.parentSession`,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
out.parentSession = source.parentSession;
|
|
446
|
+
}
|
|
447
|
+
// Session-scoped provider selection (`null` clears it and re-inherits).
|
|
448
|
+
if (source.webProvider !== undefined) {
|
|
449
|
+
out.webProvider = normalizeWebProviderName(source.webProvider, `sessions.${key}.webProvider`, {
|
|
450
|
+
allowNull: true,
|
|
451
|
+
strict: false,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Fill defaults, drop unknown keys, and reject structurally invalid configs.
|
|
459
|
+
*
|
|
460
|
+
* @param {object|null|undefined} raw
|
|
461
|
+
* @returns {object} a fresh mutable config object (never the frozen default instance)
|
|
462
|
+
*/
|
|
463
|
+
export function normalizeConfig(raw) {
|
|
464
|
+
if (raw === undefined || raw === null) return clone(DEFAULT_CONFIG);
|
|
465
|
+
const source = assertPlainObject(raw, 'config');
|
|
466
|
+
if (source.version !== undefined && source.version !== CONFIG_VERSION) {
|
|
467
|
+
throw invalid(`unsupported config version: ${String(source.version)}`, {
|
|
468
|
+
field: 'version',
|
|
469
|
+
version: source.version,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
const workspaces = {};
|
|
473
|
+
const rawWorkspaces = source.workspaces === undefined
|
|
474
|
+
? {}
|
|
475
|
+
: assertPlainObject(source.workspaces, 'workspaces');
|
|
476
|
+
for (const [key, entry] of Object.entries(rawWorkspaces)) {
|
|
477
|
+
const normalized = normalizeWorkspaceEntry(entry, key);
|
|
478
|
+
if (Object.keys(normalized).length > 0) workspaces[key] = normalized;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const sessions = {};
|
|
482
|
+
const rawSessions = source.sessions === undefined
|
|
483
|
+
? {}
|
|
484
|
+
: assertPlainObject(source.sessions, 'sessions');
|
|
485
|
+
for (const [key, entry] of Object.entries(rawSessions)) {
|
|
486
|
+
const normalized = normalizeSessionEntry(entry, key);
|
|
487
|
+
if (Object.keys(normalized).length > 0) sessions[key] = normalized;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
version: CONFIG_VERSION,
|
|
492
|
+
revision: source.revision === undefined ? 0 : assertRevision(source.revision),
|
|
493
|
+
global: normalizeGlobal(source.global ?? {}),
|
|
494
|
+
workspaces,
|
|
495
|
+
sessions,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const GLOBAL_PATCH_KEYS = new Set(['environment', 'enabledEnvironments', 'environments', 'targetId']);
|
|
500
|
+
const WORKSPACE_PATCH_KEYS = new Set(['environment', 'enabledEnvironments', 'environments', 'targetId']);
|
|
501
|
+
const SESSION_PATCH_KEYS = new Set(['environment', 'targetId', 'webProvider']);
|
|
502
|
+
const ENVIRONMENT_ENTRY_KEYS = new Set(['enabled', 'targetId', 'connection', 'provider', 'providers']);
|
|
503
|
+
|
|
504
|
+
function normalizePatchEnvironments(raw, field, options) {
|
|
505
|
+
const source = assertPlainObject(raw, field);
|
|
506
|
+
const out = {};
|
|
507
|
+
for (const [environment, entry] of Object.entries(source)) {
|
|
508
|
+
// 这里是"每个环境的总开关",不是"当前执行环境":planned 环境允许被启用。
|
|
509
|
+
normalizeEnvironmentName(environment, field, { ...(options || {}), allowPlanned: true });
|
|
510
|
+
const entrySource = assertPlainObject(entry, `${field}.${environment}`);
|
|
511
|
+
for (const key of Object.keys(entrySource)) {
|
|
512
|
+
if (!ENVIRONMENT_ENTRY_KEYS.has(key)) {
|
|
513
|
+
throw invalid(`unknown environment patch key: ${field}.${environment}.${key}`, {
|
|
514
|
+
field: `${field}.${environment}.${key}`,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
out[environment] = normalizeEnvironmentEntry(entrySource, environment, { partial: true, strict: true });
|
|
519
|
+
}
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Validate and normalize a write patch.
|
|
525
|
+
*
|
|
526
|
+
* @param {{scope:'global'|'workspace'|'session', patch:object}} input
|
|
527
|
+
* @param {{allowTestEnvironments?:boolean}} [options]
|
|
528
|
+
* @returns {object} the normalized patch (unknown keys rejected, not silently dropped)
|
|
529
|
+
*/
|
|
530
|
+
export function validatePatch({ scope, patch } = {}, { allowTestEnvironments = false } = {}) {
|
|
531
|
+
if (scope !== 'global' && scope !== 'workspace' && scope !== 'session') {
|
|
532
|
+
throw invalid('patch scope must be global, workspace or session', { scope: String(scope) });
|
|
533
|
+
}
|
|
534
|
+
const source = assertPlainObject(patch, 'patch');
|
|
535
|
+
const keys = Object.keys(source);
|
|
536
|
+
if (keys.length === 0) throw invalid('patch must not be empty', { scope });
|
|
537
|
+
|
|
538
|
+
const allowed = scope === 'session' ? SESSION_PATCH_KEYS : (scope === 'global' ? GLOBAL_PATCH_KEYS : WORKSPACE_PATCH_KEYS);
|
|
539
|
+
for (const key of keys) {
|
|
540
|
+
if (!allowed.has(key)) throw invalid(`unknown ${scope} patch key: ${key}`, { scope, key });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const options = { allowTestEnvironments };
|
|
544
|
+
const out = {};
|
|
545
|
+
if (source.environment !== undefined) {
|
|
546
|
+
out.environment = normalizeEnvironmentName(source.environment, `${scope}.environment`, options);
|
|
547
|
+
}
|
|
548
|
+
if (source.enabledEnvironments !== undefined) {
|
|
549
|
+
out.enabledEnvironments = normalizeEnabledEnvironments(
|
|
550
|
+
source.enabledEnvironments,
|
|
551
|
+
`${scope}.enabledEnvironments`,
|
|
552
|
+
options,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
if (source.environments !== undefined) {
|
|
556
|
+
out.environments = normalizePatchEnvironments(source.environments, `${scope}.environments`, options);
|
|
557
|
+
}
|
|
558
|
+
if (source.targetId !== undefined) {
|
|
559
|
+
if (source.targetId !== null && typeof source.targetId !== 'string') {
|
|
560
|
+
throw invalid(`${scope}.targetId must be a string or null`, { scope, field: 'targetId' });
|
|
561
|
+
}
|
|
562
|
+
out.targetId = source.targetId;
|
|
563
|
+
}
|
|
564
|
+
if (source.webProvider !== undefined) {
|
|
565
|
+
out.webProvider = normalizeWebProviderName(source.webProvider, `${scope}.webProvider`, {
|
|
566
|
+
allowNull: true,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Stable workspace key shared with runtime entries: `workspace:<id>` or `path:<sha256>`.
|
|
574
|
+
* Legacy alias for `workspaceKeyOf({cwd})`; returns null for a missing cwd.
|
|
575
|
+
*/
|
|
576
|
+
export function workspaceKeyForCwd(cwd) {
|
|
577
|
+
return workspaceKeyOf({ cwd });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Capability catalog for one environment; unknown/planned environments expose nothing. */
|
|
581
|
+
export function capabilitiesForEnvironment(environment) {
|
|
582
|
+
const list = ENVIRONMENT_CAPABILITIES[environment];
|
|
583
|
+
return Array.isArray(list) ? [...list] : [];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function agentSessionId(agent) {
|
|
587
|
+
const candidate = agent?.id ?? agent?.sessionId;
|
|
588
|
+
return typeof candidate === 'string' && candidate.length > 0 ? candidate : null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Resolve the effective config snapshot for one agent/workspace.
|
|
593
|
+
*
|
|
594
|
+
* Frozen total order: session own entry > parent's effective snapshot (following
|
|
595
|
+
* `agent.session.header.parentSession`, depth <= 8, cycle-safe) > workspace > global. The parent
|
|
596
|
+
* contributes its *effective* environment/target (already filtered by enabledEnvironments), not
|
|
597
|
+
* its raw config entry.
|
|
598
|
+
*
|
|
599
|
+
* A selection outside `enabledEnvironments` is rejected and falls back to `global.environment`;
|
|
600
|
+
* the rejected value is reported in `source.rejectedEnvironment` (never silently allowed).
|
|
601
|
+
*
|
|
602
|
+
* Web *provider* selection is an independent chain over the same scopes:
|
|
603
|
+
* `session.webProvider > parent's effective provider > workspace.environments.web.provider >
|
|
604
|
+
* global.environments.web.provider > "chrome"` (last step only for legacy configs without the
|
|
605
|
+
* field). A disabled provider is **not** silently replaced: the selection is kept in
|
|
606
|
+
* `snapshot.provider`, and `source.rejectedProvider` + `source.rejectedProviderReason` report that
|
|
607
|
+
* it cannot execute (the execution layer turns that into ADAPTER_DISABLED). `firefox` is a legal
|
|
608
|
+
* provider name that may be enabled, but has no driver: `providerConfig.implemented` is false and
|
|
609
|
+
* the execution layer must refuse with ADAPTER_UNSUPPORTED. A name that is not a known provider at
|
|
610
|
+
* all (`'safari'`, loaded from a newer config file) is preserved and reported as
|
|
611
|
+
* `rejectedProviderReason: 'unknown-provider'` — never replaced by chrome.
|
|
612
|
+
*
|
|
613
|
+
* Malformed config degrades to defaults with `source.degraded=true` + `source.degradedReason`
|
|
614
|
+
* instead of throwing. Only explicit invalid patches throw CONFIG_INVALID (validatePatch).
|
|
615
|
+
*
|
|
616
|
+
* @param {object} config
|
|
617
|
+
* @param {{agent?:object, workspaceKey?:string|null}} [input]
|
|
618
|
+
* @param {{allowTestEnvironments?:boolean}} [options]
|
|
619
|
+
* @returns {{revision:number, environment:string, adapterId:string, provider:string|null,
|
|
620
|
+
* executionAdapterId:string, enabledProviders:string[], providerConfig:object|null,
|
|
621
|
+
* enabledEnvironments:string[], capabilities:string[], environmentConfig:object,
|
|
622
|
+
* targetId:string|null, source:{globalRevision:number, workspaceKey:string|null,
|
|
623
|
+
* sessionId:string|null, inheritedFrom:string|null, rejectedEnvironment:string|null,
|
|
624
|
+
* providerSource:string, inheritedProviderFrom:string|null, rejectedProvider:string|null,
|
|
625
|
+
* rejectedProviderReason:string|null, degraded:boolean, degradedReason:string|null}}}
|
|
626
|
+
*/
|
|
627
|
+
export function resolveSnapshot(config, { agent = null, workspaceKey = null } = {}, { allowTestEnvironments = false } = {}) {
|
|
628
|
+
let normalized;
|
|
629
|
+
let degraded = false;
|
|
630
|
+
let degradedReason = null;
|
|
631
|
+
try {
|
|
632
|
+
normalized = normalizeConfig(config);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
degraded = true;
|
|
635
|
+
degradedReason = error instanceof AdapterError ? error.message : String(error?.message ?? error);
|
|
636
|
+
normalized = normalizeConfig(DEFAULT_CONFIG);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const sessionId = agentSessionId(agent);
|
|
640
|
+
const key = typeof workspaceKey === 'string' && workspaceKey.length > 0 ? workspaceKey : null;
|
|
641
|
+
const workspace = key && Object.hasOwn(normalized.workspaces, key) ? normalized.workspaces[key] : null;
|
|
642
|
+
|
|
643
|
+
const enabledSource = workspace?.enabledEnvironments ?? normalized.global.enabledEnvironments;
|
|
644
|
+
const enabledEnvironments = enabledSource.filter((environment) => {
|
|
645
|
+
const meta = KNOWN_ENVIRONMENTS[environment];
|
|
646
|
+
if (!meta || meta.planned) return false;
|
|
647
|
+
if (meta.testOnly && !allowTestEnvironments) return false;
|
|
648
|
+
return true;
|
|
649
|
+
});
|
|
650
|
+
const enabledSet = new Set(enabledEnvironments);
|
|
651
|
+
|
|
652
|
+
const selectable = (environment) => {
|
|
653
|
+
const meta = KNOWN_ENVIRONMENTS[environment];
|
|
654
|
+
if (!meta || meta.planned) return false;
|
|
655
|
+
if (meta.testOnly && !allowTestEnvironments) return false;
|
|
656
|
+
return true;
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const applyEnabled = (environment) => {
|
|
660
|
+
if (selectable(environment) && enabledSet.has(environment)) {
|
|
661
|
+
return { environment, rejected: null };
|
|
662
|
+
}
|
|
663
|
+
let fallback;
|
|
664
|
+
if (enabledSet.has(normalized.global.environment)) fallback = normalized.global.environment;
|
|
665
|
+
else if (enabledEnvironments.length > 0) fallback = enabledEnvironments[0];
|
|
666
|
+
else fallback = ENVIRONMENTS.WEB;
|
|
667
|
+
return { environment: fallback, rejected: environment };
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const workspaceTarget = (environment) => {
|
|
671
|
+
const entryTarget = workspace?.environments?.[environment]?.targetId;
|
|
672
|
+
if (entryTarget !== undefined) return entryTarget;
|
|
673
|
+
if (workspace?.targetId !== undefined && workspace.environment === environment) {
|
|
674
|
+
return workspace.targetId;
|
|
675
|
+
}
|
|
676
|
+
return undefined;
|
|
677
|
+
};
|
|
678
|
+
const globalTarget = (environment) => normalized.global.environments[environment]?.targetId;
|
|
679
|
+
|
|
680
|
+
// `provider` is web-only: workspace/global entries for other environments have no such key.
|
|
681
|
+
const workspaceProvider = () => workspace?.environments?.[ENVIRONMENTS.WEB]?.provider;
|
|
682
|
+
const globalProvider = () => normalized.global.environments?.[ENVIRONMENTS.WEB]?.provider;
|
|
683
|
+
|
|
684
|
+
const selectForSession = (currentId, agentParent, depth, seen) => {
|
|
685
|
+
const own = Object.hasOwn(normalized.sessions, currentId) ? normalized.sessions[currentId] : null;
|
|
686
|
+
const ownParent = isPlainObject(own)
|
|
687
|
+
&& typeof own.parentSession === 'string'
|
|
688
|
+
&& own.parentSession.length > 0
|
|
689
|
+
? own.parentSession
|
|
690
|
+
: null;
|
|
691
|
+
const parentId = depth === 0 ? (agentParent ?? ownParent) : ownParent;
|
|
692
|
+
let parent = null;
|
|
693
|
+
if (parentId !== null && depth < 8 && !seen.has(parentId)) {
|
|
694
|
+
const nextSeen = new Set(seen);
|
|
695
|
+
nextSeen.add(parentId);
|
|
696
|
+
parent = selectForSession(parentId, null, depth + 1, nextSeen);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
let environment;
|
|
700
|
+
let environmentSource;
|
|
701
|
+
if (isPlainObject(own) && own.environment !== undefined) {
|
|
702
|
+
environment = own.environment;
|
|
703
|
+
environmentSource = currentId;
|
|
704
|
+
} else if (parent !== null) {
|
|
705
|
+
environment = parent.environment;
|
|
706
|
+
environmentSource = parent.environmentSource;
|
|
707
|
+
} else if (workspace?.environment !== undefined) {
|
|
708
|
+
environment = workspace.environment;
|
|
709
|
+
environmentSource = 'workspace';
|
|
710
|
+
} else {
|
|
711
|
+
environment = normalized.global.environment;
|
|
712
|
+
environmentSource = 'global';
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Independent provider chain: own webProvider > parent's effective provider > workspace >
|
|
716
|
+
// global > "chrome". Normalization already materializes chrome for a legacy config, so the last
|
|
717
|
+
// step is a defensive default rather than a normally observable source.
|
|
718
|
+
let provider;
|
|
719
|
+
let providerSource;
|
|
720
|
+
if (isPlainObject(own) && own.webProvider !== undefined && own.webProvider !== null) {
|
|
721
|
+
provider = own.webProvider;
|
|
722
|
+
providerSource = currentId;
|
|
723
|
+
} else if (parent !== null) {
|
|
724
|
+
provider = parent.provider;
|
|
725
|
+
providerSource = parent.providerSource;
|
|
726
|
+
} else if (workspaceProvider() !== undefined) {
|
|
727
|
+
provider = workspaceProvider();
|
|
728
|
+
providerSource = 'workspace';
|
|
729
|
+
} else if (globalProvider() !== undefined) {
|
|
730
|
+
provider = globalProvider();
|
|
731
|
+
providerSource = 'global';
|
|
732
|
+
} else {
|
|
733
|
+
provider = DEFAULT_WEB_PROVIDER;
|
|
734
|
+
providerSource = 'default';
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const effective = applyEnabled(environment);
|
|
738
|
+
const rejectedEnvironment = effective.rejected !== null
|
|
739
|
+
? effective.rejected
|
|
740
|
+
: (isPlainObject(own) && own.environment !== undefined ? null : (parent?.rejectedEnvironment ?? null));
|
|
741
|
+
|
|
742
|
+
let targetId;
|
|
743
|
+
const ownEnvironmentApplied = !(isPlainObject(own) && own.environment !== undefined)
|
|
744
|
+
|| own.environment === effective.environment;
|
|
745
|
+
if (isPlainObject(own) && own.targetId !== undefined && ownEnvironmentApplied) {
|
|
746
|
+
targetId = own.targetId;
|
|
747
|
+
} else if (parent !== null && parent.environment === effective.environment) {
|
|
748
|
+
targetId = parent.targetId;
|
|
749
|
+
} else {
|
|
750
|
+
const workspaceValue = workspaceTarget(effective.environment);
|
|
751
|
+
if (workspaceValue !== undefined) {
|
|
752
|
+
targetId = workspaceValue;
|
|
753
|
+
} else {
|
|
754
|
+
const globalValue = globalTarget(effective.environment);
|
|
755
|
+
targetId = globalValue !== undefined ? globalValue : null;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
environment: effective.environment,
|
|
761
|
+
environmentSource,
|
|
762
|
+
rejectedEnvironment,
|
|
763
|
+
provider,
|
|
764
|
+
providerSource,
|
|
765
|
+
targetId,
|
|
766
|
+
};
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
let selection;
|
|
770
|
+
if (sessionId !== null) {
|
|
771
|
+
const agentParent = typeof agent?.session?.header?.parentSession === 'string'
|
|
772
|
+
&& agent.session.header.parentSession.length > 0
|
|
773
|
+
? agent.session.header.parentSession
|
|
774
|
+
: null;
|
|
775
|
+
selection = selectForSession(sessionId, agentParent, 0, new Set([sessionId]));
|
|
776
|
+
} else {
|
|
777
|
+
const environment = workspace?.environment ?? normalized.global.environment;
|
|
778
|
+
const effective = applyEnabled(environment);
|
|
779
|
+
const workspaceValue = workspaceTarget(effective.environment);
|
|
780
|
+
const globalValue = globalTarget(effective.environment);
|
|
781
|
+
let provider;
|
|
782
|
+
let providerSource;
|
|
783
|
+
if (workspaceProvider() !== undefined) {
|
|
784
|
+
provider = workspaceProvider();
|
|
785
|
+
providerSource = 'workspace';
|
|
786
|
+
} else if (globalProvider() !== undefined) {
|
|
787
|
+
provider = globalProvider();
|
|
788
|
+
providerSource = 'global';
|
|
789
|
+
} else {
|
|
790
|
+
provider = DEFAULT_WEB_PROVIDER;
|
|
791
|
+
providerSource = 'default';
|
|
792
|
+
}
|
|
793
|
+
selection = {
|
|
794
|
+
environment: effective.environment,
|
|
795
|
+
environmentSource: workspace?.environment !== undefined ? 'workspace' : 'global',
|
|
796
|
+
rejectedEnvironment: effective.rejected,
|
|
797
|
+
provider,
|
|
798
|
+
providerSource,
|
|
799
|
+
targetId: workspaceValue !== undefined ? workspaceValue : (globalValue !== undefined ? globalValue : null),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const environment = selection.environment;
|
|
804
|
+
const globalEnvironmentConfig = normalized.global.environments[environment] ?? {};
|
|
805
|
+
const workspaceEnvironmentConfig = workspace?.environments?.[environment] ?? {};
|
|
806
|
+
const environmentConfig = { ...globalEnvironmentConfig, ...workspaceEnvironmentConfig };
|
|
807
|
+
if (globalEnvironmentConfig.connection !== undefined || workspaceEnvironmentConfig.connection !== undefined) {
|
|
808
|
+
environmentConfig.connection = {
|
|
809
|
+
...(globalEnvironmentConfig.connection ?? {}),
|
|
810
|
+
...(workspaceEnvironmentConfig.connection ?? {}),
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (globalEnvironmentConfig.providers !== undefined || workspaceEnvironmentConfig.providers !== undefined) {
|
|
814
|
+
environmentConfig.providers = mergeProviderMaps(
|
|
815
|
+
globalEnvironmentConfig.providers,
|
|
816
|
+
workspaceEnvironmentConfig.providers,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const inheritedFrom = sessionId !== null && selection.environmentSource === sessionId
|
|
821
|
+
? null
|
|
822
|
+
: selection.environmentSource;
|
|
823
|
+
const inheritedProviderFrom = sessionId !== null && selection.providerSource === sessionId
|
|
824
|
+
? null
|
|
825
|
+
: selection.providerSource;
|
|
826
|
+
|
|
827
|
+
// Web provider facts. `enabledProviders` is always derived from `providers.*.enabled` (no second
|
|
828
|
+
// stored list); `provider` / `executionAdapterId` / `providerConfig` only exist for web.
|
|
829
|
+
const enabledProviders = WEB_PROVIDER_IDS.filter(
|
|
830
|
+
(id) => environmentConfig.providers?.[id]?.enabled === true,
|
|
831
|
+
);
|
|
832
|
+
const isWeb = environment === ENVIRONMENTS.WEB;
|
|
833
|
+
const provider = isWeb ? selection.provider : null;
|
|
834
|
+
const providerEntry = isWeb ? (environmentConfig.providers?.[provider] ?? {}) : {};
|
|
835
|
+
// Precedence: total gate -> unknown provider name -> per-provider switch. Every non-null value
|
|
836
|
+
// blocks execution; an unknown name is kept so nothing is silently swapped for chrome.
|
|
837
|
+
const providerKnown = provider !== null && WEB_PROVIDER_IDS.includes(provider);
|
|
838
|
+
let rejectedProviderReason = null;
|
|
839
|
+
if (isWeb) {
|
|
840
|
+
if (environmentConfig.enabled !== true) rejectedProviderReason = 'environment-disabled';
|
|
841
|
+
else if (!providerKnown) rejectedProviderReason = 'unknown-provider';
|
|
842
|
+
else if (providerEntry.enabled !== true) rejectedProviderReason = 'provider-disabled';
|
|
843
|
+
}
|
|
844
|
+
// Legacy env-level `connection` is a chrome-scoped compatibility field (§11): it is reported for
|
|
845
|
+
// chrome only and never inherited by another provider.
|
|
846
|
+
const legacyConnection = environmentConfig.connection ?? null;
|
|
847
|
+
const providerConfig = isWeb
|
|
848
|
+
? {
|
|
849
|
+
provider,
|
|
850
|
+
// Canonical engineering support fact; `implemented` is its derived boolean, never a second truth.
|
|
851
|
+
support: webProviderSupport(provider),
|
|
852
|
+
enabled: providerEntry.enabled === true,
|
|
853
|
+
executable: providerEntry.executable ?? null,
|
|
854
|
+
connection: providerEntry.connection
|
|
855
|
+
?? (provider === WEB_PROVIDERS.CHROME ? legacyConnection : null),
|
|
856
|
+
implemented: isImplementedWebProvider(provider),
|
|
857
|
+
}
|
|
858
|
+
: null;
|
|
859
|
+
|
|
860
|
+
// A snapshot is a *step input*: it must be immutable, because admission, schema declaration,
|
|
861
|
+
// Skill discovery and execution all read the same object (A11). Freezing here — not at each
|
|
862
|
+
// call site — makes "one step, one version" enforceable instead of a convention.
|
|
863
|
+
return deepFreeze({
|
|
864
|
+
revision: normalized.revision,
|
|
865
|
+
environment,
|
|
866
|
+
adapterId: environment,
|
|
867
|
+
provider,
|
|
868
|
+
executionAdapterId: isWeb ? `${environment}-${provider}` : environment,
|
|
869
|
+
enabledProviders,
|
|
870
|
+
providerConfig,
|
|
871
|
+
enabledEnvironments: [...enabledEnvironments],
|
|
872
|
+
capabilities: capabilitiesForEnvironment(environment),
|
|
873
|
+
environmentConfig,
|
|
874
|
+
// Legacy `targetId` belongs to the chrome runtime: it must not leak into another provider.
|
|
875
|
+
targetId: isWeb && provider !== WEB_PROVIDERS.CHROME ? null : (selection.targetId ?? null),
|
|
876
|
+
source: {
|
|
877
|
+
globalRevision: normalized.revision,
|
|
878
|
+
workspaceKey: key,
|
|
879
|
+
sessionId,
|
|
880
|
+
inheritedFrom,
|
|
881
|
+
rejectedEnvironment: selection.rejectedEnvironment,
|
|
882
|
+
providerSource: selection.providerSource,
|
|
883
|
+
inheritedProviderFrom,
|
|
884
|
+
rejectedProvider: rejectedProviderReason === null ? null : provider,
|
|
885
|
+
rejectedProviderReason,
|
|
886
|
+
degraded,
|
|
887
|
+
degradedReason,
|
|
888
|
+
},
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
export { CONTRACT_VERSION as CONFIG_CONTRACT_VERSION };
|