@aiwg/cli 2026.10.9 → 2026.10.11
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/model-capabilities.v1.json +275 -17
- package/bin/aiwg.mjs +8 -0
- package/dist/src/cli/handlers/mc-admission.js +39 -0
- package/dist/src/cli/handlers/mc.js +2 -31
- package/dist/src/cli/handlers/models-help.js +52 -0
- package/dist/src/cli/handlers/models.js +11 -81
- package/dist/src/cli/handlers/sessions.js +198 -15
- package/dist/src/cli/handlers/use-addon-profile.js +72 -0
- package/dist/src/cli/handlers/use.js +203 -234
- package/dist/src/cli/ui.js +1 -0
- package/dist/src/config/aiwg-config.js +38 -184
- package/dist/src/config/cli-help.js +54 -0
- package/dist/src/config/cli.js +5 -69
- package/dist/src/config/config-normalization.js +10 -0
- package/dist/src/config/worker-activation-gate.js +26 -0
- package/dist/src/config/worker-adoption-cli.js +102 -0
- package/dist/src/config/worker-adoption.js +368 -0
- package/dist/src/config/worker-config-digest.js +14 -0
- package/dist/src/config/worker-context.js +337 -0
- package/dist/src/config/worker-group-templates.js +150 -0
- package/dist/src/config/worker-guidance-warning.js +28 -0
- package/dist/src/config/worker-owner-lock.js +152 -0
- package/dist/src/config/worker-parallelism.js +154 -0
- package/dist/src/config/worker-write.js +75 -0
- package/dist/src/decision/telemetry/exporter.js +2 -1
- package/dist/src/decision/telemetry/index.js +1 -0
- package/dist/src/decision/telemetry/mapping.js +43 -0
- package/dist/src/decision/telemetry/otlp-http.js +5 -2
- package/dist/src/decision/telemetry/redaction.js +6 -1
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/telemetry/worker.js +69 -0
- package/dist/src/lint/rule-activation.js +15 -7
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/server.mjs +2 -11
- package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
- package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
- package/dist/src/mcp/tools/worker-invoke.mjs +114 -16
- package/dist/src/mission-protocol/codecs.js +8 -38
- package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
- package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
- package/dist/src/models/conductor-data-policy.d.mts +8 -0
- package/dist/src/models/conductor-data-policy.mjs +190 -0
- package/dist/src/models/config-loader.js +19 -3
- package/dist/src/models/config-writer.js +27 -0
- package/dist/src/models/model-capabilities.v1.json +275 -17
- package/dist/src/models/provider-policy.js +14 -155
- package/dist/src/models/resolver.js +2 -293
- package/dist/src/models/worker-artifact-binding.mjs +12 -0
- package/dist/src/models/worker-direct-readiness-policy.js +155 -0
- package/dist/src/models/worker-direct-readiness.js +9 -218
- package/dist/src/models/worker-health-readiness.js +105 -26
- package/dist/src/models/worker-installation.mjs +12 -14
- package/dist/src/models/worker-member-references.js +64 -0
- package/dist/src/models/worker-plan.js +317 -0
- package/dist/src/models/worker-selection-policy.js +217 -0
- package/dist/src/models/wrapper-deployment.js +1 -1
- package/dist/src/policy/authorization.js +29 -1
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- package/dist/src/serve/shared-host-admission-checks.js +116 -0
- package/dist/src/serve/shared-host-scheduler.js +25 -107
- package/dist/src/serve/shared-host-starvation.js +73 -0
- package/dist/src/serve/worker-artifacts.d.mts +4 -0
- package/dist/src/serve/worker-artifacts.mjs +56 -0
- package/dist/src/serve/worker-command-request.js +68 -0
- package/dist/src/serve/worker-invocation-paths.js +144 -0
- package/dist/src/serve/worker-invocation-preparation.js +169 -0
- package/dist/src/serve/worker-invocation.js +24 -291
- package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
- package/dist/src/serve/worker-stdin-frame.mjs +6 -6
- package/dist/src/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/batch-import.js +34 -6
- package/dist/src/sessions/contracts.js +19 -0
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/fortemi-export-mapping.js +33 -3
- package/dist/src/sessions/host-catalog.js +157 -0
- package/dist/src/sessions/image-blobs.js +141 -0
- package/dist/src/sessions/importer.js +23 -3
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/workspace-discovery.js +5 -2
- package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
- package/package.json +15 -1
- package/schemas/mission-v1.schema.json +20 -19
- package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
- package/tools/agents/providers/claude-post-deploy.mjs +35 -0
- package/tools/agents/providers/claude.mjs +3 -30
- package/tools/agents/providers/codex-post-deploy.mjs +13 -0
- package/tools/agents/providers/codex.mjs +2 -8
- package/tools/agents/providers/conductor-data-guard.mjs +61 -0
- package/tools/agents/providers/rule-delivery.mjs +3 -16
- package/tools/agents/providers/rule-presentation.mjs +38 -0
- package/tools/plugin/package-plugins.mjs +1 -1
- package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
- package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
- package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
- package/tools/ralph-external/lib/session-attempt.mjs +85 -0
- package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
- package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
- package/tools/ralph-external/lib/worker-context.mjs +237 -0
- package/tools/ralph-external/lib/worker-final.mjs +143 -0
- package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
- package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
- package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
- package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +19 -73
- package/tools/ralph-external/lib/worker-launch-cli.mjs +49 -0
- package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
- package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
- package/tools/ralph-external/lib/worker-termination.mjs +82 -0
- package/tools/ralph-external/session-launcher.mjs +124 -313
- package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Validates and merges worker selection restrictions and constrained role aliases.
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { validLocalOnlyPath } from './conductor-data-policy.mjs';
|
|
4
|
+
import { integer } from '../schema/zod-validation.mjs';
|
|
5
|
+
const uniqueStrings = z.array(z.string().min(1)).refine(values => new Set(values).size === values.length, 'Duplicate entries');
|
|
6
|
+
const WorkerDataPolicySchema = z.strictObject({
|
|
7
|
+
localOnlyPaths: uniqueStrings.refine(values => values.every(validLocalOnlyPath), 'Expected literal project-relative paths').optional(),
|
|
8
|
+
allowedProcessing: z.array(z.enum(['local', 'remote'])).refine(values => new Set(values).size === values.length).optional(),
|
|
9
|
+
allowProductImprovement: z.boolean().optional(),
|
|
10
|
+
allowUnknown: z.boolean().optional(),
|
|
11
|
+
maxRetentionDays: integer().nonnegative().optional(),
|
|
12
|
+
});
|
|
13
|
+
const baseRoles = ['efficiency', 'coding', 'reasoning'];
|
|
14
|
+
const WorkerRoleSchema = z.strictObject({
|
|
15
|
+
baseRole: z.enum(baseRoles).optional(),
|
|
16
|
+
groups: uniqueStrings.refine(values => values.length > 0),
|
|
17
|
+
tier: z.enum(['economy', 'standard', 'premium', 'max-quality']),
|
|
18
|
+
initialEffort: z.string().min(1).nullable(),
|
|
19
|
+
effortLadder: uniqueStrings,
|
|
20
|
+
escalateAfter: integer().positive().optional(),
|
|
21
|
+
maxEscalations: integer().nonnegative().optional(),
|
|
22
|
+
deescalateAfter: integer().positive().optional(),
|
|
23
|
+
allowNotApplicableEffort: z.boolean().optional(),
|
|
24
|
+
});
|
|
25
|
+
const reservedRoleKey = (key) => key === 'prototype' || Object.hasOwn(Object.prototype, key);
|
|
26
|
+
const roleGuidanceSchema = z.preprocess((input, context) => {
|
|
27
|
+
if (input !== null && typeof input === 'object')
|
|
28
|
+
for (const role of Object.keys(input)) {
|
|
29
|
+
if (reservedRoleKey(role))
|
|
30
|
+
context.addIssue({ code: 'custom', path: [role], message: `Reserved worker role alias key: ${role}` });
|
|
31
|
+
}
|
|
32
|
+
return input;
|
|
33
|
+
}, z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), WorkerRoleSchema));
|
|
34
|
+
/** Inherited object members never constitute configured role guidance. */
|
|
35
|
+
export function ownWorkerRoleGuidance(policy, role) {
|
|
36
|
+
return policy?.roleGuidance && Object.hasOwn(policy.roleGuidance, role) ? policy.roleGuidance[role] : undefined;
|
|
37
|
+
}
|
|
38
|
+
export const WorkerSelectionPolicySchema = z.strictObject({
|
|
39
|
+
roleGuidance: roleGuidanceSchema.optional(),
|
|
40
|
+
guards: z.strictObject({
|
|
41
|
+
deniedEfforts: uniqueStrings.optional(),
|
|
42
|
+
deniedModels: uniqueStrings.optional(),
|
|
43
|
+
denySelfDelegation: z.boolean().optional(),
|
|
44
|
+
requireExactModel: z.boolean().optional(),
|
|
45
|
+
allowedVisibility: z.array(z.enum(['listed', 'hidden'])).refine(v => new Set(v).size === v.length).optional(),
|
|
46
|
+
allowedLifecycle: z.array(z.enum(['active', 'deprecated'])).refine(v => new Set(v).size === v.length).optional(),
|
|
47
|
+
minimumIdentityEvidence: z.enum(['runtime-configured', 'backend-attested']).optional(),
|
|
48
|
+
}).optional(),
|
|
49
|
+
dataPolicy: WorkerDataPolicySchema.optional(),
|
|
50
|
+
dataClasses: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), WorkerDataPolicySchema).optional(),
|
|
51
|
+
freshness: z.strictObject({
|
|
52
|
+
warnHours: integer().positive().optional(),
|
|
53
|
+
blockHours: integer().positive().optional(),
|
|
54
|
+
policyDays: integer().positive().optional(),
|
|
55
|
+
}).optional(),
|
|
56
|
+
taskClasses: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), z.string().min(1)).optional(),
|
|
57
|
+
}).superRefine((policy, context) => {
|
|
58
|
+
for (const [role, guidance] of Object.entries(policy.roleGuidance ?? {})) {
|
|
59
|
+
if (baseRoles.includes(role) ? guidance.baseRole !== undefined : guidance.baseRole === undefined)
|
|
60
|
+
context.addIssue({ code: 'custom', path: ['roleGuidance', role], message: 'Unknown worker role requires a baseRole; base roles cannot be aliases' });
|
|
61
|
+
const base = guidance.baseRole && ownWorkerRoleGuidance(policy, guidance.baseRole);
|
|
62
|
+
if (base)
|
|
63
|
+
for (const message of aliasRestrictionErrors(guidance, base))
|
|
64
|
+
context.addIssue({ code: 'custom', path: ['roleGuidance', role], message });
|
|
65
|
+
if (guidance.effortLadder.includes('none') || (guidance.initialEffort === 'none' ? guidance.effortLadder.length !== 0 : guidance.initialEffort !== null && !guidance.effortLadder.includes(guidance.initialEffort))) {
|
|
66
|
+
context.addIssue({ code: 'custom', path: ['roleGuidance', role], message: 'Initial effort must belong to the ladder; real none requires an empty ladder' });
|
|
67
|
+
}
|
|
68
|
+
if (guidance.initialEffort === null && (guidance.allowNotApplicableEffort !== true || guidance.effortLadder.length !== 0)) {
|
|
69
|
+
context.addIssue({ code: 'custom', path: ['roleGuidance', role], message: 'Null effort requires an empty ladder and allowNotApplicableEffort=true' });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const key of policy.guards?.deniedModels ?? []) {
|
|
73
|
+
if (!isCanonicalWorkerModelKey(key))
|
|
74
|
+
context.addIssue({ code: 'custom', path: ['guards', 'deniedModels'], message: 'Noncanonical inference model key' });
|
|
75
|
+
}
|
|
76
|
+
if (policy.freshness?.warnHours !== undefined && policy.freshness.blockHours !== undefined && policy.freshness.warnHours > policy.freshness.blockHours) {
|
|
77
|
+
context.addIssue({ code: 'custom', path: ['freshness'], message: 'warnHours must not exceed blockHours' });
|
|
78
|
+
}
|
|
79
|
+
for (const role of Object.values(policy.taskClasses ?? {})) {
|
|
80
|
+
if (!baseRoles.includes(role) && !ownWorkerRoleGuidance(policy, role)?.baseRole)
|
|
81
|
+
context.addIssue({ code: 'custom', path: ['taskClasses'], message: 'Unknown worker role' });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
/** Aliases may reorder/subset choices, never change base authority or escalation controls. */
|
|
85
|
+
function aliasRestrictionErrors(alias, base) {
|
|
86
|
+
const errors = [];
|
|
87
|
+
if (alias.groups.some(group => !base.groups.includes(group)))
|
|
88
|
+
errors.push('Alias groups widen base role');
|
|
89
|
+
if (alias.effortLadder.some(effort => !base.effortLadder.includes(effort))
|
|
90
|
+
|| alias.initialEffort === 'none' && base.initialEffort !== 'none'
|
|
91
|
+
|| alias.initialEffort === null && base.initialEffort !== null
|
|
92
|
+
|| alias.allowNotApplicableEffort === true && base.allowNotApplicableEffort !== true)
|
|
93
|
+
errors.push('Alias effort choices widen base role');
|
|
94
|
+
if (alias.tier !== base.tier)
|
|
95
|
+
errors.push('Alias tier must retain base role authority');
|
|
96
|
+
if ((alias.maxEscalations ?? base.maxEscalations ?? 2) > (base.maxEscalations ?? 2))
|
|
97
|
+
errors.push('Alias maxEscalations widens base role');
|
|
98
|
+
for (const key of ['escalateAfter', 'deescalateAfter']) {
|
|
99
|
+
if (alias[key] !== undefined && alias[key] !== (base[key] ?? 2))
|
|
100
|
+
errors.push(`Alias ${key} must retain base role control`);
|
|
101
|
+
}
|
|
102
|
+
return errors;
|
|
103
|
+
}
|
|
104
|
+
/** Canonical RFC3986 identity; installation and account never enter the key. */
|
|
105
|
+
export function workerModelKey(inferenceProvider, apiModelId) {
|
|
106
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(inferenceProvider) || !apiModelId)
|
|
107
|
+
throw new Error('OG_CONFIG_REFERENCE: invalid inference model identity');
|
|
108
|
+
return `${inferenceProvider}/${encodeURIComponent(apiModelId).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`;
|
|
109
|
+
}
|
|
110
|
+
export function isCanonicalWorkerModelKey(key) {
|
|
111
|
+
const slash = key.indexOf('/');
|
|
112
|
+
try {
|
|
113
|
+
return slash > 0 && workerModelKey(key.slice(0, slash), decodeURIComponent(key.slice(slash + 1))) === key;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export const DEFAULT_WORKER_SELECTION = {
|
|
120
|
+
guards: { deniedEfforts: ['ultra'], deniedModels: [], denySelfDelegation: true, requireExactModel: true, allowedVisibility: ['listed'], allowedLifecycle: ['active'], minimumIdentityEvidence: 'runtime-configured' },
|
|
121
|
+
dataPolicy: { allowedProcessing: ['local', 'remote'], allowProductImprovement: false, allowUnknown: false },
|
|
122
|
+
freshness: { warnHours: 24, blockHours: 168, policyDays: 30 },
|
|
123
|
+
taskClasses: { inventory: 'efficiency', implementation: 'coding', architecture: 'reasoning', review: 'reasoning' },
|
|
124
|
+
};
|
|
125
|
+
/** Merge leaves, preserving pre-merge explicit origins and attempted relaxations. */
|
|
126
|
+
export function mergeWorkerSelection(layers) {
|
|
127
|
+
const policy = Object.create(null);
|
|
128
|
+
const origins = Object.create(null);
|
|
129
|
+
const diagnostics = [];
|
|
130
|
+
const visit = (target, input, parts, layer) => {
|
|
131
|
+
for (const [key, value] of Object.entries(input)) {
|
|
132
|
+
const path = [...parts, key];
|
|
133
|
+
const pointer = `/defaults/workerSelection/${path.map(p => p.replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}`;
|
|
134
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
135
|
+
if (!Object.hasOwn(target, key))
|
|
136
|
+
target[key] = Object.create(null);
|
|
137
|
+
visit(target[key], value, path, layer);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const origin = { source: layer.source, authority: layer.authority, digest: layer.digest, pointer, contribution: structuredClone(value) };
|
|
141
|
+
const prior = origins[pointer] ?? [];
|
|
142
|
+
const explicit = prior.filter(o => o.authority !== 'builtin');
|
|
143
|
+
let effective = structuredClone(value);
|
|
144
|
+
let relaxation = false;
|
|
145
|
+
if (layer.authority !== 'builtin' && explicit.length > 0) {
|
|
146
|
+
const previous = target[key];
|
|
147
|
+
if (['deniedEfforts', 'deniedModels', 'localOnlyPaths'].includes(key))
|
|
148
|
+
effective = [...new Set([...previous, ...value])];
|
|
149
|
+
else if (['allowedVisibility', 'allowedLifecycle', 'allowedProcessing'].includes(key)) {
|
|
150
|
+
relaxation = value.some((v) => !previous.includes(v));
|
|
151
|
+
effective = previous.filter((v) => value.includes(v));
|
|
152
|
+
}
|
|
153
|
+
else if (['denySelfDelegation', 'requireExactModel'].includes(key)) {
|
|
154
|
+
relaxation = previous === true && value === false;
|
|
155
|
+
effective = previous || value;
|
|
156
|
+
}
|
|
157
|
+
else if (['allowUnknown', 'allowProductImprovement'].includes(key)) {
|
|
158
|
+
relaxation = previous === false && value === true;
|
|
159
|
+
effective = previous && value;
|
|
160
|
+
}
|
|
161
|
+
else if (['maxRetentionDays', 'warnHours', 'blockHours', 'policyDays'].includes(key)) {
|
|
162
|
+
relaxation = value > previous;
|
|
163
|
+
effective = Math.min(previous, value);
|
|
164
|
+
}
|
|
165
|
+
else if (key === 'minimumIdentityEvidence') {
|
|
166
|
+
relaxation = previous === 'backend-attested' && value !== previous;
|
|
167
|
+
effective = previous === 'backend-attested' ? previous : value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (relaxation)
|
|
171
|
+
diagnostics.push({ code: 'OG_POLICY_RELAXATION', pointer, message: 'Explicit restriction cannot relax an inherited restriction', origins: [...explicit, origin] });
|
|
172
|
+
target[key] = effective;
|
|
173
|
+
origins[pointer] = [...prior, origin];
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
for (const layer of layers)
|
|
177
|
+
if (layer.policy) {
|
|
178
|
+
for (const [role, guidance] of Object.entries(layer.policy.roleGuidance ?? {})) {
|
|
179
|
+
if (!guidance.baseRole)
|
|
180
|
+
continue;
|
|
181
|
+
const previous = ownWorkerRoleGuidance(policy, role);
|
|
182
|
+
if (layer.authority !== 'project' || previous?.baseRole && previous.baseRole !== guidance.baseRole)
|
|
183
|
+
diagnostics.push({
|
|
184
|
+
code: 'OG_POLICY_RELAXATION', pointer: `/defaults/workerSelection/roleGuidance/${role}/baseRole`,
|
|
185
|
+
message: 'Only project layers may declare constrained aliases; inherited aliases cannot change baseRole',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
visit(policy, layer.policy, [], layer);
|
|
189
|
+
}
|
|
190
|
+
for (const [role, guidance] of Object.entries(policy.roleGuidance ?? {})) {
|
|
191
|
+
const base = guidance.baseRole && ownWorkerRoleGuidance(policy, guidance.baseRole);
|
|
192
|
+
if (base)
|
|
193
|
+
for (const field of ['maxEscalations', 'escalateAfter', 'deescalateAfter'])
|
|
194
|
+
guidance[field] ??= base[field];
|
|
195
|
+
if (guidance.baseRole && !base)
|
|
196
|
+
diagnostics.push({
|
|
197
|
+
code: 'OG_CONFIG_REFERENCE', pointer: `/defaults/workerSelection/roleGuidance/${role}/baseRole`, message: 'Alias base role has no guidance',
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const validation = WorkerSelectionPolicySchema.safeParse(policy);
|
|
201
|
+
if (!validation.success)
|
|
202
|
+
for (const issue of validation.error.issues)
|
|
203
|
+
diagnostics.push({ code: 'OG_CONFIG_REFERENCE', pointer: `/defaults/workerSelection/${issue.path.join('/')}`, message: issue.message });
|
|
204
|
+
// Data classes are applied as further restrictions to the base path.
|
|
205
|
+
for (const dataClass of Object.values(policy.dataClasses ?? {})) {
|
|
206
|
+
const base = policy.dataPolicy ?? {};
|
|
207
|
+
if (dataClass.allowedProcessing)
|
|
208
|
+
dataClass.allowedProcessing = dataClass.allowedProcessing.filter(p => !base.allowedProcessing || base.allowedProcessing.includes(p));
|
|
209
|
+
for (const key of ['allowUnknown', 'allowProductImprovement'])
|
|
210
|
+
if (base[key] === false)
|
|
211
|
+
dataClass[key] = false;
|
|
212
|
+
if (base.maxRetentionDays !== undefined)
|
|
213
|
+
dataClass.maxRetentionDays = Math.min(dataClass.maxRetentionDays ?? base.maxRetentionDays, base.maxRetentionDays);
|
|
214
|
+
}
|
|
215
|
+
return { policy, origins, diagnostics };
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=worker-selection-policy.js.map
|
|
@@ -156,7 +156,7 @@ export async function verifyModelWrapperDeployment(agentsPath, expectation = {})
|
|
|
156
156
|
const missing = [];
|
|
157
157
|
const mismatches = [];
|
|
158
158
|
for (const [role, wrapper] of expected) {
|
|
159
|
-
const matches = files.filter(file => path.basename(file).replace(
|
|
159
|
+
const matches = files.filter(file => path.basename(file).replace(/(?:\.agent)?\.(?:md|toml|json|yaml|yml)$/i, '') === wrapper);
|
|
160
160
|
if (matches.length === 0) {
|
|
161
161
|
missing.push(wrapper);
|
|
162
162
|
continue;
|
|
@@ -170,13 +170,41 @@ const LEGACY_MANIFESTS = [
|
|
|
170
170
|
'.aiwg/ops/security/repo-access.manifest.yaml',
|
|
171
171
|
'.aiwg/security/repo-access.manifest.yaml',
|
|
172
172
|
];
|
|
173
|
+
/**
|
|
174
|
+
* Factory delivery defaults written by `aiwg init`/`aiwg use`
|
|
175
|
+
* (emptyConfig in src/config/aiwg-config.ts). Kept as a literal here —
|
|
176
|
+
* rather than importing the config module — because aiwg-config already
|
|
177
|
+
* imports this module for authorization validation, so a value import
|
|
178
|
+
* would be a cycle. A unit test pins this literal against
|
|
179
|
+
* emptyConfig().delivery (#3635).
|
|
180
|
+
*/
|
|
181
|
+
const DEFAULT_DELIVERY_POLICY = {
|
|
182
|
+
mode: 'pr-required',
|
|
183
|
+
default_branch: 'main',
|
|
184
|
+
require_ci_green: true,
|
|
185
|
+
auto_close_issues: true,
|
|
186
|
+
issue_comment_on_cycle: true,
|
|
187
|
+
force_push_policy: 'never',
|
|
188
|
+
};
|
|
189
|
+
/** True when the delivery block is exactly what the tool writes for a fresh project. */
|
|
190
|
+
export function isDefaultDeliveryPolicy(delivery) {
|
|
191
|
+
if (!delivery || typeof delivery !== 'object')
|
|
192
|
+
return false;
|
|
193
|
+
const actual = delivery;
|
|
194
|
+
const actualKeys = Object.keys(actual);
|
|
195
|
+
return actualKeys.length === Object.keys(DEFAULT_DELIVERY_POLICY).length
|
|
196
|
+
&& actualKeys.every((key) => actual[key] === DEFAULT_DELIVERY_POLICY[key]);
|
|
197
|
+
}
|
|
173
198
|
export async function auditLegacyPermissions(projectDir, config) {
|
|
174
199
|
const out = validateAuthorization(config?.authorization);
|
|
175
200
|
if (!config?.authorization && config?.repos?.some(repo => repo.allowed?.length))
|
|
176
201
|
out.push({ severity: 'warning', code: 'legacy-repos-allowed', source: '.aiwg/aiwg.config', message: 'repos[].allowed should be normalized into authorization roles and assignments.' });
|
|
177
202
|
if (!config?.authorization && Object.keys(config?.repo_maintainer?.tiers ?? {}).length)
|
|
178
203
|
out.push({ severity: 'warning', code: 'legacy-maintainer-tiers', source: '.aiwg/aiwg.config', message: 'repo_maintainer.tiers should be represented by normalized role assignments.' });
|
|
179
|
-
|
|
204
|
+
// Tool-written delivery defaults are not operator legacy input: a fresh
|
|
205
|
+
// `aiwg use` must not produce something that needs migration (#3635).
|
|
206
|
+
// Operator-edited delivery (any key added, removed, or changed) still flags.
|
|
207
|
+
if (!config?.authorization && config?.delivery && !isDefaultDeliveryPolicy(config.delivery))
|
|
180
208
|
out.push({ severity: 'warning', code: 'legacy-delivery-policy', source: '.aiwg/aiwg.config', message: 'delivery and signing operations should be normalized into scoped role assignments.' });
|
|
181
209
|
for (const path of LEGACY_MANIFESTS) {
|
|
182
210
|
try {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AiwgProbeCommand {
|
|
2
|
+
file: string;
|
|
3
|
+
prefix: string[];
|
|
4
|
+
via: 'aiwg-bin' | 'entrypoint' | 'package' | 'path';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveAiwgProbeCommand(options?: {
|
|
8
|
+
env?: NodeJS.ProcessEnv;
|
|
9
|
+
argv1?: string | undefined;
|
|
10
|
+
execPath?: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
packageRoot?: string | null;
|
|
13
|
+
}): AiwgProbeCommand;
|
|
14
|
+
|
|
15
|
+
export function probeNotOnPathDetail(): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Resolves the aiwg invocation for doctor's discovery self-probes so they
|
|
2
|
+
// run the current entrypoint instead of failing when aiwg is not on PATH.
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export function resolveAiwgProbeCommand({
|
|
7
|
+
env = process.env,
|
|
8
|
+
argv1 = process.argv[1],
|
|
9
|
+
execPath = process.execPath,
|
|
10
|
+
cwd = process.cwd(),
|
|
11
|
+
packageRoot = null,
|
|
12
|
+
} = {}) {
|
|
13
|
+
const override = env?.AIWG_BIN;
|
|
14
|
+
if (typeof override === 'string' && override.length > 0) {
|
|
15
|
+
return { file: override, prefix: [], via: 'aiwg-bin' };
|
|
16
|
+
}
|
|
17
|
+
const entry = typeof argv1 === 'string' && argv1.length > 0
|
|
18
|
+
? (path.isAbsolute(argv1) ? argv1 : path.resolve(cwd, argv1))
|
|
19
|
+
: null;
|
|
20
|
+
// Only the aiwg launcher itself is a usable entrypoint; doctor also runs as
|
|
21
|
+
// `node tools/cli/doctor.mjs`, where argv[1] is doctor, not aiwg.
|
|
22
|
+
const isLauncher = entry && ['aiwg', 'aiwg.mjs'].includes(path.basename(entry));
|
|
23
|
+
if (entry && isLauncher && existsSync(entry)) {
|
|
24
|
+
return { file: execPath, prefix: [entry], via: 'entrypoint' };
|
|
25
|
+
}
|
|
26
|
+
if (typeof packageRoot === 'string' && packageRoot.length > 0) {
|
|
27
|
+
const bin = path.join(packageRoot, 'bin', 'aiwg.mjs');
|
|
28
|
+
if (existsSync(bin)) {
|
|
29
|
+
return { file: execPath, prefix: [bin], via: 'package' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { file: 'aiwg', prefix: [], via: 'path' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function probeNotOnPathDetail() {
|
|
36
|
+
return 'aiwg not on PATH (spawn ENOENT) — add the npm global bin to PATH or invoke doctor via the install entrypoint';
|
|
37
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export function attemptBlock(snapshot, candidate, policy, now) {
|
|
2
|
+
const accounting = candidate.accounting;
|
|
3
|
+
const deny = (cause, kind = 'policy-exclusion') => ({ kind, cause, terminal: true });
|
|
4
|
+
const attempts = Object.values(snapshot.records).filter(record => record.accounting?.attemptNumber !== undefined);
|
|
5
|
+
const taskAttempts = attempts.filter(record => record.accounting.taskKey === accounting.taskKey);
|
|
6
|
+
if (taskAttempts.length) {
|
|
7
|
+
const previous = taskAttempts.sort((a, b) => a.accounting.attemptNumber - b.accounting.attemptNumber).at(-1);
|
|
8
|
+
if (candidate.allowance?.previousRequestId !== previous.requestId)
|
|
9
|
+
return deny('new attempt must reference the latest task attempt');
|
|
10
|
+
if (previous.state !== 'settled')
|
|
11
|
+
return deny('previous attempt requires proof-checked reconciliation before replacement');
|
|
12
|
+
const failure = previous.accounting.failure;
|
|
13
|
+
const purpose = candidate.allowance?.purpose ?? 'retry';
|
|
14
|
+
if (purpose === 'retry' || purpose === 'fallback' || failure?.class === 'rate-limit') {
|
|
15
|
+
if (!failure || !failure.retryable || !['rate-limit', 'unavailable', 'timeout'].includes(failure.class))
|
|
16
|
+
return deny('previous failure does not permit automatic retry');
|
|
17
|
+
if (!(policy.managed.retry?.on ?? ['rate-limit', 'unavailable', 'timeout']).includes(failure.class))
|
|
18
|
+
return deny(`${failure.class} retries are disabled`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (candidate.allowance?.previousRequestId)
|
|
22
|
+
return deny('unknown previous task attempt');
|
|
23
|
+
for (const key of [accounting.runKey, accounting.taskKey]) {
|
|
24
|
+
const scope = snapshot.allowances[key];
|
|
25
|
+
if (Date.parse(scope.deadlineAt ?? '') <= now)
|
|
26
|
+
return deny('cumulative allowance deadline elapsed');
|
|
27
|
+
const prior = attempts.filter(record => record.accounting.runKey === key || record.accounting.taskKey === key);
|
|
28
|
+
if (scope.limits.maxAttempts !== undefined && prior.reduce((sum, record) => sum + record.accounting.attemptCount, 0) + accounting.attemptCount > scope.limits.maxAttempts)
|
|
29
|
+
return deny('cumulative attempt allowance exhausted', 'budget-denial');
|
|
30
|
+
for (const [dimension, limit] of [['tokens', scope.limits.maxTokens], ['costUsd', scope.limits.maxCostUsd]]) {
|
|
31
|
+
if (limit === undefined)
|
|
32
|
+
continue;
|
|
33
|
+
let used = 0;
|
|
34
|
+
for (const record of prior) {
|
|
35
|
+
const value = chargedUsage(record, dimension);
|
|
36
|
+
if (value === null)
|
|
37
|
+
return deny(`unknown ${dimension} charge has no finite reservation`, 'budget-denial');
|
|
38
|
+
used += value;
|
|
39
|
+
}
|
|
40
|
+
const reserved = accounting.reservation[dimension];
|
|
41
|
+
if (reserved === null || reserved === 0 || used + reserved > limit)
|
|
42
|
+
return deny(`cumulative ${dimension} reservation exceeds allowance`, 'budget-denial');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (taskAttempts.some(record => Date.parse(record.accounting.notBefore ?? '') > now))
|
|
46
|
+
return { kind: 'policy-exclusion', cause: 'retry-backoff', terminal: false };
|
|
47
|
+
if (policy.managed.budgets?.unknownCost === 'deny' && accounting.reservation.costUsd === null)
|
|
48
|
+
return deny('unknown cost is denied by policy', 'budget-denial');
|
|
49
|
+
const quotaScope = candidate.managed?.cli && snapshot.hostBinding
|
|
50
|
+
? policy.managed.upstream[candidate.managed.cli].quotaScope : candidate.managed?.quotaScope;
|
|
51
|
+
const quotas = quotaScope ? [`quota/${encodeURIComponent(quotaScope)}`]
|
|
52
|
+
: Object.keys(snapshot.cooldowns ?? {});
|
|
53
|
+
if (quotas.some(key => Date.parse(snapshot.cooldowns?.[key]?.until ?? '') > now))
|
|
54
|
+
return { kind: 'policy-exclusion', cause: 'quota-cooldown', terminal: false };
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
function chargedUsage(record, dimension) {
|
|
58
|
+
const accounting = record.accounting;
|
|
59
|
+
const usage = accounting.usage;
|
|
60
|
+
const measured = !usage || usage.quality === 'unknown' ? null : dimension === 'costUsd' ? usage.costUsd
|
|
61
|
+
: usage.inputTokens === null || usage.outputTokens === null || usage.cacheTokens === null || usage.reasoningTokens === null ? null
|
|
62
|
+
: usage.inputTokens + usage.outputTokens + usage.cacheTokens + usage.reasoningTokens;
|
|
63
|
+
const reservation = accounting.reservation[dimension];
|
|
64
|
+
// Uncertain execution can still accrue spend; estimates never refund a reserve.
|
|
65
|
+
if (record.state !== 'settled' || usage?.quality !== 'reported') {
|
|
66
|
+
return reservation === null || measured === null ? reservation : Math.max(reservation, measured);
|
|
67
|
+
}
|
|
68
|
+
return measured ?? reservation;
|
|
69
|
+
}
|
|
70
|
+
export function capacityBlock(snapshot, candidate, policy, now = -Infinity) {
|
|
71
|
+
const slots = policy.managed ? candidate.reservedSlots ?? 1 : 1;
|
|
72
|
+
const active = Object.values(snapshot.records).filter(record => record.state === 'admitted' || (policy.managed && record.state === 'uncertain'));
|
|
73
|
+
const limits = [
|
|
74
|
+
{ dimension: 'global', cap: policy.maxConcurrent, matches: () => true },
|
|
75
|
+
];
|
|
76
|
+
if (policy.managed)
|
|
77
|
+
for (const key of candidate.dimensions ?? []) {
|
|
78
|
+
const cap = policy.managed.dimensionCaps[key];
|
|
79
|
+
if (cap !== undefined)
|
|
80
|
+
limits.push({ dimension: key, cap, matches: record => !record.dimensions || record.dimensions.includes(key) });
|
|
81
|
+
}
|
|
82
|
+
for (const [field, quotas] of [
|
|
83
|
+
['environment', policy.environmentQuotas], ['provider', policy.providerQuotas],
|
|
84
|
+
['runtimeKind', { host: policy.defaultHostQuota ?? 1, ...policy.runtimeQuotas }],
|
|
85
|
+
]) {
|
|
86
|
+
const cap = quotas?.[candidate[field]];
|
|
87
|
+
if (cap !== undefined)
|
|
88
|
+
limits.push({ dimension: `${field}/${candidate[field]}`, cap: policy.managed ? cap : Math.ceil(cap), matches: record => record[field] === candidate[field] });
|
|
89
|
+
}
|
|
90
|
+
const impossible = limits.find(limit => slots > limit.cap);
|
|
91
|
+
if (impossible)
|
|
92
|
+
return { kind: 'policy-exclusion', cause: 'request-exceeds-cap', dimension: impossible.dimension, terminal: false };
|
|
93
|
+
const used = (records, limit) => records.filter(limit.matches)
|
|
94
|
+
.reduce((sum, record) => sum + (policy.managed ? record.reservedSlots ?? 1 : 1), 0);
|
|
95
|
+
const blocked = limits.filter(limit => used(active, limit) + slots > limit.cap);
|
|
96
|
+
if (!blocked.length)
|
|
97
|
+
return undefined;
|
|
98
|
+
const dimensions = blocked.map(limit => limit.dimension);
|
|
99
|
+
const certain = active.filter(record => record.state === 'admitted' && !(Date.parse(record.leaseExpiresAt ?? '') <= now));
|
|
100
|
+
if (policy.managed && blocked.some(limit => used(certain, limit) + slots <= limit.cap)) {
|
|
101
|
+
return { kind: 'safety-blocked', cause: 'uncertain-lease', dimension: dimensions[0], dimensions, terminal: false };
|
|
102
|
+
}
|
|
103
|
+
return { kind: 'capacity-wait', cause: 'capacity-cap', dimension: dimensions[0], dimensions, terminal: false };
|
|
104
|
+
}
|
|
105
|
+
/** Past retry/cooldown exclusion does not count toward an eligible service gap. */
|
|
106
|
+
export function attemptReadyAt(snapshot, candidate, policy) {
|
|
107
|
+
const retry = Object.values(snapshot.records).filter(record => record.accounting?.attemptNumber !== undefined
|
|
108
|
+
&& record.accounting.taskKey === candidate.accounting?.taskKey)
|
|
109
|
+
.reduce((latest, record) => Math.max(latest, Date.parse(record.accounting.notBefore ?? '') || 0), 0);
|
|
110
|
+
const quota = candidate.managed?.cli && snapshot.hostBinding
|
|
111
|
+
? policy.managed.upstream[candidate.managed.cli].quotaScope : candidate.managed?.quotaScope;
|
|
112
|
+
const cooldown = Object.entries(snapshot.cooldowns ?? {}).filter(([key]) => !quota || key === `quota/${encodeURIComponent(quota)}`)
|
|
113
|
+
.reduce((latest, [, value]) => Math.max(latest, Date.parse(value.until) || 0), 0);
|
|
114
|
+
return Math.max(retry, cooldown);
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=shared-host-admission-checks.js.map
|