@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,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker AIWG context providers and launch-time bootstrap ensure (#3685).
|
|
3
|
+
*
|
|
4
|
+
* Orchestration must automatically `aiwg use` the providers configured for
|
|
5
|
+
* it, so any new worker process starts with the AIWG bootstrap in place.
|
|
6
|
+
* This module owns the config-side half: resolving WHICH providers
|
|
7
|
+
* (config `providers` plus every CLI configured under
|
|
8
|
+
* `parallelism.clis` / `parallelism.groups.*.members`, mapped through the
|
|
9
|
+
* provider registry) and checking/ensuring that bootstrap inside exactly
|
|
10
|
+
* one worker workdir (never outside it).
|
|
11
|
+
*
|
|
12
|
+
* The tools/ half (`tools/ralph-external/lib/worker-context.mjs`) loads the
|
|
13
|
+
* config through `readAiwgConfig` and injects it here; direct callers may
|
|
14
|
+
* pass an already-loaded config object instead.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { getProviderArtifactPathStrings, getProviderDefinition, normalizeProviderDefinitionId, } from '../providers/provider-definitions.js';
|
|
20
|
+
import { emptyConfig, readAiwgConfig, writeAiwgConfig, } from './aiwg-config.js';
|
|
21
|
+
/**
|
|
22
|
+
* Typed refusal when a worker's AIWG bootstrap cannot be ensured.
|
|
23
|
+
* `remedy` names the operator action that unblocks the launch.
|
|
24
|
+
*/
|
|
25
|
+
export class WorkerContextBootstrapError extends Error {
|
|
26
|
+
code = 'OG_WORKER_CONTEXT_UNAVAILABLE';
|
|
27
|
+
remedy;
|
|
28
|
+
provider;
|
|
29
|
+
workdir;
|
|
30
|
+
constructor(message, remedy, opts) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'WorkerContextBootstrapError';
|
|
33
|
+
this.remedy = remedy;
|
|
34
|
+
if (opts?.provider !== undefined)
|
|
35
|
+
this.provider = opts.provider;
|
|
36
|
+
if (opts?.workdir !== undefined)
|
|
37
|
+
this.workdir = opts.workdir;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Explicit provider lists must be non-empty arrays of deployable ids. */
|
|
41
|
+
export function validateExplicitWorkerContextProviders(providers) {
|
|
42
|
+
if (!Array.isArray(providers) || providers.length === 0
|
|
43
|
+
|| providers.some(p => typeof p !== 'string' || p.length === 0 || p.includes('\0') || p.includes(','))) {
|
|
44
|
+
throw new WorkerContextBootstrapError('worker context providers must be a non-empty array of provider ids', 'pass an explicit non-empty provider list, or configure providers plus orchestration CLIs in .aiwg/aiwg.config');
|
|
45
|
+
}
|
|
46
|
+
return [...new Set(providers)];
|
|
47
|
+
}
|
|
48
|
+
const ADAPTER_REFERENCE = /^(providers|workerAdapters):([a-z][a-z0-9-]{0,62})$/;
|
|
49
|
+
/**
|
|
50
|
+
* Map one orchestration CLI id to its AIWG provider id.
|
|
51
|
+
* A CLI without an explicit `parallelism.clis` entry defaults to
|
|
52
|
+
* `providers:<cli>` (the same default `resolveWorkerGroupInstallation`
|
|
53
|
+
* applies). Non-provider adapters (e.g. `workerAdapters:local-http`) and
|
|
54
|
+
* ids the registry does not know are reported as skipped, never guessed.
|
|
55
|
+
*/
|
|
56
|
+
export function mapCliToProviderId(cli, parallelism) {
|
|
57
|
+
if (typeof cli !== 'string' || cli.length === 0) {
|
|
58
|
+
return { skipped: `empty CLI reference` };
|
|
59
|
+
}
|
|
60
|
+
const raw = typeof parallelism?.clis?.[cli]?.adapter === 'string'
|
|
61
|
+
&& parallelism.clis[cli].adapter.length > 0
|
|
62
|
+
? parallelism.clis[cli].adapter
|
|
63
|
+
: `providers:${cli}`;
|
|
64
|
+
const match = ADAPTER_REFERENCE.exec(raw);
|
|
65
|
+
if (!match || match[1] !== 'providers') {
|
|
66
|
+
return { skipped: `clis/${cli}: ${raw}` };
|
|
67
|
+
}
|
|
68
|
+
const provider = normalizeProviderDefinitionId(match[2]);
|
|
69
|
+
if (!provider)
|
|
70
|
+
return { skipped: `clis/${cli}: ${raw} (unknown provider)` };
|
|
71
|
+
return { provider };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the full worker-context provider set: the union of the config's
|
|
75
|
+
* own `providers` and every CLI configured for orchestration
|
|
76
|
+
* (`parallelism.clis` keys/adapters and the `cli` of every
|
|
77
|
+
* `parallelism.groups.*.members[]`), mapped to AIWG provider ids through
|
|
78
|
+
* the provider registry. Non-provider adapters are skipped and reported.
|
|
79
|
+
* Fails closed when the union is empty.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveWorkerContextProvidersFromConfig(config) {
|
|
82
|
+
const providers = [];
|
|
83
|
+
const skipped = [];
|
|
84
|
+
const add = (id) => {
|
|
85
|
+
if (!providers.includes(id))
|
|
86
|
+
providers.push(id);
|
|
87
|
+
};
|
|
88
|
+
const skip = (entry) => {
|
|
89
|
+
if (!skipped.includes(entry))
|
|
90
|
+
skipped.push(entry);
|
|
91
|
+
};
|
|
92
|
+
const configured = config?.providers;
|
|
93
|
+
if (Array.isArray(configured)) {
|
|
94
|
+
for (const entry of configured) {
|
|
95
|
+
if (typeof entry !== 'string' || entry.length === 0)
|
|
96
|
+
continue;
|
|
97
|
+
// Configured providers deploy verbatim when the registry does not
|
|
98
|
+
// know them: project-local provider adapters resolve at deploy time
|
|
99
|
+
// (see the `use` handler's project-local resolution), so an unknown
|
|
100
|
+
// id here is the deployer's decision, not this resolver's.
|
|
101
|
+
add(normalizeProviderDefinitionId(entry) ?? entry);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const parallelism = config?.parallelism;
|
|
105
|
+
for (const cli of Object.keys(parallelism?.clis ?? {})) {
|
|
106
|
+
const mapped = mapCliToProviderId(cli, parallelism);
|
|
107
|
+
if ('provider' in mapped)
|
|
108
|
+
add(mapped.provider);
|
|
109
|
+
else
|
|
110
|
+
skip(mapped.skipped);
|
|
111
|
+
}
|
|
112
|
+
for (const [groupId, group] of Object.entries(parallelism?.groups ?? {})) {
|
|
113
|
+
for (const member of group?.members ?? []) {
|
|
114
|
+
const cli = member?.cli;
|
|
115
|
+
const mapped = mapCliToProviderId(cli, parallelism);
|
|
116
|
+
if ('provider' in mapped)
|
|
117
|
+
add(mapped.provider);
|
|
118
|
+
else
|
|
119
|
+
skip(typeof cli === 'string' && cli ? `groups/${groupId}/members cli '${cli}': ${mapped.skipped}` : `groups/${groupId}/members: ${mapped.skipped}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (providers.length === 0) {
|
|
123
|
+
throw new WorkerContextBootstrapError('worker context deployment needs configured providers: the union of config providers and orchestration CLIs is empty'
|
|
124
|
+
+ (skipped.length > 0 ? ` (skipped: ${skipped.join('; ')})` : ''), 'configure providers and/or parallelism clis/groups in .aiwg/aiwg.config, or pass an explicit provider list');
|
|
125
|
+
}
|
|
126
|
+
return { providers, skipped };
|
|
127
|
+
}
|
|
128
|
+
/** AIWG-managed rule-section markers written into a deployed AGENTS.md. */
|
|
129
|
+
const WORKER_CONTEXT_RULE_MARKER = /<!--\s*AIWG:[A-Za-z0-9_-]+:(start|end)\s*-->/;
|
|
130
|
+
/**
|
|
131
|
+
* Check that a worker workdir already carries one provider's bootstrap:
|
|
132
|
+
* the kernel artifact dirs (`use all` writes kernel skills, rules,
|
|
133
|
+
* behaviors — never the full agent/command surface) and, for providers
|
|
134
|
+
* whose contract targets AGENTS.md, the AIWG rule sections.
|
|
135
|
+
*/
|
|
136
|
+
export function workerContextBootstrapStatus({ workdir, provider }) {
|
|
137
|
+
const missing = [];
|
|
138
|
+
const definition = getProviderDefinition(provider);
|
|
139
|
+
if (!definition) {
|
|
140
|
+
return { present: false, missing: [`unknown provider '${provider}'`] };
|
|
141
|
+
}
|
|
142
|
+
const paths = getProviderArtifactPathStrings(provider);
|
|
143
|
+
const kernelDirs = [
|
|
144
|
+
definition.paths.kernelSkills,
|
|
145
|
+
paths?.rules ?? null,
|
|
146
|
+
paths?.behaviors ?? null,
|
|
147
|
+
].filter((dir) => typeof dir === 'string' && dir.length > 0);
|
|
148
|
+
const requiredDirs = kernelDirs.length > 0
|
|
149
|
+
? kernelDirs
|
|
150
|
+
: Object.values(paths ?? {}).filter((dir) => typeof dir === 'string' && dir.length > 0);
|
|
151
|
+
for (const dir of [...new Set(requiredDirs)]) {
|
|
152
|
+
const absolute = path.isAbsolute(dir) ? dir : path.join(workdir, dir);
|
|
153
|
+
let entries = null;
|
|
154
|
+
try {
|
|
155
|
+
entries = readdirSync(absolute);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
entries = null;
|
|
159
|
+
}
|
|
160
|
+
if (!entries || entries.length === 0)
|
|
161
|
+
missing.push(`provider dir '${dir}'`);
|
|
162
|
+
}
|
|
163
|
+
if ((definition.context?.bootstrapTargets ?? []).includes('AGENTS.md')) {
|
|
164
|
+
let content = null;
|
|
165
|
+
try {
|
|
166
|
+
content = readFileSync(path.join(workdir, 'AGENTS.md'), 'utf8');
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
content = null;
|
|
170
|
+
}
|
|
171
|
+
if (!content || !WORKER_CONTEXT_RULE_MARKER.test(content)) {
|
|
172
|
+
missing.push('AGENTS.md rule sections');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { present: missing.length === 0, missing };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Materialize the union provider set into the worker workdir's own config
|
|
179
|
+
* through the existing loader/writer (never ad-hoc JSON). Existing entries
|
|
180
|
+
* keep their order; union members absent from the worktree base are
|
|
181
|
+
* appended. A missing config is created minimal. An already-current config
|
|
182
|
+
* is left untouched so the deploy below stays the only writer.
|
|
183
|
+
*/
|
|
184
|
+
export async function syncWorkerContextProviders(options) {
|
|
185
|
+
const { workdir } = options;
|
|
186
|
+
if (typeof workdir !== 'string' || !path.isAbsolute(workdir)) {
|
|
187
|
+
throw new WorkerContextBootstrapError(`worker context sync needs an absolute workdir, got ${JSON.stringify(workdir)}`, 'sync worker context providers only into the worker workdir', { workdir: typeof workdir === 'string' ? workdir : undefined });
|
|
188
|
+
}
|
|
189
|
+
const providers = validateExplicitWorkerContextProviders(options.providers);
|
|
190
|
+
const read = options.readConfig ?? ((dir) => readAiwgConfig(dir));
|
|
191
|
+
const existing = await read(workdir);
|
|
192
|
+
const current = Array.isArray(existing?.providers)
|
|
193
|
+
? existing.providers.filter((entry) => typeof entry === 'string')
|
|
194
|
+
: [];
|
|
195
|
+
const merged = [...current];
|
|
196
|
+
for (const provider of providers) {
|
|
197
|
+
if (!merged.includes(provider))
|
|
198
|
+
merged.push(provider);
|
|
199
|
+
}
|
|
200
|
+
const unchanged = merged.length === current.length && merged.every((entry, index) => entry === current[index]);
|
|
201
|
+
if (unchanged)
|
|
202
|
+
return { providers: merged, updated: false };
|
|
203
|
+
const base = existing !== null && typeof existing === 'object' && 'version' in existing
|
|
204
|
+
? existing
|
|
205
|
+
: {};
|
|
206
|
+
const next = { ...emptyConfig(merged), ...base, providers: merged };
|
|
207
|
+
const write = options.writeConfig ?? ((dir, config) => writeAiwgConfig(dir, config));
|
|
208
|
+
await write(workdir, next);
|
|
209
|
+
return { providers: merged, updated: true };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Canonical one-pass deploy argv: a single `aiwg use all` invocation
|
|
213
|
+
* covering every listed provider. Never one deploy per provider:
|
|
214
|
+
* per-provider invocations rewrite AGENTS.md piecemeal and clobber each
|
|
215
|
+
* other's rule sections, while one pass keeps them all. No `--providers`
|
|
216
|
+
* flag: it feeds only the global bootstrap and the orchestrated
|
|
217
|
+
* verification report — `executeCore` resolves project-scope providers
|
|
218
|
+
* from config (`src/cli/handlers/use.ts`), ignoring both the flag and any
|
|
219
|
+
* explicitly resolved list. The supported project-scope mechanism is the
|
|
220
|
+
* configured-provider path: `syncWorkerContextProviders` first, then this
|
|
221
|
+
* single pass reads the union back from the worktree config.
|
|
222
|
+
*/
|
|
223
|
+
export function buildWorkerContextDeployArgs({ providers, target }) {
|
|
224
|
+
// Fail fast on a hostile list; the synced worktree config carries the set.
|
|
225
|
+
validateExplicitWorkerContextProviders(providers);
|
|
226
|
+
return ['use', 'all', '--target', target, '--yes'];
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Ensure a worker workdir carries its CLI's AIWG bootstrap before launch.
|
|
230
|
+
* Present: no-op. Missing: sync the union set into the workdir config,
|
|
231
|
+
* then exactly one `aiwg use all` pass into the workdir (never anywhere
|
|
232
|
+
* else), then re-verify. Impossible (no binary, failed deploy, still
|
|
233
|
+
* missing afterwards): throw `WorkerContextBootstrapError` naming the
|
|
234
|
+
* remedy — the caller refuses the launch rather than starting a
|
|
235
|
+
* context-free worker.
|
|
236
|
+
*/
|
|
237
|
+
export async function ensureWorkerContextBootstrap(options) {
|
|
238
|
+
const { workdir, provider, aiwgBin = process.env.AIWG_BIN ?? 'aiwg', spawn = spawnSync, timeoutMs = 300000, statusFn = workerContextBootstrapStatus, } = options;
|
|
239
|
+
if (typeof workdir !== 'string' || !path.isAbsolute(workdir) || !existsSync(workdir)) {
|
|
240
|
+
throw new WorkerContextBootstrapError(`worker context bootstrap needs an existing absolute workdir, got ${JSON.stringify(workdir)}`, 'launch the worker with its resolved absolute workdir', { provider, workdir: typeof workdir === 'string' ? workdir : undefined });
|
|
241
|
+
}
|
|
242
|
+
const status = statusFn({ workdir, provider });
|
|
243
|
+
if (status.present)
|
|
244
|
+
return { ensured: false, providers: [provider], skippedBridges: [] };
|
|
245
|
+
const providers = options.providers && options.providers.length > 0
|
|
246
|
+
? validateExplicitWorkerContextProviders(options.providers)
|
|
247
|
+
: [provider];
|
|
248
|
+
// The configured-provider path: the single pass below resolves its set
|
|
249
|
+
// from the workdir config, so the union must be materialized there
|
|
250
|
+
// first — `--providers` never reaches project-scope deploys.
|
|
251
|
+
const sync = options.syncFn ?? syncWorkerContextProviders;
|
|
252
|
+
let synced;
|
|
253
|
+
try {
|
|
254
|
+
synced = await sync({ workdir, providers });
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error instanceof WorkerContextBootstrapError)
|
|
258
|
+
throw error;
|
|
259
|
+
throw new WorkerContextBootstrapError(`worker context sync failed: ${error instanceof Error ? error.message : String(error)}`, `write the union providers into ${path.join(workdir, '.aiwg', 'aiwg.config')} (or run 'aiwg use all --target ${workdir} --yes' manually) before launch`, { provider, workdir });
|
|
260
|
+
}
|
|
261
|
+
const argv = buildWorkerContextDeployArgs({ providers: synced.providers, target: workdir });
|
|
262
|
+
const deploy = options.deployFn ?? (({ worktreePath, providers: deployProviders, aiwgBin: bin }) => {
|
|
263
|
+
// worktreePath IS the worker workdir by construction: the only
|
|
264
|
+
// `--target` ever passed is `workdir` above, and the spawn cwd is the
|
|
265
|
+
// same directory, so no deploy write can land outside it.
|
|
266
|
+
if (path.resolve(worktreePath) !== path.resolve(workdir)) {
|
|
267
|
+
throw new WorkerContextBootstrapError(`refusing worker context deploy outside the worker workdir (${worktreePath} !== ${workdir})`, 'deploy worker context only into the worker workdir', { provider, workdir });
|
|
268
|
+
}
|
|
269
|
+
const result = spawn(bin, argv, { cwd: workdir, encoding: 'utf8', timeout: timeoutMs });
|
|
270
|
+
if (result.error) {
|
|
271
|
+
throw new WorkerContextBootstrapError(`worker context deploy failed to run (${result.error?.message});`
|
|
272
|
+
+ ` missing in ${workdir}: ${status.missing.join('; ')}`, `install the aiwg CLI on PATH or set AIWG_BIN to its location (tried '${bin}'),`
|
|
273
|
+
+ ` or run 'aiwg ${argv.join(' ')}' manually before launch`, { provider, workdir });
|
|
274
|
+
}
|
|
275
|
+
if (result.status !== 0) {
|
|
276
|
+
throw new WorkerContextBootstrapError(`worker context deploy exited ${result.status}: ${String(result.stderr ?? '').trim().slice(0, 2000)};`
|
|
277
|
+
+ ` missing in ${workdir}: ${status.missing.join('; ')}`, `run 'aiwg ${argv.join(' ')}' manually and resolve the reported failure before launch`, { provider, workdir });
|
|
278
|
+
}
|
|
279
|
+
return { providers: [...deployProviders] };
|
|
280
|
+
});
|
|
281
|
+
let deployed;
|
|
282
|
+
try {
|
|
283
|
+
deployed = await deploy({ worktreePath: workdir, providers: synced.providers, aiwgBin });
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (error instanceof WorkerContextBootstrapError)
|
|
287
|
+
throw error;
|
|
288
|
+
throw new WorkerContextBootstrapError(`worker context deploy failed: ${error instanceof Error ? error.message : String(error)};`
|
|
289
|
+
+ ` missing in ${workdir}: ${status.missing.join('; ')}`, `run 'aiwg ${argv.join(' ')}' manually and resolve the reported failure before launch`, { provider, workdir });
|
|
290
|
+
}
|
|
291
|
+
const after = statusFn({ workdir, provider });
|
|
292
|
+
if (!after.present) {
|
|
293
|
+
throw new WorkerContextBootstrapError(`worker context deploy did not produce the bootstrap: still missing in ${workdir}: ${after.missing.join('; ')}`, `run 'aiwg ${argv.join(' ')}' manually and verify the provider bootstrap before launch`, { provider, workdir });
|
|
294
|
+
}
|
|
295
|
+
// Keep deploy-touched tracked bridges out of the worker diff. A workdir
|
|
296
|
+
// outside any git repository has no tracked files; skip silently there.
|
|
297
|
+
let skippedBridges = [];
|
|
298
|
+
if (options.skipWorktreeFn) {
|
|
299
|
+
skippedBridges = options.skipWorktreeFn({ workdir });
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
try {
|
|
303
|
+
skippedBridges = flagDeployDirtiedSkipWorktree({ workdir, spawn, timeoutMs });
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
skippedBridges = [];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { ensured: true, providers: [...deployed.providers], skippedBridges };
|
|
310
|
+
}
|
|
311
|
+
/** Tracked files the deploy modified, hidden from the worker diff. */
|
|
312
|
+
export function flagDeployDirtiedSkipWorktree({ workdir, spawn = spawnSync, timeoutMs = 30000 }) {
|
|
313
|
+
const status = spawn('git', ['status', '--porcelain=v1', '--untracked-files=no'], { cwd: workdir, encoding: 'utf8', timeout: timeoutMs });
|
|
314
|
+
if (status.error)
|
|
315
|
+
throw new Error(`git status failed: ${status.error?.message}`);
|
|
316
|
+
if (status.status !== 0)
|
|
317
|
+
throw new Error(`git status exited ${status.status}`);
|
|
318
|
+
const files = [];
|
|
319
|
+
for (const line of String(status.stdout ?? '').split('\n')) {
|
|
320
|
+
if (!line.trim())
|
|
321
|
+
continue;
|
|
322
|
+
const raw = line.slice(3);
|
|
323
|
+
const arrow = raw.indexOf(' -> ');
|
|
324
|
+
const rel = arrow >= 0 ? raw.slice(arrow + 4) : raw;
|
|
325
|
+
if (!rel || rel.startsWith('"'))
|
|
326
|
+
throw new Error(`worker context deploy dirtied an unflaggeable path: ${line}`);
|
|
327
|
+
files.push(rel);
|
|
328
|
+
}
|
|
329
|
+
if (files.length > 0) {
|
|
330
|
+
const flag = spawn('git', ['update-index', '--skip-worktree', '--', ...files], { cwd: workdir, encoding: 'utf8', timeout: timeoutMs });
|
|
331
|
+
if (flag.error || flag.status !== 0) {
|
|
332
|
+
throw new Error(`git update-index --skip-worktree failed: ${flag.error?.message ?? String(flag.stderr ?? '').trim()}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return files;
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=worker-context.js.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { mergeWorkerSelection } from '../models/worker-selection-policy.js';
|
|
2
|
+
import { workerConfigDigest } from './worker-config-digest.js';
|
|
3
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
const escape = (value) => value.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
5
|
+
export function expandWorkerGroupTemplates(input, source = 'parallelism') {
|
|
6
|
+
const origins = {};
|
|
7
|
+
const errors = [];
|
|
8
|
+
if (!object(input))
|
|
9
|
+
return { parallelism: input, origins, errors };
|
|
10
|
+
const parallelism = structuredClone(input);
|
|
11
|
+
const templates = input.groupTemplates ?? {};
|
|
12
|
+
if (parallelism.groups)
|
|
13
|
+
parallelism.groups = Object.assign(Object.create(null), parallelism.groups);
|
|
14
|
+
const fail = (pointer, message) => { errors.push(`OG_POLICY_RELAXATION ${pointer}: ${message}`); };
|
|
15
|
+
const merge = (base, own, pointer) => {
|
|
16
|
+
const result = { ...base, ...own, execution: { ...base.execution, ...own.execution } };
|
|
17
|
+
delete result.template;
|
|
18
|
+
if (base.maxParallel !== undefined && own.maxParallel !== undefined) {
|
|
19
|
+
if (own.maxParallel > base.maxParallel)
|
|
20
|
+
fail(pointer, 'Template concurrency ceiling cannot be widened');
|
|
21
|
+
result.maxParallel = Math.min(base.maxParallel, own.maxParallel);
|
|
22
|
+
}
|
|
23
|
+
if (base.efforts && own.efforts) {
|
|
24
|
+
if (own.efforts.some(effort => !base.efforts.includes(effort)))
|
|
25
|
+
fail(pointer, 'Template effort ceiling cannot be widened');
|
|
26
|
+
result.efforts = own.efforts.filter(effort => base.efforts.includes(effort));
|
|
27
|
+
}
|
|
28
|
+
if (base.members && own.members) {
|
|
29
|
+
for (const member of own.members) {
|
|
30
|
+
const prior = base.members.find(candidate => candidate.id === member.id);
|
|
31
|
+
if (!prior || prior.cli !== member.cli || prior.model !== member.model
|
|
32
|
+
|| member.roles.some(role => !prior.roles.includes(role)) || prior.enabled === false && member.enabled !== false) {
|
|
33
|
+
fail(pointer, 'Template member ceiling cannot be widened or rebound');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
result.requiredRoles = [...new Set([...(base.requiredRoles ?? []), ...(own.requiredRoles ?? [])])];
|
|
38
|
+
if (base.dataPolicy || own.dataPolicy) {
|
|
39
|
+
const merged = mergeWorkerSelection([
|
|
40
|
+
{ source, authority: 'project', digest: workerConfigDigest(base), policy: base.dataPolicy ? { dataPolicy: base.dataPolicy } : undefined },
|
|
41
|
+
{ source, authority: 'project', digest: workerConfigDigest(own), policy: own.dataPolicy ? { dataPolicy: own.dataPolicy } : undefined },
|
|
42
|
+
]);
|
|
43
|
+
for (const diagnostic of merged.diagnostics)
|
|
44
|
+
fail(`${pointer}/dataPolicy`, diagnostic.message);
|
|
45
|
+
result.dataPolicy = merged.policy.dataPolicy;
|
|
46
|
+
}
|
|
47
|
+
for (const key of Object.keys(base.execution ?? {})) {
|
|
48
|
+
const before = base.execution[key];
|
|
49
|
+
const after = own.execution?.[key];
|
|
50
|
+
// Execution constraints have no universally safe cross-profile ordering.
|
|
51
|
+
if (after !== undefined && after !== before)
|
|
52
|
+
fail(`${pointer}/execution/${key}`, 'Inherited execution control cannot change');
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
const shape = (value, pointer) => {
|
|
57
|
+
if (!object(value) || value.template !== undefined && typeof value.template !== 'string'
|
|
58
|
+
|| value.members !== undefined && (!Array.isArray(value.members) || value.members.some((member) => !object(member) || !Array.isArray(member.roles)))
|
|
59
|
+
|| value.efforts !== undefined && !Array.isArray(value.efforts)
|
|
60
|
+
|| value.execution !== undefined && !object(value.execution)) {
|
|
61
|
+
errors.push(`OG_CONFIG_REFERENCE ${pointer}: Invalid group template shape`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
const expanded = new Map();
|
|
67
|
+
const depths = new Map();
|
|
68
|
+
const failed = new Set();
|
|
69
|
+
const expand = (name) => {
|
|
70
|
+
const pending = [];
|
|
71
|
+
const active = new Set();
|
|
72
|
+
let next = name;
|
|
73
|
+
// Collect uncached ancestors iteratively, then expand from the root down.
|
|
74
|
+
while (!expanded.has(next) && !failed.has(next)) {
|
|
75
|
+
const pointer = `/parallelism/groupTemplates/${escape(next)}`;
|
|
76
|
+
if (active.has(next)) {
|
|
77
|
+
errors.push(`OG_CONFIG_REFERENCE ${pointer}: Cyclic or excessive template nesting`);
|
|
78
|
+
failed.add(next);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (!object(templates) || !Object.hasOwn(templates, next)) {
|
|
82
|
+
errors.push(`OG_CONFIG_REFERENCE ${pointer}: Unknown template`);
|
|
83
|
+
failed.add(next);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
const own = templates[next];
|
|
87
|
+
if (!shape(own, pointer)) {
|
|
88
|
+
failed.add(next);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
active.add(next);
|
|
92
|
+
pending.push(next);
|
|
93
|
+
if (!own.template)
|
|
94
|
+
break;
|
|
95
|
+
next = own.template;
|
|
96
|
+
}
|
|
97
|
+
for (const current of pending.reverse()) {
|
|
98
|
+
const own = templates[current];
|
|
99
|
+
if (failed.has(current) || own.template && failed.has(own.template)) {
|
|
100
|
+
failed.add(current);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const depth = (own.template ? depths.get(own.template) : 0) + 1;
|
|
104
|
+
if (depth > 64) {
|
|
105
|
+
errors.push(`OG_CONFIG_REFERENCE /parallelism/groupTemplates/${escape(current)}: Cyclic or excessive template nesting`);
|
|
106
|
+
failed.add(current);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const parent = own.template ? expanded.get(own.template) : {};
|
|
110
|
+
expanded.set(current, merge(parent, own, `/parallelism/groupTemplates/${escape(current)}`));
|
|
111
|
+
depths.set(current, depth);
|
|
112
|
+
}
|
|
113
|
+
return expanded.get(name) ?? {};
|
|
114
|
+
};
|
|
115
|
+
if (object(templates))
|
|
116
|
+
for (const name of Object.keys(templates))
|
|
117
|
+
expand(name);
|
|
118
|
+
if (object(input.groups)) {
|
|
119
|
+
for (const [id, own] of Object.entries(input.groups)) {
|
|
120
|
+
const pointer = `/parallelism/groups/${escape(id)}`;
|
|
121
|
+
if (!shape(own, pointer))
|
|
122
|
+
continue;
|
|
123
|
+
const base = own.template ? expand(own.template) : {};
|
|
124
|
+
const result = merge(base, own, pointer);
|
|
125
|
+
// Existing shape validation requires members on every fully expanded group.
|
|
126
|
+
parallelism.groups[id] = result;
|
|
127
|
+
if (own.template) {
|
|
128
|
+
const names = [];
|
|
129
|
+
for (let name = own.template; name && expanded.has(name); name = templates[name].template)
|
|
130
|
+
names.push(name);
|
|
131
|
+
names.reverse();
|
|
132
|
+
origins[`${pointer}/template`] = names.map(name => ({ source, authority: 'project',
|
|
133
|
+
digest: workerConfigDigest(templates[name]), pointer: `/parallelism/groupTemplates/${escape(name)}`,
|
|
134
|
+
contribution: structuredClone(templates[name]),
|
|
135
|
+
}));
|
|
136
|
+
origins[`${pointer}/template`].push({ source, authority: 'project', digest: workerConfigDigest(own), pointer, contribution: structuredClone(own) });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Keep raw declarations for stable provenance; the validator checks them separately.
|
|
141
|
+
return { parallelism, origins, errors };
|
|
142
|
+
}
|
|
143
|
+
/** Group data declarations are further restrictions on the already merged model policy. */
|
|
144
|
+
export function mergeGroupWorkerPolicy(policy, group) {
|
|
145
|
+
return mergeWorkerSelection([
|
|
146
|
+
{ source: 'models:effective', authority: 'project', digest: workerConfigDigest(policy), policy },
|
|
147
|
+
{ source: 'parallelism:group', authority: 'project', digest: workerConfigDigest(group), policy: group.dataPolicy ? { dataPolicy: group.dataPolicy } : undefined },
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=worker-group-templates.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Reports guidance refresh steps after direct parallelism edits.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { normalizeProviderDefinitionId } from '../providers/provider-definitions.js';
|
|
5
|
+
export function warnStaleParallelism(cfg, projectDir, key, before) {
|
|
6
|
+
if (key !== 'parallelism' && !key.startsWith('parallelism.'))
|
|
7
|
+
return;
|
|
8
|
+
if (isDeepStrictEqual(before, getDottedPath(cfg, key)))
|
|
9
|
+
return;
|
|
10
|
+
const configured = (cfg.providers ?? []).map(normalizeProviderDefinitionId).filter((id) => !!id);
|
|
11
|
+
const recorded = Object.values(cfg.installed ?? {}).flatMap(entry => Object.keys(entry.deployedTo ?? {}))
|
|
12
|
+
.map(normalizeProviderDefinitionId).filter((id) => !!id).sort();
|
|
13
|
+
const providers = [...new Set([...configured, ...recorded])];
|
|
14
|
+
const targets = providers.length ? providers : ['<provider>'];
|
|
15
|
+
const lines = [
|
|
16
|
+
`WARNING: ${key} changed project configuration; generated workspace/provider guidance may now be stale.`,
|
|
17
|
+
`From project root ${path.resolve(projectDir)}, preview and refresh every configured or recorded provider context:`,
|
|
18
|
+
...targets.flatMap(provider => [
|
|
19
|
+
` aiwg regenerate --workspace --provider ${provider} --dry-run`,
|
|
20
|
+
` aiwg regenerate --workspace --provider ${provider}`,
|
|
21
|
+
]),
|
|
22
|
+
' aiwg workspace-context doctor',
|
|
23
|
+
`Shared parallelism defaults and labels use primary provider '${cfg.providers?.[0] ?? 'unknown'}' (providers[0]); --provider selects the adapter/context target.`,
|
|
24
|
+
];
|
|
25
|
+
console.error(lines.join('\n'));
|
|
26
|
+
}
|
|
27
|
+
function getDottedPath(value, key) { return key.split('.').reduce((node, part) => node?.[part], value); }
|
|
28
|
+
//# sourceMappingURL=worker-guidance-warning.js.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Publishes complete owner identities atomically and fences dead-owner takeover by generation.
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { hostname } from 'node:os';
|
|
4
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { mkdir, writeFile, symlink, readlink, unlink, rm, readdir } from 'node:fs/promises';
|
|
6
|
+
import { readFileSync, readlinkSync } from 'node:fs';
|
|
7
|
+
import { projectControlPath } from './project-artifacts.js';
|
|
8
|
+
export const workerWriteLock = (project) => projectControlPath(project, 'aiwg.config.apply-lock');
|
|
9
|
+
const missing = (error) => error.code === 'ENOENT';
|
|
10
|
+
function processStart(pid) {
|
|
11
|
+
try {
|
|
12
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
13
|
+
return `${readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim()}:${stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19]}`;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function identity() {
|
|
20
|
+
return { generation: randomUUID(), host: hostname(), pid: process.pid, start: processStart(process.pid) };
|
|
21
|
+
}
|
|
22
|
+
function provablyDead(owner) {
|
|
23
|
+
if (owner.released)
|
|
24
|
+
return true; // The operation ended and explicitly relinquished exclusion.
|
|
25
|
+
if (owner.host !== hostname() || !Number.isSafeInteger(owner.pid) || owner.pid <= 0)
|
|
26
|
+
return false;
|
|
27
|
+
try {
|
|
28
|
+
process.kill(owner.pid, 0);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return error.code === 'ESRCH';
|
|
32
|
+
}
|
|
33
|
+
const start = processStart(owner.pid);
|
|
34
|
+
return typeof owner.start === 'string' && owner.start.length > 0 && start !== null && owner.start !== start;
|
|
35
|
+
}
|
|
36
|
+
function latest(directory) {
|
|
37
|
+
let owner = JSON.parse(readFileSync(join(directory, 'owner.json'), 'utf8'));
|
|
38
|
+
// Immutable successor links are a compare-and-swap: only one contender can claim a generation.
|
|
39
|
+
for (;;) {
|
|
40
|
+
if (!/^[a-f0-9-]{36}$/.test(owner.generation))
|
|
41
|
+
throw new Error('Invalid worker lock generation');
|
|
42
|
+
try {
|
|
43
|
+
owner = JSON.parse(readlinkSync(join(directory, `next-${owner.generation}`)));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (missing(error))
|
|
47
|
+
return owner;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function sweepOrphanedOwners(path) {
|
|
53
|
+
const parent = dirname(path);
|
|
54
|
+
const prefix = `${basename(path)}.`;
|
|
55
|
+
for (const entry of await readdir(parent, { withFileTypes: true })) {
|
|
56
|
+
if (!entry.isDirectory() || !entry.name.startsWith(prefix) || !/^[a-f0-9-]{36}$/.test(entry.name.slice(prefix.length)))
|
|
57
|
+
continue;
|
|
58
|
+
const directory = join(parent, entry.name);
|
|
59
|
+
let owner;
|
|
60
|
+
try {
|
|
61
|
+
owner = latest(directory);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
continue;
|
|
65
|
+
} // Incomplete or unreadable identities cannot prove death.
|
|
66
|
+
if (!provablyDead(owner))
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
if (resolve(parent, await readlink(path)) === directory)
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (!missing(error))
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
await rm(directory, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function lease(path, directory, owner) {
|
|
80
|
+
const assertOwned = () => {
|
|
81
|
+
try {
|
|
82
|
+
if (readlinkSync(path) === basename(directory) && latest(directory).generation === owner.generation)
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
catch { /* Missing or replaced ownership must never authorize another write. */ }
|
|
86
|
+
throw new Error(`Worker configuration lock ownership lost (${owner.generation}); dispatch remains closed at ${path}`);
|
|
87
|
+
};
|
|
88
|
+
return { path, generation: owner.generation, assertOwned, release: async (keepBlocked = false) => {
|
|
89
|
+
assertOwned();
|
|
90
|
+
if (keepBlocked) {
|
|
91
|
+
// A caught failure can be retried even while this embedding process stays alive.
|
|
92
|
+
await symlink(JSON.stringify({ ...identity(), released: true }), join(directory, `next-${owner.generation}`));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// A live owner cannot be reclaimed; unlink only while we still own this exact generation.
|
|
96
|
+
await unlink(path);
|
|
97
|
+
await rm(directory, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
} };
|
|
100
|
+
}
|
|
101
|
+
export async function acquireWorkerWriteLock(project, block = false, proposalDigest) {
|
|
102
|
+
const path = workerWriteLock(project);
|
|
103
|
+
const owner = { ...identity(), ...(proposalDigest ? { proposalDigest } : {}) };
|
|
104
|
+
const directory = `${path}.${owner.generation}`;
|
|
105
|
+
await mkdir(dirname(path), { recursive: true });
|
|
106
|
+
await sweepOrphanedOwners(path);
|
|
107
|
+
await mkdir(directory);
|
|
108
|
+
try {
|
|
109
|
+
await writeFile(join(directory, 'owner.json'), JSON.stringify(owner), { mode: 0o600 });
|
|
110
|
+
if (block)
|
|
111
|
+
await writeFile(join(directory, 'blocked'), 'Worker policy apply/recovery in progress.\n', { mode: 0o600 });
|
|
112
|
+
// No visible lock can exist without an owner, even if initialization is interrupted.
|
|
113
|
+
await symlink(basename(directory), path);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
await rm(directory, { recursive: true, force: true });
|
|
117
|
+
if (error.code === 'EEXIST')
|
|
118
|
+
throw new Error(`Worker configuration is locked: ${path}; use authorized recovery after its owner exits`);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
return lease(path, directory, owner);
|
|
122
|
+
}
|
|
123
|
+
export async function reclaimWorkerWriteLock(project) {
|
|
124
|
+
const path = workerWriteLock(project);
|
|
125
|
+
let directory;
|
|
126
|
+
let previous;
|
|
127
|
+
try {
|
|
128
|
+
directory = resolve(dirname(path), await readlink(path));
|
|
129
|
+
previous = latest(directory);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (missing(error))
|
|
133
|
+
throw new Error(`No interrupted apply; nothing to recover at ${path}`);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
if (!provablyDead(previous))
|
|
137
|
+
throw new Error(`Worker configuration is locked by a live or unverifiable owner (${previous.host}:${previous.pid}); ${path}`);
|
|
138
|
+
const owner = identity();
|
|
139
|
+
try {
|
|
140
|
+
await symlink(JSON.stringify(owner), join(directory, `next-${previous.generation}`));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error.code === 'EEXIST')
|
|
144
|
+
throw new Error(`Worker configuration is locked: another recovery claimed generation ${previous.generation}`);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
const result = lease(path, directory, owner);
|
|
148
|
+
result.assertOwned();
|
|
149
|
+
await writeFile(join(directory, 'blocked'), 'Worker policy recovery in progress.\n', { mode: 0o600 });
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=worker-owner-lock.js.map
|