@aiwg/cli 2026.10.0 → 2026.10.3
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/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3091 -24
- package/bin/aiwg.mjs +86 -21
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/channel/manager.mjs +59 -10
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/decision.js +297 -2
- package/dist/src/cli/handlers/diagnose.js +9 -2
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/init.js +245 -30
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +69 -6
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +16 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/runtime-info.js +24 -9
- package/dist/src/cli/handlers/script-runner.js +16 -6
- package/dist/src/cli/handlers/session.js +396 -62
- package/dist/src/cli/handlers/sessions.js +146 -40
- package/dist/src/cli/handlers/setup-manifest.js +439 -39
- package/dist/src/cli/handlers/setup.js +139 -39
- package/dist/src/cli/handlers/subcommands.js +3 -1
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/use.js +13 -0
- package/dist/src/cli/handlers/utilities.js +106 -23
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/config/aiwg-config.js +268 -4
- package/dist/src/config/cli.js +146 -19
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/user-config.js +36 -10
- package/dist/src/decision/adapters/jev.js +80 -28
- package/dist/src/decision/ask.js +352 -0
- package/dist/src/decision/driver.js +4 -0
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +3 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +121 -0
- package/dist/src/decision/ledger.js +839 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +3 -1
- package/dist/src/installation/manager-command.mjs +1 -1
- package/dist/src/installation/manager.mjs +17 -2
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/adapters/codex-runtime.js +360 -56
- package/dist/src/mcp/cli.mjs +587 -77
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.mjs +16 -26
- package/dist/src/mcp/helpers.mjs +106 -2
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +264 -57
- package/dist/src/mcp/registry.mjs +198 -45
- package/dist/src/mcp/server.mjs +403 -20
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +58 -0
- package/dist/src/mcp/tools/orchestration.mjs +38 -0
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +639 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +70 -13
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3091 -24
- package/dist/src/models/model-discovery.js +862 -63
- package/dist/src/models/provider-policy.js +625 -11
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +259 -0
- package/dist/src/models/worker-direct-readiness.js +529 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +131 -0
- package/dist/src/models/worker-installation.mjs +879 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.mjs +387 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/grok-build-headless.js +43 -21
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +9 -0
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +538 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +45 -0
- package/dist/src/sessions/adapters/generic.js +37 -3
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +423 -24
- package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
- package/dist/src/sessions/adapters/muse.js +68 -29
- package/dist/src/sessions/adapters/pi.js +1 -0
- package/dist/src/sessions/batch-contracts.js +13 -1
- package/dist/src/sessions/batch-import.js +23 -42
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +35 -0
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/importer.js +24 -8
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/repository.js +54 -13
- package/dist/src/sessions/workspace-discovery.js +99 -37
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +18 -1
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
- package/tools/_resolve-impl.mjs +39 -0
- package/tools/agents/deploy-agents.mjs +15 -2
- package/tools/agents/providers/base.mjs +63 -43
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +6 -3
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/providers/deepseek-harness-transport.mjs +358 -0
- package/tools/providers/omp-transport.mjs +192 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
|
@@ -3,14 +3,24 @@
|
|
|
3
3
|
* @implements #1802
|
|
4
4
|
* @implements #1805
|
|
5
5
|
*/
|
|
6
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
7
|
+
import addFormats from 'ajv-formats';
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
6
11
|
import { z } from 'zod';
|
|
7
12
|
import { createRequire } from 'node:module';
|
|
13
|
+
import { compileWorkerVersionPattern, workerVersionRangeMismatch } from './worker-installation.mjs';
|
|
14
|
+
import { normalizeProviderDefinitionId } from '../providers/provider-definitions.js';
|
|
15
|
+
import { qualificationDigest } from './qualification-digest.mjs';
|
|
8
16
|
const require = createRequire(import.meta.url);
|
|
9
17
|
function requireModelResource(filename) {
|
|
10
18
|
const candidates = [
|
|
11
19
|
`./${filename}`,
|
|
12
20
|
`../../agentic/code/providers/${filename}`,
|
|
13
21
|
`../../../agentic/code/providers/${filename}`,
|
|
22
|
+
`../../schemas/models/${filename}`,
|
|
23
|
+
`../../../schemas/models/${filename}`,
|
|
14
24
|
];
|
|
15
25
|
for (const candidate of candidates) {
|
|
16
26
|
try {
|
|
@@ -32,6 +42,27 @@ const ProviderSchema = z.enum([
|
|
|
32
42
|
const OutcomeSchema = z.enum([
|
|
33
43
|
'native', 'compiled', 'inherited', 'global-only', 'informational', 'unsupported',
|
|
34
44
|
]);
|
|
45
|
+
// Use the bundled owner for nested controls, including forward-compatible optional
|
|
46
|
+
// health records. Zod must validate and preserve them, never silently strip them.
|
|
47
|
+
const capabilitySchema = requireModelResource('provider-model-capabilities.v1.schema.json');
|
|
48
|
+
const capabilityAjv = addFormats(new Ajv2020({ strict: false, allErrors: true }));
|
|
49
|
+
capabilityAjv.addSchema(capabilitySchema);
|
|
50
|
+
const validateAdapter = capabilityAjv.compile({ $ref: `${capabilitySchema.$id}#/$defs/workerAdapter` });
|
|
51
|
+
export const WorkerAdapterSchema = z.custom().superRefine((value, context) => {
|
|
52
|
+
if (!validateAdapter(value)) {
|
|
53
|
+
context.addIssue({ code: 'custom', message: `Invalid worker adapter: ${capabilityAjv.errorsText(validateAdapter.errors)}` });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const invocation = value.invocation;
|
|
58
|
+
if (invocation.kind === 'process' && invocation.versionPattern !== undefined)
|
|
59
|
+
compileWorkerVersionPattern(invocation.versionPattern);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
context.addIssue({ code: 'custom', path: ['invocation', 'versionPattern'],
|
|
63
|
+
message: `Invalid worker adapter: ${error instanceof Error ? error.message : String(error)}` });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
35
66
|
const CapabilityEntrySchema = z.object({
|
|
36
67
|
agent: OutcomeSchema,
|
|
37
68
|
skill: OutcomeSchema,
|
|
@@ -45,12 +76,29 @@ const CapabilityEntrySchema = z.object({
|
|
|
45
76
|
verification: z.string().min(1),
|
|
46
77
|
sourceUrl: z.string().url(),
|
|
47
78
|
verifiedAt: z.string().date(),
|
|
48
|
-
|
|
79
|
+
workerAdapter: WorkerAdapterSchema.optional(),
|
|
80
|
+
// Descriptive health coverage is not an installed executable registration.
|
|
81
|
+
workerHealthAdapter: WorkerAdapterSchema.optional(),
|
|
82
|
+
workerHealthCoverage: z.object({
|
|
83
|
+
scope: z.literal('out'), coverage: z.literal('unobserved'), handshake: z.literal('unobserved'),
|
|
84
|
+
evidenceGrades: z.array(z.enum(['H', 'R', 'D'])).max(3),
|
|
85
|
+
qualificationOwner: z.string().regex(/^B-OG-[0-9]{2}$/),
|
|
86
|
+
provenance: z.array(z.string().min(1).max(4096)).min(1).max(16), reason: z.string().min(1).max(4096),
|
|
87
|
+
}).strict().optional(),
|
|
88
|
+
}).strict();
|
|
49
89
|
const CapabilityRegistrySchema = z.object({
|
|
90
|
+
$schema: z.enum([
|
|
91
|
+
'../../../schemas/models/provider-model-capabilities.v1.schema.json',
|
|
92
|
+
'https://aiwg.io/schemas/models/provider-model-capabilities.v1.schema.json',
|
|
93
|
+
]).optional(),
|
|
50
94
|
version: z.literal('1.0.0'),
|
|
51
95
|
verifiedAt: z.string().date(),
|
|
52
96
|
providers: z.record(ProviderSchema, CapabilityEntrySchema),
|
|
53
|
-
})
|
|
97
|
+
workerAdapters: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), WorkerAdapterSchema)
|
|
98
|
+
.refine(value => Object.keys(value).length <= 128, 'At most 128 worker adapters').optional(),
|
|
99
|
+
workerHealthAdapters: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), WorkerAdapterSchema)
|
|
100
|
+
.refine(value => Object.keys(value).length <= 128, 'At most 128 worker health descriptors').optional(),
|
|
101
|
+
}).strict();
|
|
54
102
|
const CatalogModelSchema = z.object({
|
|
55
103
|
id: z.string().min(1),
|
|
56
104
|
status: z.enum(['active', 'deprecated', 'unverified']),
|
|
@@ -66,22 +114,228 @@ const CatalogProviderSchema = z.object({
|
|
|
66
114
|
sourceUrl: z.string().url(),
|
|
67
115
|
verifiedAt: z.string().date(),
|
|
68
116
|
});
|
|
117
|
+
let catalogValidators;
|
|
118
|
+
function getCatalogValidators() {
|
|
119
|
+
if (!catalogValidators) {
|
|
120
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
121
|
+
const file = [resolve(here, '../../schemas/models/model-catalog.v1.schema.json'), resolve(here, '../../../schemas/models/model-catalog.v1.schema.json')].find(existsSync);
|
|
122
|
+
if (!file)
|
|
123
|
+
throw new Error('Model catalog schema is unavailable');
|
|
124
|
+
const schema = JSON.parse(readFileSync(file, 'utf8'));
|
|
125
|
+
const ajv = addFormats(new Ajv2020({ strict: false, allErrors: true }));
|
|
126
|
+
ajv.addSchema(schema);
|
|
127
|
+
catalogValidators = { catalog: ajv.getSchema(schema.$id), observation: ajv.compile({ $ref: `${schema.$id}#/$defs/workerObservation` }), discovery: ajv.compile({ $ref: `${schema.$id}#/properties/discovery` }) };
|
|
128
|
+
}
|
|
129
|
+
return catalogValidators;
|
|
130
|
+
}
|
|
131
|
+
export function validateWorkerModelObservation(input) {
|
|
132
|
+
const validate = getCatalogValidators().observation;
|
|
133
|
+
return validate(input) ? [] : (validate.errors ?? []).map(error => `${error.instancePath}: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
const WorkerObservationSchema = z.custom(input => validateWorkerModelObservation(input).length === 0);
|
|
136
|
+
const DiscoveredWorkerModelSchema = z.object({
|
|
137
|
+
id: z.string().min(1), apiModelId: z.string().optional(), llmProvider: z.string().optional(),
|
|
138
|
+
hidden: z.boolean().optional(), reasoningEfforts: z.array(z.string()).optional(), worker: WorkerObservationSchema.optional(),
|
|
139
|
+
}).passthrough().superRefine((model, context) => {
|
|
140
|
+
if (!model.worker)
|
|
141
|
+
return;
|
|
142
|
+
if (model.hidden !== undefined && model.worker.visibility !== (model.hidden ? 'hidden' : 'listed'))
|
|
143
|
+
context.addIssue({ code: 'custom', path: ['worker', 'visibility'], message: 'Conflicting visibility evidence' });
|
|
144
|
+
const efforts = model.reasoningEfforts ?? [];
|
|
145
|
+
const observed = model.worker.efforts.map(e => e.id);
|
|
146
|
+
if (efforts.length !== observed.length || new Set(observed).size !== observed.length || efforts.some(e => !observed.includes(e)))
|
|
147
|
+
context.addIssue({ code: 'custom', path: ['worker', 'efforts'], message: 'Conflicting effort evidence' });
|
|
148
|
+
});
|
|
149
|
+
const ProviderDiscoverySchema = z.object({
|
|
150
|
+
provider: z.string(), source: z.enum(['native', 'cache', 'remote', 'static']), observedAt: z.string().datetime({ offset: true }),
|
|
151
|
+
runtimeVersion: z.string().optional(), accountScope: z.enum(['local-account', 'local-runtime', 'public-feed', 'static']),
|
|
152
|
+
accountBinding: z.string().min(1).optional(), models: z.array(DiscoveredWorkerModelSchema),
|
|
153
|
+
}).passthrough();
|
|
154
|
+
const DiscoverySchema = z.object({
|
|
155
|
+
source: z.enum(['native', 'cache', 'remote', 'static']), upstreamSource: z.enum(['native', 'remote', 'static']).optional(),
|
|
156
|
+
fetchedAt: z.string().datetime({ offset: true }), remoteUrl: z.string().url().optional(), remoteError: z.string().optional(),
|
|
157
|
+
inventorySignature: z.string().optional(), providers: z.record(z.string(), ProviderDiscoverySchema),
|
|
158
|
+
instances: z.record(z.string(), ProviderDiscoverySchema).optional(),
|
|
159
|
+
}).strict();
|
|
160
|
+
/** Optional enrichment is checked without tightening legacy catalog rows. */
|
|
161
|
+
export function validateWorkerCatalogDiscovery(input) {
|
|
162
|
+
if (input === undefined)
|
|
163
|
+
return [];
|
|
164
|
+
const validate = getCatalogValidators().discovery;
|
|
165
|
+
if (!validate(input))
|
|
166
|
+
return (validate.errors ?? []).map(error => `${error.instancePath}: ${error.message}`);
|
|
167
|
+
const parsed = DiscoverySchema.safeParse(input);
|
|
168
|
+
return parsed.success ? [] : parsed.error.issues.map(issue => `${issue.path.join('/')}: ${issue.message}`);
|
|
169
|
+
}
|
|
69
170
|
const CatalogSchema = z.object({
|
|
70
171
|
version: z.literal('1.0.0'),
|
|
172
|
+
$schema: z.literal('../../../schemas/models/model-catalog.v1.schema.json').optional(),
|
|
173
|
+
discovery: DiscoverySchema.optional(),
|
|
71
174
|
refreshedAt: z.string().date(),
|
|
72
175
|
staleAfterDays: z.number().int().positive(),
|
|
73
176
|
providers: z.record(ProviderSchema, CatalogProviderSchema),
|
|
74
|
-
});
|
|
177
|
+
}).strict();
|
|
75
178
|
const PolicySchema = z.object({
|
|
76
179
|
role: z.enum(['reasoning', 'coding', 'efficiency']),
|
|
77
180
|
tier: z.enum(['economy', 'standard', 'premium']),
|
|
78
181
|
effort: z.enum(['minimal', 'low', 'medium', 'high', 'xhigh']).optional(),
|
|
79
182
|
override: z.string().min(1).optional(),
|
|
80
183
|
}).strict();
|
|
184
|
+
const uniqueStrings = z.array(z.string().min(1)).refine(values => new Set(values).size === values.length, 'Duplicate entries');
|
|
185
|
+
const WorkerDataPolicySchema = z.object({
|
|
186
|
+
allowedProcessing: z.array(z.enum(['local', 'remote'])).refine(values => new Set(values).size === values.length).optional(),
|
|
187
|
+
allowProductImprovement: z.boolean().optional(),
|
|
188
|
+
allowUnknown: z.boolean().optional(),
|
|
189
|
+
maxRetentionDays: z.number().int().nonnegative().optional(),
|
|
190
|
+
}).strict();
|
|
191
|
+
const WorkerRoleSchema = z.object({
|
|
192
|
+
groups: uniqueStrings.refine(values => values.length > 0),
|
|
193
|
+
tier: z.enum(['economy', 'standard', 'premium', 'max-quality']),
|
|
194
|
+
initialEffort: z.string().min(1).nullable(),
|
|
195
|
+
effortLadder: uniqueStrings,
|
|
196
|
+
escalateAfter: z.number().int().positive().optional(),
|
|
197
|
+
maxEscalations: z.number().int().nonnegative().optional(),
|
|
198
|
+
deescalateAfter: z.number().int().positive().optional(),
|
|
199
|
+
allowNotApplicableEffort: z.boolean().optional(),
|
|
200
|
+
}).strict();
|
|
201
|
+
export const WorkerSelectionPolicySchema = z.object({
|
|
202
|
+
roleGuidance: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), WorkerRoleSchema).optional(),
|
|
203
|
+
guards: z.object({
|
|
204
|
+
deniedEfforts: uniqueStrings.optional(),
|
|
205
|
+
deniedModels: uniqueStrings.optional(),
|
|
206
|
+
denySelfDelegation: z.boolean().optional(),
|
|
207
|
+
requireExactModel: z.boolean().optional(),
|
|
208
|
+
allowedVisibility: z.array(z.enum(['listed', 'hidden'])).refine(v => new Set(v).size === v.length).optional(),
|
|
209
|
+
allowedLifecycle: z.array(z.enum(['active', 'deprecated'])).refine(v => new Set(v).size === v.length).optional(),
|
|
210
|
+
minimumIdentityEvidence: z.enum(['runtime-configured', 'backend-attested']).optional(),
|
|
211
|
+
}).strict().optional(),
|
|
212
|
+
dataPolicy: WorkerDataPolicySchema.optional(),
|
|
213
|
+
dataClasses: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), WorkerDataPolicySchema).optional(),
|
|
214
|
+
freshness: z.object({
|
|
215
|
+
warnHours: z.number().int().positive().optional(),
|
|
216
|
+
blockHours: z.number().int().positive().optional(),
|
|
217
|
+
policyDays: z.number().int().positive().optional(),
|
|
218
|
+
}).strict().optional(),
|
|
219
|
+
taskClasses: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), z.string().min(1)).optional(),
|
|
220
|
+
}).strict().superRefine((policy, context) => {
|
|
221
|
+
for (const [role, guidance] of Object.entries(policy.roleGuidance ?? {})) {
|
|
222
|
+
if (!['efficiency', 'coding', 'reasoning'].includes(role))
|
|
223
|
+
context.addIssue({ code: 'custom', path: ['roleGuidance', role], message: 'Unknown worker role' });
|
|
224
|
+
if (guidance.effortLadder.includes('none') || (guidance.initialEffort === 'none' ? guidance.effortLadder.length !== 0 : guidance.initialEffort !== null && !guidance.effortLadder.includes(guidance.initialEffort))) {
|
|
225
|
+
context.addIssue({ code: 'custom', path: ['roleGuidance', role], message: 'Initial effort must belong to the ladder; real none requires an empty ladder' });
|
|
226
|
+
}
|
|
227
|
+
if (guidance.initialEffort === null && (guidance.allowNotApplicableEffort !== true || guidance.effortLadder.length !== 0)) {
|
|
228
|
+
context.addIssue({ code: 'custom', path: ['roleGuidance', role], message: 'Null effort requires an empty ladder and allowNotApplicableEffort=true' });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (const key of policy.guards?.deniedModels ?? []) {
|
|
232
|
+
if (!isCanonicalWorkerModelKey(key))
|
|
233
|
+
context.addIssue({ code: 'custom', path: ['guards', 'deniedModels'], message: 'Noncanonical inference model key' });
|
|
234
|
+
}
|
|
235
|
+
if (policy.freshness?.warnHours !== undefined && policy.freshness.blockHours !== undefined && policy.freshness.warnHours > policy.freshness.blockHours) {
|
|
236
|
+
context.addIssue({ code: 'custom', path: ['freshness'], message: 'warnHours must not exceed blockHours' });
|
|
237
|
+
}
|
|
238
|
+
for (const role of Object.values(policy.taskClasses ?? {})) {
|
|
239
|
+
if (!['coding', 'reasoning', 'efficiency'].includes(role))
|
|
240
|
+
context.addIssue({ code: 'custom', path: ['taskClasses'], message: 'Unknown worker role' });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
/** Canonical RFC3986 identity; installation and account never enter the key. */
|
|
244
|
+
export function workerModelKey(inferenceProvider, apiModelId) {
|
|
245
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(inferenceProvider) || !apiModelId)
|
|
246
|
+
throw new Error('OG_CONFIG_REFERENCE: invalid inference model identity');
|
|
247
|
+
return `${inferenceProvider}/${encodeURIComponent(apiModelId).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`;
|
|
248
|
+
}
|
|
249
|
+
export function isCanonicalWorkerModelKey(key) {
|
|
250
|
+
const slash = key.indexOf('/');
|
|
251
|
+
try {
|
|
252
|
+
return slash > 0 && workerModelKey(key.slice(0, slash), decodeURIComponent(key.slice(slash + 1))) === key;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export const DEFAULT_WORKER_SELECTION = {
|
|
259
|
+
guards: { deniedEfforts: ['ultra'], deniedModels: [], denySelfDelegation: true, requireExactModel: true, allowedVisibility: ['listed'], allowedLifecycle: ['active'], minimumIdentityEvidence: 'runtime-configured' },
|
|
260
|
+
dataPolicy: { allowedProcessing: ['local', 'remote'], allowProductImprovement: false, allowUnknown: false },
|
|
261
|
+
freshness: { warnHours: 24, blockHours: 168, policyDays: 30 },
|
|
262
|
+
taskClasses: { inventory: 'efficiency', implementation: 'coding', architecture: 'reasoning', review: 'reasoning' },
|
|
263
|
+
};
|
|
264
|
+
/** Merge leaves, preserving pre-merge explicit origins and attempted relaxations. */
|
|
265
|
+
export function mergeWorkerSelection(layers) {
|
|
266
|
+
const policy = {};
|
|
267
|
+
const origins = {};
|
|
268
|
+
const diagnostics = [];
|
|
269
|
+
const visit = (target, input, parts, layer) => {
|
|
270
|
+
for (const [key, value] of Object.entries(input)) {
|
|
271
|
+
const path = [...parts, key];
|
|
272
|
+
const pointer = `/defaults/workerSelection/${path.map(p => p.replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}`;
|
|
273
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
274
|
+
target[key] ??= {};
|
|
275
|
+
visit(target[key], value, path, layer);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const origin = { source: layer.source, authority: layer.authority, digest: layer.digest, pointer, contribution: structuredClone(value) };
|
|
279
|
+
const prior = origins[pointer] ?? [];
|
|
280
|
+
const explicit = prior.filter(o => o.authority !== 'builtin');
|
|
281
|
+
let effective = structuredClone(value);
|
|
282
|
+
let relaxation = false;
|
|
283
|
+
if (layer.authority !== 'builtin' && explicit.length > 0) {
|
|
284
|
+
const previous = target[key];
|
|
285
|
+
if (['deniedEfforts', 'deniedModels'].includes(key))
|
|
286
|
+
effective = [...new Set([...previous, ...value])];
|
|
287
|
+
else if (['allowedVisibility', 'allowedLifecycle', 'allowedProcessing'].includes(key)) {
|
|
288
|
+
relaxation = value.some((v) => !previous.includes(v));
|
|
289
|
+
effective = previous.filter((v) => value.includes(v));
|
|
290
|
+
}
|
|
291
|
+
else if (['denySelfDelegation', 'requireExactModel'].includes(key)) {
|
|
292
|
+
relaxation = previous === true && value === false;
|
|
293
|
+
effective = previous || value;
|
|
294
|
+
}
|
|
295
|
+
else if (['allowUnknown', 'allowProductImprovement'].includes(key)) {
|
|
296
|
+
relaxation = previous === false && value === true;
|
|
297
|
+
effective = previous && value;
|
|
298
|
+
}
|
|
299
|
+
else if (['maxRetentionDays', 'warnHours', 'blockHours', 'policyDays'].includes(key)) {
|
|
300
|
+
relaxation = value > previous;
|
|
301
|
+
effective = Math.min(previous, value);
|
|
302
|
+
}
|
|
303
|
+
else if (key === 'minimumIdentityEvidence') {
|
|
304
|
+
relaxation = previous === 'backend-attested' && value !== previous;
|
|
305
|
+
effective = previous === 'backend-attested' ? previous : value;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (relaxation)
|
|
309
|
+
diagnostics.push({ code: 'OG_POLICY_RELAXATION', pointer, message: 'Explicit restriction cannot relax an inherited restriction', origins: [...explicit, origin] });
|
|
310
|
+
target[key] = effective;
|
|
311
|
+
origins[pointer] = [...prior, origin];
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
for (const layer of layers)
|
|
315
|
+
if (layer.policy)
|
|
316
|
+
visit(policy, layer.policy, [], layer);
|
|
317
|
+
const validation = WorkerSelectionPolicySchema.safeParse(policy);
|
|
318
|
+
if (!validation.success)
|
|
319
|
+
for (const issue of validation.error.issues)
|
|
320
|
+
diagnostics.push({ code: 'OG_CONFIG_REFERENCE', pointer: `/defaults/workerSelection/${issue.path.join('/')}`, message: issue.message });
|
|
321
|
+
// Data classes are applied as further restrictions to the base path.
|
|
322
|
+
for (const dataClass of Object.values(policy.dataClasses ?? {})) {
|
|
323
|
+
const base = policy.dataPolicy ?? {};
|
|
324
|
+
if (dataClass.allowedProcessing)
|
|
325
|
+
dataClass.allowedProcessing = dataClass.allowedProcessing.filter(p => !base.allowedProcessing || base.allowedProcessing.includes(p));
|
|
326
|
+
for (const key of ['allowUnknown', 'allowProductImprovement'])
|
|
327
|
+
if (base[key] === false)
|
|
328
|
+
dataClass[key] = false;
|
|
329
|
+
if (base.maxRetentionDays !== undefined)
|
|
330
|
+
dataClass.maxRetentionDays = Math.min(dataClass.maxRetentionDays ?? base.maxRetentionDays, base.maxRetentionDays);
|
|
331
|
+
}
|
|
332
|
+
return { policy, origins, diagnostics };
|
|
333
|
+
}
|
|
81
334
|
const UserProjectModelConfigSchema = z.object({
|
|
82
335
|
defaults: z.object({
|
|
83
336
|
tier: z.enum(['economy', 'standard', 'premium', 'max-quality']).optional(),
|
|
84
337
|
provider: ProviderSchema.optional(),
|
|
338
|
+
workerSelection: WorkerSelectionPolicySchema.optional(),
|
|
85
339
|
}).strict().optional(),
|
|
86
340
|
agentOverrides: z.record(z.string(), z.object({
|
|
87
341
|
'model-tier': z.enum(['economy', 'standard', 'premium', 'max-quality']).optional(),
|
|
@@ -103,18 +357,239 @@ let registryCache = null;
|
|
|
103
357
|
let catalogCache = null;
|
|
104
358
|
export function loadProviderModelCapabilities() {
|
|
105
359
|
if (!registryCache) {
|
|
106
|
-
registryCache =
|
|
107
|
-
const expected = new Set(ProviderSchema.options);
|
|
108
|
-
const actual = new Set(Object.keys(registryCache.providers));
|
|
109
|
-
if (actual.size !== expected.size || [...expected].some(id => !actual.has(id))) {
|
|
110
|
-
throw new Error('Provider model capability registry must cover all registered providers');
|
|
111
|
-
}
|
|
360
|
+
registryCache = parseProviderModelCapabilities(capabilityData);
|
|
112
361
|
}
|
|
113
362
|
return registryCache;
|
|
114
363
|
}
|
|
364
|
+
export function parseProviderModelCapabilities(input) {
|
|
365
|
+
const registry = CapabilityRegistrySchema.parse(input);
|
|
366
|
+
const expected = new Set(ProviderSchema.options);
|
|
367
|
+
const actual = new Set(Object.keys(registry.providers));
|
|
368
|
+
if (actual.size !== expected.size || [...expected].some(id => !actual.has(id))) {
|
|
369
|
+
throw new Error('Provider model capability registry must cover all registered providers');
|
|
370
|
+
}
|
|
371
|
+
const ids = new Set();
|
|
372
|
+
for (const adapter of [...Object.values(registry.providers).flatMap(p => p.workerAdapter ? [p.workerAdapter] : []),
|
|
373
|
+
...Object.values(registry.workerAdapters ?? {})]) {
|
|
374
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(adapter.id) || ids.has(adapter.id))
|
|
375
|
+
throw new Error(`Duplicate or invalid worker adapter ID: ${adapter.id}`);
|
|
376
|
+
ids.add(adapter.id);
|
|
377
|
+
}
|
|
378
|
+
return registry;
|
|
379
|
+
}
|
|
380
|
+
/** Exact, case-sensitive references; deployment and inference provider identities differ. */
|
|
381
|
+
export function resolveWorkerAdapter(registry, reference) {
|
|
382
|
+
const match = /^(providers|workerAdapters):([a-z][a-z0-9-]{0,62})$/.exec(reference);
|
|
383
|
+
if (!match)
|
|
384
|
+
throw new Error(`Invalid worker adapter reference: ${reference}`);
|
|
385
|
+
const [, namespace, id] = match;
|
|
386
|
+
const adapter = namespace === 'providers'
|
|
387
|
+
? registry.providers[id]?.workerAdapter
|
|
388
|
+
: registry.workerAdapters?.[id];
|
|
389
|
+
if (!adapter)
|
|
390
|
+
throw new Error(`Unknown worker adapter reference: ${reference}`);
|
|
391
|
+
return WorkerAdapterSchema.parse(adapter);
|
|
392
|
+
}
|
|
393
|
+
/** Pure direct/shared mapping: no admission store, Mission or activation marker. */
|
|
394
|
+
export function resolveWorkerInstallation(registry, installation) {
|
|
395
|
+
const adapter = resolveWorkerAdapter(registry, installation.adapter);
|
|
396
|
+
if (adapter.invocation.kind === 'process') {
|
|
397
|
+
if (installation.endpoint !== undefined)
|
|
398
|
+
throw new Error('Process adapter forbids endpoint');
|
|
399
|
+
const executable = installation.executable ?? adapter.invocation.executable;
|
|
400
|
+
if (!executable.trim() || executable.includes('\0'))
|
|
401
|
+
throw new Error('Process adapter requires executable');
|
|
402
|
+
return { adapter, executable };
|
|
403
|
+
}
|
|
404
|
+
if (installation.executable !== undefined)
|
|
405
|
+
throw new Error('HTTP adapter forbids executable');
|
|
406
|
+
if (!installation.endpoint)
|
|
407
|
+
throw new Error('HTTP adapter requires endpoint');
|
|
408
|
+
const endpoint = new URL(installation.endpoint);
|
|
409
|
+
if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.hash) {
|
|
410
|
+
throw new Error('Invalid HTTP adapter endpoint');
|
|
411
|
+
}
|
|
412
|
+
return { adapter, endpoint: installation.endpoint };
|
|
413
|
+
}
|
|
414
|
+
/** Typed policy failures let public surfaces retain the cause without exposing internal details. */
|
|
415
|
+
export class WorkerPolicyError extends Error {
|
|
416
|
+
code;
|
|
417
|
+
constructor(code, message) {
|
|
418
|
+
super(message);
|
|
419
|
+
this.code = code;
|
|
420
|
+
this.name = 'WorkerPolicyError';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Dispatch-time safety checks belong here, alongside transport qualification.
|
|
425
|
+
* Like route validation, rendering requires observation evidence; a default
|
|
426
|
+
* observation may supply it when no model observation is available.
|
|
427
|
+
*/
|
|
428
|
+
export function assertWorkerSelectionAllowed(selected, context) {
|
|
429
|
+
const policy = context.policy ?? DEFAULT_WORKER_SELECTION;
|
|
430
|
+
if (isWorkerEffortDenied(selected.effort.value, policy)) {
|
|
431
|
+
throw new WorkerPolicyError('OG_EFFORT_DENIED', `Worker invocation denied effort: ${selected.effort.value}`);
|
|
432
|
+
}
|
|
433
|
+
if (isContributorWorkerModel(selected.model)
|
|
434
|
+
|| isWorkerModelDenied(workerModelKey(selected.inferenceProvider, selected.model), policy)) {
|
|
435
|
+
throw new WorkerPolicyError('OG_MODEL_UNAPPROVED', `Worker invocation denied model: ${selected.model}`);
|
|
436
|
+
}
|
|
437
|
+
const { data, unknownDataClass } = workerRouteDataPolicy(policy, context.dataClass);
|
|
438
|
+
if (unknownDataClass)
|
|
439
|
+
throw new WorkerPolicyError('OG_DATA_POLICY', 'Worker invocation has denied data-use: unknown data class');
|
|
440
|
+
const observation = context.modelObservation ?? context.defaultObservation;
|
|
441
|
+
if (!observation)
|
|
442
|
+
throw new WorkerPolicyError('OG_MODEL_UNAPPROVED', 'Worker invocation requires model or default observation evidence');
|
|
443
|
+
const guidance = context.role ? policy.roleGuidance?.[context.role] : undefined;
|
|
444
|
+
if (isWorkerNullEffortDenied(selected.effort.value, observation, guidance)) {
|
|
445
|
+
throw new WorkerPolicyError('OG_EFFORT_DENIED', 'Worker invocation denied effort: null effort requires qualified absence of an effort control');
|
|
446
|
+
}
|
|
447
|
+
for (const [kind, observation] of [
|
|
448
|
+
['model', context.modelObservation], ['default', context.defaultObservation],
|
|
449
|
+
]) {
|
|
450
|
+
if (!observation)
|
|
451
|
+
continue;
|
|
452
|
+
if (observation.lifecycle === 'retired'
|
|
453
|
+
|| (observation.retiredAt != null && Date.parse(observation.retiredAt) <= context.now)
|
|
454
|
+
|| (observation.sunsetAt != null && Date.parse(observation.sunsetAt) <= context.now)) {
|
|
455
|
+
throw new WorkerPolicyError('OG_MODEL_UNAPPROVED', `Worker invocation ${kind} observation is retired`);
|
|
456
|
+
}
|
|
457
|
+
const blockMs = (policy.freshness?.blockHours ?? DEFAULT_WORKER_SELECTION.freshness.blockHours) * 3_600_000;
|
|
458
|
+
const observedAt = Date.parse(observation.observedAt);
|
|
459
|
+
if (!Number.isFinite(context.now) || !Number.isFinite(observedAt)
|
|
460
|
+
|| !Number.isFinite(blockMs) || blockMs <= 0
|
|
461
|
+
|| (observation.verifiedAt !== undefined && !Number.isFinite(Date.parse(observation.verifiedAt)))
|
|
462
|
+
|| isWorkerEvidenceStale(observation, policy, context.now)) {
|
|
463
|
+
throw new WorkerPolicyError('OG_MODEL_UNAPPROVED', `Worker invocation ${kind} observation is stale or invalid`);
|
|
464
|
+
}
|
|
465
|
+
if (hasSelectionRelevantConflicts(observation)) {
|
|
466
|
+
throw new WorkerPolicyError('OG_MODEL_UNAPPROVED', `Worker invocation ${kind} observation has conflicting selection-relevant evidence`);
|
|
467
|
+
}
|
|
468
|
+
if (observation.dataUse && isWorkerDataUseDenied({
|
|
469
|
+
processing: observation.dataUse.processing ?? 'unknown',
|
|
470
|
+
productImprovement: observation.dataUse.productImprovement ?? null,
|
|
471
|
+
retentionDays: observation.dataUse.retentionDays ?? null,
|
|
472
|
+
}, data)) {
|
|
473
|
+
throw new WorkerPolicyError('OG_DATA_POLICY', `Worker invocation ${kind} observation has denied data-use`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const WORKER_CONTROLS = ['model', 'effort', 'sandbox', 'approval', 'workdir',
|
|
478
|
+
'finalOutput', 'stdin', 'structuredOutput', 'stepCap', 'trust', 'children', 'fallback'];
|
|
479
|
+
/** Caller controls only add restrictions. Absent resolved authority, require all controls. */
|
|
480
|
+
export function requiredWorkerControls(context) {
|
|
481
|
+
if (context.requiredControls !== undefined && (!Array.isArray(context.requiredControls)
|
|
482
|
+
|| context.requiredControls.some(name => typeof name !== 'string' || !name))) {
|
|
483
|
+
throw new Error('Worker required controls must be an array of control names');
|
|
484
|
+
}
|
|
485
|
+
const controls = new Set(WORKER_CONTROLS);
|
|
486
|
+
if (context.policy && context.permissions) {
|
|
487
|
+
if (context.effort === null)
|
|
488
|
+
controls.delete('effort');
|
|
489
|
+
// Step limits are observed unless explicitly required by the trusted route.
|
|
490
|
+
controls.delete('stepCap');
|
|
491
|
+
if (context.policy.guards?.denySelfDelegation === false)
|
|
492
|
+
controls.delete('children');
|
|
493
|
+
}
|
|
494
|
+
for (const name of context.requiredControls ?? [])
|
|
495
|
+
controls.add(name);
|
|
496
|
+
return [...controls];
|
|
497
|
+
}
|
|
498
|
+
/** The same sandbox requirement governs inspection, resolution and rendering. */
|
|
499
|
+
export function requiresWorkerExternalBoundary(adapter, selection) {
|
|
500
|
+
const providers = [adapter.id, adapter.provider].map(normalizeProviderDefinitionId);
|
|
501
|
+
return requiredWorkerControls(selection).includes('sandbox')
|
|
502
|
+
&& (providers.some(provider => provider && ['claude', 'muse', 'hermes'].includes(provider))
|
|
503
|
+
|| adapter.invocationProfiles?.[selection.profile]?.controls.sandbox?.support === 'external'
|
|
504
|
+
|| selection.permissions?.sandbox === 'external');
|
|
505
|
+
}
|
|
506
|
+
/** Inspect declared controls before installation evidence can be produced. This
|
|
507
|
+
* does not authorize invocation; consumers must use qualifyWorkerInvocationProfile. */
|
|
508
|
+
export function inspectWorkerInvocationProfile(adapter, selection) {
|
|
509
|
+
WorkerAdapterSchema.parse(adapter);
|
|
510
|
+
const profile = adapter.invocationProfiles?.[selection.profile];
|
|
511
|
+
if (adapter.qualification.status !== 'qualified' || !profile || profile.transport !== selection.transport) {
|
|
512
|
+
throw new Error('Worker transport/profile is unqualified');
|
|
513
|
+
}
|
|
514
|
+
// Shared range predicate (worker-installation.mjs): read-only surfaces
|
|
515
|
+
// (worker-direct-readiness.ts) reuse the same helper instead of
|
|
516
|
+
// re-deriving this inspector's range logic.
|
|
517
|
+
if (workerVersionRangeMismatch(selection.cliVersion, [adapter.supportedVersions, profile.supportedVersions]) !== null) {
|
|
518
|
+
throw new Error('Worker CLI version is outside qualified profile');
|
|
519
|
+
}
|
|
520
|
+
const requiredControls = requiredWorkerControls(selection);
|
|
521
|
+
if (requiresWorkerExternalBoundary(adapter, selection)
|
|
522
|
+
&& (!selection.boundary || !['os', 'executor'].includes(selection.boundary.kind)
|
|
523
|
+
|| !selection.boundary.identity || !/^sha256:[a-f0-9]{64}$/.test(selection.boundary.digest)
|
|
524
|
+
|| !selection.boundary.controls?.includes('sandbox'))) {
|
|
525
|
+
throw new Error('Required worker control sandbox needs a qualified external enforcing boundary');
|
|
526
|
+
}
|
|
527
|
+
for (const name of requiredControls) {
|
|
528
|
+
if ((name === 'children' && !adapter.capabilities.nestedAgentsControllable)
|
|
529
|
+
|| (name === 'fallback' && !adapter.capabilities.nativeFallbackControllable)) {
|
|
530
|
+
throw new Error(`Required worker control is unqualified: ${name}`);
|
|
531
|
+
}
|
|
532
|
+
const control = profile.controls[name];
|
|
533
|
+
if (!control || !['native', 'external'].includes(control.support) || !control.mapping || !control.evidence) {
|
|
534
|
+
throw new Error(`Required worker control is unqualified: ${name}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (selection.transport === 'mcp' && (!selection.mcp || !profile.mcp
|
|
538
|
+
|| Object.keys(profile.mcp).some(key => profile.mcp[key]
|
|
539
|
+
!== selection.mcp[key]))) {
|
|
540
|
+
throw new Error('MCP server/tool/schema/session qualification changed');
|
|
541
|
+
}
|
|
542
|
+
return profile;
|
|
543
|
+
}
|
|
544
|
+
/** Qualification is transport/version scoped; declaration or boundary claims
|
|
545
|
+
* alone cannot qualify an external sandbox. The invocation owner rechecks the
|
|
546
|
+
* installation scope, including this boundary, against the recorded digest. */
|
|
547
|
+
export function qualifyWorkerInvocationProfile(adapter, selection) {
|
|
548
|
+
const profile = inspectWorkerInvocationProfile(adapter, selection);
|
|
549
|
+
if (requiresWorkerExternalBoundary(adapter, selection) && !profile.installationQualification) {
|
|
550
|
+
throw new Error('Required worker control sandbox needs matching installation qualification for its external boundary');
|
|
551
|
+
}
|
|
552
|
+
return profile;
|
|
553
|
+
}
|
|
554
|
+
/** Health timing evidence belongs to the exact execution profile and tuple.
|
|
555
|
+
* Fixture qualification does not become authenticated/live qualification. */
|
|
556
|
+
export function workerHealthScopeDigest(profile, scope) {
|
|
557
|
+
const { installationQualification: _installation, health, ...executionProfile } = profile;
|
|
558
|
+
const { qualification: _qualification, ...signals } = health ?? {};
|
|
559
|
+
return qualificationDigest({ profile: executionProfile, health: signals,
|
|
560
|
+
profileRef: scope.profileRef, cliVersion: scope.cliVersion, accountScope: scope.accountScope,
|
|
561
|
+
modelKey: workerModelKey(scope.inferenceProvider, scope.model), effort: scope.effort });
|
|
562
|
+
}
|
|
563
|
+
/** Reused by observation and invocation owners before using qualified health.
|
|
564
|
+
* Older profiles have no qualification record and retain degraded observation. */
|
|
565
|
+
export function assertWorkerHealthQualification(profile, scope) {
|
|
566
|
+
const evidence = profile.health?.qualification;
|
|
567
|
+
if (!evidence)
|
|
568
|
+
return;
|
|
569
|
+
if (evidence.status === 'unqualified') {
|
|
570
|
+
if (profile.health?.coverage === 'full' || profile.health?.handshake === 'semantic') {
|
|
571
|
+
throw new Error('Unqualified worker health cannot assert full or semantic coverage');
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const modelKey = workerModelKey(scope.inferenceProvider, scope.model);
|
|
576
|
+
if (evidence.cliVersion !== scope.cliVersion || evidence.accountScope !== scope.accountScope
|
|
577
|
+
|| evidence.profileRef !== scope.profileRef || evidence.modelKey !== modelKey || evidence.effort !== scope.effort
|
|
578
|
+
|| !profile.health?.qualifiedAt || !Number.isFinite(Date.parse(profile.health.qualifiedAt))
|
|
579
|
+
|| evidence.scopeDigest !== workerHealthScopeDigest(profile, scope)
|
|
580
|
+
|| (evidence.status === 'live-qualified' && evidence.authorizedLive !== true)) {
|
|
581
|
+
throw new Error('Worker health qualification scope changed or lacks live authorization');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
export function parseProviderModelCatalog(input) {
|
|
585
|
+
const validate = getCatalogValidators().catalog;
|
|
586
|
+
if (!validate(input))
|
|
587
|
+
throw new Error(`MODEL_POLICY_INVALID: ${validate.errors?.map(e => `${e.instancePath} ${e.message}`).join('; ')}`);
|
|
588
|
+
return CatalogSchema.parse(input);
|
|
589
|
+
}
|
|
115
590
|
export function loadProviderModelCatalog() {
|
|
116
591
|
if (!catalogCache)
|
|
117
|
-
catalogCache =
|
|
592
|
+
catalogCache = parseProviderModelCatalog(catalogData);
|
|
118
593
|
return catalogCache;
|
|
119
594
|
}
|
|
120
595
|
export function validateCanonicalModelPolicy(input) {
|
|
@@ -138,7 +613,8 @@ export function validateUserProjectModelConfig(input) {
|
|
|
138
613
|
return {
|
|
139
614
|
valid: false,
|
|
140
615
|
diagnostics: parsed.error.issues.map(issue => ({
|
|
141
|
-
code: 'MODEL_POLICY_INVALID',
|
|
616
|
+
code: issue.path.includes('workerSelection') ? 'OG_CONFIG_REFERENCE' : 'MODEL_POLICY_INVALID',
|
|
617
|
+
pointer: `/${issue.path.map(p => String(p).replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}`,
|
|
142
618
|
severity: 'error',
|
|
143
619
|
message: `${issue.path.join('.') || 'config'}: ${issue.message}`,
|
|
144
620
|
})),
|
|
@@ -274,4 +750,142 @@ export function resetModelPolicyCachesForTests() {
|
|
|
274
750
|
registryCache = null;
|
|
275
751
|
catalogCache = null;
|
|
276
752
|
}
|
|
753
|
+
// Dispatch rechecks consume the effective policy from resolution. Keep these
|
|
754
|
+
// predicates shared with route validation so defaults and restrictions agree.
|
|
755
|
+
function isContributorWorkerModel(model) {
|
|
756
|
+
return /(?:^|[-_:/.])contributor(?:$|[-_:/.])/i.test(model);
|
|
757
|
+
}
|
|
758
|
+
function isWorkerModelDenied(modelKey, policy) {
|
|
759
|
+
return (policy.guards?.deniedModels ?? DEFAULT_WORKER_SELECTION.guards.deniedModels).includes(modelKey);
|
|
760
|
+
}
|
|
761
|
+
function isWorkerEffortDenied(effort, policy) {
|
|
762
|
+
return effort !== null && (policy.guards?.deniedEfforts ?? DEFAULT_WORKER_SELECTION.guards.deniedEfforts).includes(effort);
|
|
763
|
+
}
|
|
764
|
+
function isWorkerNullEffortDenied(effort, observation, guidance) {
|
|
765
|
+
return effort === null && (observation.effortSupport !== 'not-applicable' || guidance?.allowNotApplicableEffort !== true);
|
|
766
|
+
}
|
|
767
|
+
function workerRouteDataPolicy(policy, dataClass) {
|
|
768
|
+
const data = { ...DEFAULT_WORKER_SELECTION.dataPolicy, ...policy.dataPolicy };
|
|
769
|
+
const narrowed = dataClass ? policy.dataClasses?.[dataClass] : undefined;
|
|
770
|
+
if (narrowed) {
|
|
771
|
+
if (narrowed.allowedProcessing)
|
|
772
|
+
data.allowedProcessing = data.allowedProcessing?.filter(p => narrowed.allowedProcessing.includes(p));
|
|
773
|
+
if (narrowed.allowUnknown === false)
|
|
774
|
+
data.allowUnknown = false;
|
|
775
|
+
if (narrowed.allowProductImprovement === false)
|
|
776
|
+
data.allowProductImprovement = false;
|
|
777
|
+
if (narrowed.maxRetentionDays !== undefined)
|
|
778
|
+
data.maxRetentionDays = Math.min(data.maxRetentionDays ?? Infinity, narrowed.maxRetentionDays);
|
|
779
|
+
}
|
|
780
|
+
return { data, unknownDataClass: Boolean(dataClass && !narrowed) };
|
|
781
|
+
}
|
|
782
|
+
function isWorkerDataUseDenied(facts, data) {
|
|
783
|
+
return (facts.processing === 'unknown' && data.allowUnknown !== true)
|
|
784
|
+
|| (facts.processing !== 'unknown' && !data.allowedProcessing?.includes(facts.processing))
|
|
785
|
+
|| (facts.productImprovement === null && data.allowUnknown !== true)
|
|
786
|
+
|| (facts.productImprovement === true && data.allowProductImprovement !== true)
|
|
787
|
+
|| (data.maxRetentionDays !== undefined && (facts.retentionDays === null ? data.allowUnknown !== true : facts.retentionDays > data.maxRetentionDays));
|
|
788
|
+
}
|
|
789
|
+
function isWorkerEvidenceStale(observation, policy, now) {
|
|
790
|
+
const observed = Date.parse(observation.observedAt);
|
|
791
|
+
const verified = observation.verifiedAt === undefined ? undefined : Date.parse(observation.verifiedAt);
|
|
792
|
+
const blockHours = policy.freshness?.blockHours ?? DEFAULT_WORKER_SELECTION.freshness.blockHours;
|
|
793
|
+
const policyDays = policy.freshness?.policyDays ?? DEFAULT_WORKER_SELECTION.freshness.policyDays;
|
|
794
|
+
return observed > now || now - observed > blockHours * 3600000
|
|
795
|
+
|| (verified !== undefined && (verified > now || now - verified > policyDays * 86400000));
|
|
796
|
+
}
|
|
797
|
+
/** Optional catalog facts do not veto otherwise qualified selection. */
|
|
798
|
+
export function hasSelectionRelevantConflicts(observation) {
|
|
799
|
+
const relevant = /^\/(?:lifecycle|availability|visibility|hidden|efforts|effortSupport|reasoningEfforts|sunsetAt|retiredAt|replacementModelId|supersededBy|dataUse|id|apiModelId|llmProvider|cliId|family|runtimeVersion|accountScope|endpointFingerprint|identityEvidence|contextTokens|maxOutputTokens|qualification)(?:\/|$)/;
|
|
800
|
+
return observation.conflicts?.some(conflict => relevant.test(conflict.field)) ?? false;
|
|
801
|
+
}
|
|
802
|
+
/** Validate a resolved route without launching or creating managed state. */
|
|
803
|
+
export function validateWorkerPolicyRoute(input) {
|
|
804
|
+
const diagnostics = [];
|
|
805
|
+
const deny = (code, pointer, message) => { diagnostics.push({ code, pointer, message }); };
|
|
806
|
+
const shapeErrors = validateWorkerModelObservation(input.observation);
|
|
807
|
+
if (shapeErrors.length)
|
|
808
|
+
return [{ code: 'OG_ADAPTER_UNQUALIFIED', pointer: '/worker', message: shapeErrors.join('; ') }];
|
|
809
|
+
const observation = input.observation;
|
|
810
|
+
if (hasSelectionRelevantConflicts(observation))
|
|
811
|
+
deny('OG_MODEL_UNAPPROVED', '/worker/conflicts', 'Conflicting selection-relevant model evidence blocks the route');
|
|
812
|
+
else if (observation.conflicts?.length)
|
|
813
|
+
diagnostics.push({
|
|
814
|
+
code: 'OG_MODEL_UNAPPROVED', pointer: '/worker/conflicts', blocking: false,
|
|
815
|
+
message: `Conflicting optional model evidence remains unknown: ${observation.conflicts.map(conflict => conflict.field).join(', ')}`,
|
|
816
|
+
});
|
|
817
|
+
const guards = input.policy.guards ?? DEFAULT_WORKER_SELECTION.guards;
|
|
818
|
+
const guidance = input.policy.roleGuidance?.[input.role];
|
|
819
|
+
if (observation.qualification.status !== 'qualified' || !observation.qualification.roles.includes(input.role))
|
|
820
|
+
deny('OG_ADAPTER_UNQUALIFIED', '/worker/qualification', 'Role lacks qualified evidence');
|
|
821
|
+
if (!guidance)
|
|
822
|
+
deny('OG_CONFIG_REFERENCE', `/defaults/workerSelection/roleGuidance/${input.role}`, 'Role guidance is unavailable');
|
|
823
|
+
const substitution = input.substitution?.model === input.selectedModel
|
|
824
|
+
&& input.substitution.effort === input.effort && input.substitution.rationale.trim();
|
|
825
|
+
if (input.wrapperPin && input.wrapperPin !== input.selectedModel && !substitution)
|
|
826
|
+
deny('OG_PIN_MISMATCH', '/agentOverrides', 'Wrapper exact pin differs from selected model');
|
|
827
|
+
if (guards.requireExactModel !== false && input.selectedModel !== input.apiModelId)
|
|
828
|
+
deny('OG_PIN_MISMATCH', '/worker/id', 'Selected model is not the exact observed API model');
|
|
829
|
+
let modelKey;
|
|
830
|
+
try {
|
|
831
|
+
modelKey = workerModelKey(input.inferenceProvider, input.apiModelId);
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
deny('OG_CONFIG_REFERENCE', '/worker/id', 'Invalid inference model identity');
|
|
835
|
+
}
|
|
836
|
+
// The dispatch guard checks the selected model; when requireExactModel is
|
|
837
|
+
// false that can differ from apiModelId, so deny on either identity.
|
|
838
|
+
let selectedKey;
|
|
839
|
+
try {
|
|
840
|
+
selectedKey = workerModelKey(input.inferenceProvider, input.selectedModel);
|
|
841
|
+
}
|
|
842
|
+
catch { /* reported via apiModelId */ }
|
|
843
|
+
if (isContributorWorkerModel(input.apiModelId) || isContributorWorkerModel(input.selectedModel)
|
|
844
|
+
|| (modelKey && isWorkerModelDenied(modelKey, input.policy))
|
|
845
|
+
|| (selectedKey && isWorkerModelDenied(selectedKey, input.policy)))
|
|
846
|
+
deny('OG_MODEL_UNAPPROVED', '/defaults/workerSelection/guards/deniedModels', 'Inference model is denied');
|
|
847
|
+
if (input.identityEvidence === 'unknown' || input.identityEvidence === 'mismatched' || (guards.minimumIdentityEvidence === 'backend-attested' && input.identityEvidence !== 'backend-attested'))
|
|
848
|
+
deny('OG_ADAPTER_UNQUALIFIED', '/defaults/workerSelection/guards/minimumIdentityEvidence', 'Identity evidence does not meet the required order');
|
|
849
|
+
if (!(guards.allowedVisibility ?? ['listed']).includes(observation.visibility) || !(guards.allowedLifecycle ?? ['active']).includes(observation.lifecycle) || observation.availability !== 'accessible')
|
|
850
|
+
deny('OG_MODEL_UNAPPROVED', '/worker', 'Visibility, lifecycle or availability is ineligible');
|
|
851
|
+
if (input.effort === null) {
|
|
852
|
+
if (isWorkerNullEffortDenied(input.effort, observation, guidance))
|
|
853
|
+
deny('OG_EFFORT_DENIED', '/worker/effortSupport', 'Null effort requires qualified absence of an effort control');
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
const effort = observation.efforts.find(e => e.id === input.effort);
|
|
857
|
+
if (observation.effortSupport !== 'explicit' || !effort || !(input.effort === 'none' ? guidance?.initialEffort === 'none' && guidance.effortLadder.length === 0 : guidance?.effortLadder.includes(input.effort)) || isWorkerEffortDenied(input.effort, input.policy) || (guards.denySelfDelegation !== false && effort.selfDelegates !== false))
|
|
858
|
+
deny('OG_EFFORT_DENIED', '/worker/efforts', 'Unsupported, denied or self-delegating effort');
|
|
859
|
+
}
|
|
860
|
+
const { data, unknownDataClass } = workerRouteDataPolicy(input.policy, input.dataClass);
|
|
861
|
+
if (unknownDataClass)
|
|
862
|
+
deny('OG_DATA_POLICY', '/defaults/workerSelection/dataClasses', 'Unknown data class');
|
|
863
|
+
if (isWorkerDataUseDenied(observation.dataUse, data))
|
|
864
|
+
deny('OG_DATA_POLICY', '/worker/dataUse', 'Execution path data facts do not satisfy policy');
|
|
865
|
+
const now = (input.now ?? new Date()).getTime();
|
|
866
|
+
if (observation.retiredAt != null && Date.parse(observation.retiredAt) <= now
|
|
867
|
+
|| observation.sunsetAt && Date.parse(observation.sunsetAt) <= now) {
|
|
868
|
+
deny('OG_MODEL_UNAPPROVED', '/worker/lifecycle', 'Model retirement or sunset is effective');
|
|
869
|
+
}
|
|
870
|
+
if (isWorkerEvidenceStale(observation, input.policy, now))
|
|
871
|
+
deny('OG_ADAPTER_UNQUALIFIED', '/worker/observedAt', 'Model evidence is stale or from the future');
|
|
872
|
+
else if (now - Date.parse(observation.observedAt) > (input.policy.freshness?.warnHours ?? DEFAULT_WORKER_SELECTION.freshness.warnHours) * 3600000) {
|
|
873
|
+
diagnostics.push({ code: 'OG_ADAPTER_UNQUALIFIED', pointer: '/worker/observedAt', blocking: false, message: 'Model observation exceeds the freshness warning threshold' });
|
|
874
|
+
}
|
|
875
|
+
if (input.context && !workerContextHeadroom(input.context, observation).fits) {
|
|
876
|
+
deny('OG_MODEL_UNAPPROVED', '/worker/contextTokens', 'Bounded context and output do not fit qualified limits');
|
|
877
|
+
}
|
|
878
|
+
return diagnostics;
|
|
879
|
+
}
|
|
880
|
+
export function workerContextHeadroom(estimate, observation) {
|
|
881
|
+
const safetyMargin = Math.max(estimate.safetyMargin ?? 0, Math.ceil((observation.contextTokens ?? 0) * 0.1));
|
|
882
|
+
const terms = [estimate.briefTokens, estimate.inputTokens, estimate.toolOutputAllowance, estimate.reservedOutput, safetyMargin];
|
|
883
|
+
const total = terms.reduce((sum, value) => sum + value, 0);
|
|
884
|
+
const fits = Boolean(estimate.estimator.trim()) && terms.every(value => Number.isSafeInteger(value) && value >= 0)
|
|
885
|
+
&& (estimate.safetyMargin === undefined || Number.isSafeInteger(estimate.safetyMargin) && estimate.safetyMargin >= 0)
|
|
886
|
+
&& Number.isSafeInteger(total) && observation.contextTokens !== null && observation.maxOutputTokens !== null
|
|
887
|
+
&& total <= observation.contextTokens && estimate.reservedOutput <= observation.maxOutputTokens;
|
|
888
|
+
return { ...estimate, safetyMargin, total, qualifiedContextTokens: observation.contextTokens,
|
|
889
|
+
qualifiedMaxOutputTokens: observation.maxOutputTokens, fits };
|
|
890
|
+
}
|
|
277
891
|
//# sourceMappingURL=provider-policy.js.map
|