@aiwg/cli 2026.10.10 → 2026.10.12
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 +78 -14
- package/agentic/code/providers/model-catalog.v1.json +62 -12
- package/dist/src/artifacts/binding-target-identity.js +51 -0
- package/dist/src/artifacts/checksum-manifest.js +12 -0
- package/dist/src/artifacts/index-builder.js +11 -13
- package/dist/src/artifacts/query-engine.js +5 -11
- package/dist/src/cli/handlers/doctor-help.js +32 -0
- package/dist/src/cli/handlers/gates.js +19 -18
- package/dist/src/cli/handlers/models-help.js +7 -1
- package/dist/src/cli/handlers/models.js +3 -3
- package/dist/src/cli/handlers/script-runner.js +8 -5
- package/dist/src/cli/handlers/sessions-intake.js +104 -0
- package/dist/src/cli/handlers/sessions-tail.js +172 -0
- package/dist/src/cli/handlers/sessions.js +287 -31
- package/dist/src/cli/handlers/skill-lint.js +10 -3
- package/dist/src/cli/handlers/steward-format.js +100 -0
- package/dist/src/cli/handlers/steward-models.js +167 -0
- package/dist/src/cli/handlers/steward.js +14 -248
- package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
- package/dist/src/cli/handlers/use.js +18 -28
- package/dist/src/cli/handlers/utilities.js +1 -24
- package/dist/src/config/aiwg-config.js +7 -0
- package/dist/src/config/authorization-cli.js +89 -0
- package/dist/src/config/cli-help.js +1 -0
- package/dist/src/config/cli.js +10 -1
- package/dist/src/config/worker-context.js +337 -0
- package/dist/src/decision/context-pruning.js +3 -2
- package/dist/src/decision/ensemble/contract.js +15 -4
- package/dist/src/decision/ensemble-study/artifacts.js +5 -0
- package/dist/src/decision/ensemble-study/score.js +4 -3
- package/dist/src/decision/feature-export.js +3 -9
- package/dist/src/decision/heldout/collector.js +2 -2
- package/dist/src/decision/issue-triage/runtime.js +3 -9
- package/dist/src/decision/qualification/gates.js +2 -1
- package/dist/src/decision/qualification/release.js +2 -3
- package/dist/src/decision/routing/report.js +2 -0
- package/dist/src/decision/sdlc-screening.js +3 -5
- package/dist/src/decision/sensitivity/comparative.js +5 -3
- package/dist/src/gates/driver.js +6 -77
- package/dist/src/gates/evaluate.js +4 -11
- package/dist/src/gates/floors.js +4 -4
- package/dist/src/gates/index.js +3 -3
- package/dist/src/gates/lattice.js +26 -0
- package/dist/src/gates/registry.js +2 -1
- package/dist/src/gates/report.js +19 -7
- package/dist/src/issues/audit-cli.js +120 -0
- package/dist/src/issues/audit-contract.js +64 -0
- package/dist/src/issues/audit-local.js +82 -0
- package/dist/src/issues/audit.js +220 -0
- package/dist/src/lint/rule-activation.js +15 -1
- package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
- package/dist/src/mission-protocol/canonical-codecs.js +270 -0
- package/dist/src/mission-protocol/codecs.js +22 -550
- package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
- package/dist/src/mission-protocol/health-trace-types.js +43 -0
- package/dist/src/mission-protocol/types.js +2 -42
- package/dist/src/mission-protocol/worker-contract-json.js +10 -0
- package/dist/src/mission-protocol/worker-review-gate.js +100 -0
- package/dist/src/mission-protocol/worker-review-types.js +2 -0
- package/dist/src/models/catalog-policy.js +280 -0
- package/dist/src/models/index.js +1 -1
- package/dist/src/models/model-capabilities.v1.json +78 -14
- package/dist/src/models/model-catalog.v1.json +62 -12
- package/dist/src/models/policy-resources.js +28 -0
- package/dist/src/models/provider-policy.js +10 -539
- package/dist/src/models/resolver.js +26 -8
- package/dist/src/models/router.js +15 -2
- package/dist/src/models/tiers.js +13 -0
- package/dist/src/models/types.js +0 -6
- package/dist/src/models/worker-ceilings.js +59 -0
- package/dist/src/models/worker-invocation-policy.js +243 -0
- package/dist/src/models/worker-plan.js +16 -20
- package/dist/src/models/worker-review-lineage.js +55 -0
- package/dist/src/models/worker-selection-policy.js +1 -1
- package/dist/src/models/wrapper-route.js +6 -3
- package/dist/src/policy/authorization.js +30 -1
- package/dist/src/policy/hitl-config.js +63 -0
- package/dist/src/policy/hitl-receipt.js +57 -0
- package/dist/src/policy/hitl-types.js +4 -0
- package/dist/src/policy/hitl-validation.js +39 -0
- package/dist/src/policy/hitl.js +117 -0
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- package/dist/src/rules/activation.d.mts +1 -1
- package/dist/src/rules/activation.mjs +0 -2
- package/dist/src/rules/gate-coverage.js +51 -0
- package/dist/src/rules/gate-references.js +16 -0
- package/dist/src/rules/rule-frontmatter.schema.json +7 -1
- package/dist/src/serve/worker-command-request.js +21 -4
- package/dist/src/serve/worker-invocation-preparation.js +3 -1
- package/dist/src/serve/worker-invocation.js +21 -10
- package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
- package/dist/src/sessions/adapters/grok-build-native.js +269 -0
- package/dist/src/sessions/adapters/grok-build.js +57 -15
- package/dist/src/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
- package/dist/src/sessions/adapters/hermes.js +42 -3
- package/dist/src/sessions/adapters/warp.js +1 -1
- package/dist/src/sessions/batch-import.js +44 -6
- package/dist/src/sessions/contracts.js +20 -1
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/discovery-capability.js +38 -0
- 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 +9 -232
- package/dist/src/sessions/index.js +6 -0
- package/dist/src/sessions/intake-dir.js +71 -0
- package/dist/src/sessions/normalization.js +251 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/tail.js +91 -0
- package/dist/src/sessions/workspace-discovery.js +88 -9
- package/dist/src/skills/agent-skill-sidecar.js +28 -0
- package/dist/src/skills/agent-skills.js +4 -23
- package/dist/src/skills/claude-surface-projection.js +25 -0
- package/dist/src/skills/deployer.js +8 -288
- package/dist/src/skills/deployment-policy.js +114 -0
- package/dist/src/skills/deployment-projection.js +152 -0
- package/dist/src/skills/deployment-types.js +20 -0
- package/dist/src/skills/exporter.js +8 -31
- package/dist/src/skills/provider-extensions.js +41 -0
- package/dist/src/skills/run.js +6 -0
- package/dist/src/skills/types.js +0 -8
- package/dist/src/skills/validator-resources.js +68 -0
- package/dist/src/skills/validator.js +6 -55
- package/dist/src/storage/cli-migrate-options.js +125 -0
- package/dist/src/storage/cli-migrate.js +111 -0
- package/dist/src/storage/cli.js +4 -279
- package/dist/src/storage/migration-adapter.js +79 -0
- package/dist/src/storage/migration-copy.js +108 -0
- package/dist/src/storage/migration-protocol.js +14 -197
- package/dist/src/storage/migration-records.js +186 -0
- package/dist/src/storage/migration-types.js +12 -0
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/tracker/issue-pages.js +110 -0
- package/package.json +7 -1
- package/schemas/mission-v1.schema.json +335 -0
- package/schemas/models/model-catalog.v1.schema.json +19 -1
- package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
- package/schemas/models/model-policy.v1.schema.json +1 -1
- package/tools/agents/providers/provider-paths-staging.mjs +35 -0
- package/tools/agents/providers/rule-binding-targets.mjs +146 -0
- package/tools/agents/providers/rule-delivery.mjs +7 -126
- package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
- package/tools/ralph-external/lib/worker-context.mjs +237 -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 +17 -29
- package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
- 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 +51 -96
- package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
addManagedMarker, contentHash, replaceBoundRulesBlock, deployEmulatedBehaviors, filterAgentFiles,
|
|
13
13
|
} from './base.mjs';
|
|
14
14
|
|
|
15
|
-
const { parseRuleFrontmatter, inferRuleActivation,
|
|
15
|
+
const { parseRuleFrontmatter, inferRuleActivation, readCoreBody, validateCoreSet, CORE_RULES_START, CORE_RULES_END } = await importImpl(import.meta.url, 'rules/activation.mjs');
|
|
16
16
|
const { resolveDeployVersion } = await importImpl(import.meta.url, 'providers/deploy-version.mjs');
|
|
17
17
|
const { trackTemporaryDirectory } = await importImpl(import.meta.url, 'utils/temporary-directory.js');
|
|
18
18
|
const { loadRulePinConfigs, installedRuleCapabilities, resolveRulePins } = await importImpl(import.meta.url, 'rules/pins.mjs');
|
|
@@ -20,6 +20,8 @@ const { guardRulesInlineBudget } = await importImpl(import.meta.url, 'rules/inli
|
|
|
20
20
|
const { userConfigFile } = await importImpl(import.meta.url, 'config/user-config-dir.mjs');
|
|
21
21
|
const { resolveProjectAiwgDir } = await importImpl(import.meta.url, 'config/project-artifacts-runtime.mjs');
|
|
22
22
|
import { writeContextFileSync, CONTEXT_STAGE_ENV } from './context-write.mjs';
|
|
23
|
+
import { captureProviderPathDescriptors, setStagedProviderPath, restoreProviderPaths } from './provider-paths-staging.mjs';
|
|
24
|
+
import { resolveDeliveryRuleBindings } from './rule-binding-targets.mjs';
|
|
23
25
|
import { projectOnDemandReferences, renderRuleKernelIndex } from './rule-presentation.mjs';
|
|
24
26
|
export { renderRuleKernelIndex } from './rule-presentation.mjs';
|
|
25
27
|
const PREPARATION_HOOK = Symbol.for('aiwg.provider-preparation-hook.v1');
|
|
@@ -175,128 +177,7 @@ export async function prepareRuleDelivery(opts) {
|
|
|
175
177
|
// All pins use native kernel projection, including legacy untagged rules.
|
|
176
178
|
rule.tagged = true;
|
|
177
179
|
}
|
|
178
|
-
const bindings =
|
|
179
|
-
const targets = [];
|
|
180
|
-
const targetKeys = new Set();
|
|
181
|
-
if (rules.some(rule => Object.values(rule.bound_to).some(ids => ids.length))) {
|
|
182
|
-
const { loadLocalCapabilityEntries, loadLocalOverrideCapabilityEntries, createRuleBindingTargetResolver } = await importImpl(import.meta.url, 'artifacts/query-engine.js');
|
|
183
|
-
const root = resolveAiwgRoot(opts.srcRoot) || opts.srcRoot;
|
|
184
|
-
// Framework graph paths are package-relative; project graph paths belong
|
|
185
|
-
// to the queried project. Normalize before merging so one source cannot
|
|
186
|
-
// become two apparently ambiguous targets after a source-index rebuild.
|
|
187
|
-
const indexEntries = new Map();
|
|
188
|
-
const entryPaths = new Map();
|
|
189
|
-
const loadEntries = (cwd, overridesOnly = false) => (overridesOnly
|
|
190
|
-
? loadLocalOverrideCapabilityEntries(cwd)
|
|
191
|
-
: loadLocalCapabilityEntries(cwd, undefined, indexEntries)).map(entry => {
|
|
192
|
-
const anchor = entry.path.startsWith('agentic/code/') ? root : cwd;
|
|
193
|
-
const key = `${anchor}\0${entry.path}`;
|
|
194
|
-
if (!entryPaths.has(key)) entryPaths.set(key, path.resolve(anchor, entry.path));
|
|
195
|
-
const resolvedPath = entryPaths.get(key);
|
|
196
|
-
// Canonical IDs hash the indexed package-relative path. Keep it through
|
|
197
|
-
// resolution; the selected target is anchored to root below.
|
|
198
|
-
return { ...entry, path: entry.path.startsWith('agentic/code/') ? entry.path
|
|
199
|
-
: opts.mapInputPath ? opts.mapInputPath(resolvedPath) : resolvedPath };
|
|
200
|
-
});
|
|
201
|
-
let rebuilt = false;
|
|
202
|
-
const cacheDir = process.env.AIWG_RULE_BINDING_INDEX_CACHE;
|
|
203
|
-
const cacheFile = cacheDir && path.join(cacheDir, `${contentHash(opts.logicalInputPath ? opts.logicalInputPath(root) : root)}.json`);
|
|
204
|
-
const buildSourceEntries = async (refresh = false) => {
|
|
205
|
-
if (!refresh && cacheFile) {
|
|
206
|
-
try { return JSON.parse(fs.readFileSync(cacheFile, 'utf8')).map(entry => ({ ...entry, path: path.isAbsolute(entry.path) && opts.mapInputPath ? opts.mapInputPath(entry.path) : entry.path })); }
|
|
207
|
-
catch (error) {
|
|
208
|
-
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const { collectRuleBindingEntries } = await importImpl(import.meta.url, 'artifacts/index-builder.js');
|
|
212
|
-
const sourceEntries = await collectRuleBindingEntries(root,
|
|
213
|
-
root === opts.srcRoot && !fs.existsSync(path.join(root, 'agentic')) ? '.' : 'agentic/code');
|
|
214
|
-
rebuilt = true;
|
|
215
|
-
const entries = sourceEntries.map(entry => ({ ...entry, path: entry.path.startsWith('agentic/code/')
|
|
216
|
-
? entry.path : path.resolve(root, entry.path) }));
|
|
217
|
-
if (cacheFile) {
|
|
218
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
219
|
-
// Workers may publish the same immutable source snapshot concurrently.
|
|
220
|
-
const temporary = `${cacheFile}.${process.pid}.tmp`;
|
|
221
|
-
fs.writeFileSync(temporary, JSON.stringify(entries.map(entry => ({ ...entry, path: opts.logicalInputPath ? opts.logicalInputPath(entry.path) : entry.path }))));
|
|
222
|
-
fs.renameSync(temporary, cacheFile);
|
|
223
|
-
}
|
|
224
|
-
return entries;
|
|
225
|
-
};
|
|
226
|
-
const localEntries = opts.bindingEntries || loadEntries(opts.target, Boolean(packageRelations));
|
|
227
|
-
let entries = [...localEntries];
|
|
228
|
-
if (!opts.bindingEntries) {
|
|
229
|
-
// The validated package table is authoritative for its exact source digest.
|
|
230
|
-
// Keep the canonical resolver for aliases, local overrides and ambiguity.
|
|
231
|
-
const sourceEntries = packageRelations
|
|
232
|
-
? Object.values(packageRelations.index.artifacts).map(entry => ({ ...entry,
|
|
233
|
-
indexScope: 'packaged', indexGraph: 'framework' }))
|
|
234
|
-
: loadEntries(root);
|
|
235
|
-
// A relocated project graph may also be visible while querying the
|
|
236
|
-
// package root. Keep its project-anchored instance only.
|
|
237
|
-
const uniqueSources = sourceEntries.filter(entry => !(opts.sharedProjectGraph &&
|
|
238
|
-
entry.path.startsWith(path.join(root, '.aiwg') + path.sep) &&
|
|
239
|
-
localEntries.some(local => local.type === entry.type && local.name === entry.name)));
|
|
240
|
-
entries.push(...uniqueSources);
|
|
241
|
-
if (!sourceEntries.length) entries.push(...await buildSourceEntries());
|
|
242
|
-
}
|
|
243
|
-
// Project-local installed bundles are outside the package source index.
|
|
244
|
-
// Reuse the existing binding index builder only for those source roots.
|
|
245
|
-
if (!opts.bindingEntries) {
|
|
246
|
-
for (const entry of Object.values(configs.project.installed || {})) {
|
|
247
|
-
if (!entry.localPath) continue;
|
|
248
|
-
const localRoot = path.resolve(opts.target, entry.localPath);
|
|
249
|
-
if (localRoot === root || !fs.existsSync(localRoot)) continue;
|
|
250
|
-
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'aiwg-local-rule-index-'));
|
|
251
|
-
try {
|
|
252
|
-
const { buildIndex } = await importImpl(import.meta.url, 'artifacts/index-builder.js');
|
|
253
|
-
await buildIndex(localRoot, { scope: '.', outputDir: temp, force: true, quiet: true });
|
|
254
|
-
const index = JSON.parse(read(path.join(temp, '.aiwg', '.index', 'metadata.json')));
|
|
255
|
-
const scoped = Object.values(index.entries).map(entry => ({ ...entry, path: path.resolve(localRoot, entry.path) }));
|
|
256
|
-
localEntries.push(...scoped);
|
|
257
|
-
entries.push(...scoped);
|
|
258
|
-
} finally { fs.rmSync(temp, { recursive: true, force: true }); }
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// A stored index can predate the source tree (edge/dev checkouts). Rebuild once from source
|
|
262
|
-
// before reporting a binding target as missing.
|
|
263
|
-
const resolvedTargets = new Map();
|
|
264
|
-
let resolveBinding = createRuleBindingTargetResolver(entries);
|
|
265
|
-
const resolveTarget = async (type, id) => {
|
|
266
|
-
const key = `${type}:${id}`;
|
|
267
|
-
if (resolvedTargets.has(key)) return resolvedTargets.get(key);
|
|
268
|
-
let target;
|
|
269
|
-
try { target = resolveBinding(type, id); }
|
|
270
|
-
catch (error) {
|
|
271
|
-
if (packageRelations || rebuilt || opts.bindingEntries || !/^Missing /.test(error.message)) throw error;
|
|
272
|
-
indexEntries.clear();
|
|
273
|
-
entryPaths.clear();
|
|
274
|
-
entries = [...localEntries, ...await buildSourceEntries(true)];
|
|
275
|
-
rebuilt = true;
|
|
276
|
-
resolveBinding = createRuleBindingTargetResolver(entries);
|
|
277
|
-
target = resolveBinding(type, id);
|
|
278
|
-
// A rebuild replaces the input set for every lookup.
|
|
279
|
-
resolvedTargets.clear();
|
|
280
|
-
}
|
|
281
|
-
resolvedTargets.set(key, target);
|
|
282
|
-
return target;
|
|
283
|
-
};
|
|
284
|
-
for (const rule of rules.filter(rule => rule.tagged)) {
|
|
285
|
-
for (const [key, type] of Object.entries(RULE_BINDING_TYPES)) {
|
|
286
|
-
for (const id of rule.bound_to[key]) {
|
|
287
|
-
const target = await resolveTarget(type, id);
|
|
288
|
-
const source = path.isAbsolute(target.path) ? target.path : path.resolve(target.path.startsWith('agentic/code/') ? root : opts.target, target.path);
|
|
289
|
-
if (!fs.existsSync(source)) throw new Error(`${rule.file}: binding target content missing: ${source}`);
|
|
290
|
-
bindings.set(source, [...(bindings.get(source) || []), rule]);
|
|
291
|
-
const key = `${target.type}:${source}`;
|
|
292
|
-
if (!targetKeys.has(key)) {
|
|
293
|
-
targetKeys.add(key);
|
|
294
|
-
targets.push({ ...target, source });
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
180
|
+
const { bindings, targets } = await resolveDeliveryRuleBindings(opts, rules, configs, root, packageRelations);
|
|
300
181
|
return { rules, bindings, targets, pins: pinned.rules, pinWarnings: pinned.warnings };
|
|
301
182
|
}
|
|
302
183
|
|
|
@@ -1010,7 +891,7 @@ export async function prepareRuleDeployment(original, provider, deploy) {
|
|
|
1010
891
|
const mappings = [];
|
|
1011
892
|
const preimages = [...initialPreimages];
|
|
1012
893
|
let retained = false;
|
|
1013
|
-
const originals =
|
|
894
|
+
const originals = captureProviderPathDescriptors(provider);
|
|
1014
895
|
const logicalTarget = original.target;
|
|
1015
896
|
const priorAiwgRoot = process.env.AIWG_ROOT;
|
|
1016
897
|
const priorRegistry = process.env.AIWG_USER_REGISTRY_PATH;
|
|
@@ -1148,7 +1029,7 @@ export async function prepareRuleDeployment(original, provider, deploy) {
|
|
|
1148
1029
|
if (!path.isAbsolute(dir)) continue;
|
|
1149
1030
|
const to = path.join(temp, 'native', key);
|
|
1150
1031
|
mount(dir, to);
|
|
1151
|
-
provider
|
|
1032
|
+
setStagedProviderPath(provider, key, to);
|
|
1152
1033
|
}
|
|
1153
1034
|
let kernel = provider.kernelSkillsPath;
|
|
1154
1035
|
if (kernel && path.isAbsolute(kernel)) {
|
|
@@ -1344,7 +1225,7 @@ export async function prepareRuleDeployment(original, provider, deploy) {
|
|
|
1344
1225
|
return plan;
|
|
1345
1226
|
} finally {
|
|
1346
1227
|
if (priorContextStage === undefined) delete process.env[CONTEXT_STAGE_ENV]; else process.env[CONTEXT_STAGE_ENV] = priorContextStage;
|
|
1347
|
-
|
|
1228
|
+
restoreProviderPaths(provider, originals);
|
|
1348
1229
|
if (priorAiwgRoot === undefined) delete process.env.AIWG_ROOT; else process.env.AIWG_ROOT = priorAiwgRoot;
|
|
1349
1230
|
if (priorRegistry === undefined) delete process.env.AIWG_USER_REGISTRY_PATH; else process.env.AIWG_USER_REGISTRY_PATH = priorRegistry;
|
|
1350
1231
|
if (priorArtifacts === undefined) delete process.env.AIWG_ARTIFACTS_PATH; else process.env.AIWG_ARTIFACTS_PATH = priorArtifacts;
|
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
* - `--approval-mode <mode>` — common to both; "`muse exec` accepts both
|
|
32
32
|
* approval flags" (configuration). Opt-in only: the adapter sets NO
|
|
33
33
|
* approval posture by default (#230 out of scope: `--yolo` defaults).
|
|
34
|
+
* - `--trust-workspace` — loads project rules, skills and hooks for the
|
|
35
|
+
* workspace (#3685: untrusted Muse reports `rules.context_load`
|
|
36
|
+
* `project_sources=0` with user skills only and empty hook admission;
|
|
37
|
+
* trusted reports `project_sources=1` with project skills and hooks).
|
|
38
|
+
* Tier 0 workspaces are conductor-prepared worker trees the conductor
|
|
39
|
+
* owns, so the direct profile binds this flag by default through the
|
|
40
|
+
* host-controlled trust mapping; the brief never supplies it.
|
|
34
41
|
* - `muse export --session <uuid> --out <path>` — offline,
|
|
35
42
|
* byte-deterministic transcript projection; `--last` and `--redacted`
|
|
36
43
|
* skip the picker / redact payload strings (cookbook:
|
|
@@ -173,6 +180,9 @@ export class MuseAdapter extends ProviderAdapter {
|
|
|
173
180
|
if ((permissions.approval ?? 'never') !== 'never') throw new Error('Muse direct worker approval requires never');
|
|
174
181
|
// Trust is a host-owned, qualification-bound control, like the schema.
|
|
175
182
|
// Brief contents and CLI defaults cannot opt into workspace instructions.
|
|
183
|
+
// Tier 0 binds ["--trust-workspace"] because the workspace is always a
|
|
184
|
+
// conductor-prepared worker tree (#3685); untrusted trees stay refused
|
|
185
|
+
// at dispatch by the operator native-argv gate.
|
|
176
186
|
let trust;
|
|
177
187
|
try { trust = JSON.parse(profile.controls.trust.mapping); } catch { /* fail closed below */ }
|
|
178
188
|
if (!Array.isArray(trust) || !(trust.length === 0 || trust.length === 1 && trust[0] === '--trust-workspace')) {
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker AIWG context loading and launch-time ensure, tools side (#3685).
|
|
3
|
+
*
|
|
4
|
+
* The provider-set UNION lives in `src/config/worker-context.ts` and the
|
|
5
|
+
* config loads through the existing `readAiwgConfig` loader (never ad-hoc
|
|
6
|
+
* JSON). Both cross the tools/src boundary through `importImpl`, so this
|
|
7
|
+
* module stays runnable from an installed package (`dist/src`) and a
|
|
8
|
+
* checkout (`src/`); every such seam is injectable for synthetic tests.
|
|
9
|
+
* `src/` must never import `tools/`, so the src module is always the
|
|
10
|
+
* callee, never the caller.
|
|
11
|
+
*
|
|
12
|
+
* Also home to the single-pass deploy, section verifier, and
|
|
13
|
+
* skip-worktree helpers extracted from `worktree-preparation.mjs` (which
|
|
14
|
+
* re-exports them) so that file stays agent-friendly.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
21
|
+
|
|
22
|
+
/** Load the project config through the existing loader. */
|
|
23
|
+
async function defaultReadConfig(repoRoot) {
|
|
24
|
+
const { readAiwgConfig } = await importImpl(import.meta.url, 'config/aiwg-config.js');
|
|
25
|
+
return readAiwgConfig(repoRoot);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Resolve the provider union through the src-side pure function. */
|
|
29
|
+
async function defaultResolveFromConfig(config) {
|
|
30
|
+
const { resolveWorkerContextProvidersFromConfig } = await importImpl(import.meta.url, 'config/worker-context.js');
|
|
31
|
+
return resolveWorkerContextProvidersFromConfig(config);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Map one orchestration CLI id through the src-side registry mapping. */
|
|
35
|
+
async function defaultMapCli(cli, parallelism) {
|
|
36
|
+
const { mapCliToProviderId } = await importImpl(import.meta.url, 'config/worker-context.js');
|
|
37
|
+
return mapCliToProviderId(cli, parallelism);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the worker-context provider set (#3685 round 2): the union of
|
|
42
|
+
* `.aiwg/aiwg.config` `providers` and every CLI configured for
|
|
43
|
+
* orchestration (`parallelism.clis` keys/adapters and the `cli` of every
|
|
44
|
+
* `parallelism.groups.*.members[]`), mapped through the provider
|
|
45
|
+
* registry. An explicit list wins and is returned verbatim.
|
|
46
|
+
*
|
|
47
|
+
* @returns {Promise<{providers: string[], skipped: string[]}>}
|
|
48
|
+
*/
|
|
49
|
+
export async function resolveWorkerContextProviders({ repoRoot, providers = null, readConfig = null, resolveFromConfig = null } = {}) {
|
|
50
|
+
if (providers !== null && providers !== undefined) {
|
|
51
|
+
if (!Array.isArray(providers) || providers.length === 0
|
|
52
|
+
|| providers.some(p => typeof p !== 'string' || p.length === 0 || p.includes('\0') || p.includes(','))) {
|
|
53
|
+
throw new Error('worker context providers must be a non-empty array of provider ids');
|
|
54
|
+
}
|
|
55
|
+
return { providers: [...new Set(providers)], skipped: [] };
|
|
56
|
+
}
|
|
57
|
+
if (typeof repoRoot !== 'string' || !repoRoot) {
|
|
58
|
+
throw new Error('worker context providers need a repoRoot or an explicit provider list');
|
|
59
|
+
}
|
|
60
|
+
const config = await (readConfig ?? defaultReadConfig)(repoRoot);
|
|
61
|
+
if (!config) {
|
|
62
|
+
throw new Error(`worker context deployment needs configured providers: cannot read ${path.join(repoRoot, '.aiwg', 'aiwg.config')} (missing or invalid); configure providers and orchestration CLIs, or pass an explicit provider list`);
|
|
63
|
+
}
|
|
64
|
+
return (resolveFromConfig ?? defaultResolveFromConfig)(config);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Ensure a worker workdir carries its CLI's AIWG bootstrap before launch.
|
|
69
|
+
* Resolves the worker CLI to its provider (from the project config, so
|
|
70
|
+
* orchestration CLIs map through the registry), checks presence, and
|
|
71
|
+
* deploys through the same one-pass helper when missing. The deploy
|
|
72
|
+
* target is always exactly `workdir` — never anywhere else.
|
|
73
|
+
*
|
|
74
|
+
* Injectable for synthetic tests: `ensureFn` stubs the whole ensure,
|
|
75
|
+
* otherwise `mapCli`, `readConfig`, `resolveFromConfig`, `deployFn`,
|
|
76
|
+
* `statusFn`, and `skipWorktreeFn` stub each seam.
|
|
77
|
+
*
|
|
78
|
+
* @returns {Promise<{ensured: boolean, provider: string, providers: string[], skippedBridges: string[], skipped: string[]}>}
|
|
79
|
+
*/
|
|
80
|
+
export async function ensureWorkerContextForLaunch({
|
|
81
|
+
workdir,
|
|
82
|
+
cli,
|
|
83
|
+
config = null,
|
|
84
|
+
repoRoot = null,
|
|
85
|
+
providers = null,
|
|
86
|
+
readConfig = null,
|
|
87
|
+
resolveFromConfig = null,
|
|
88
|
+
mapCli = null,
|
|
89
|
+
ensureFn = null,
|
|
90
|
+
statusFn = null,
|
|
91
|
+
deployFn = null,
|
|
92
|
+
skipWorktreeFn = null,
|
|
93
|
+
aiwgBin = process.env.AIWG_BIN ?? 'aiwg',
|
|
94
|
+
} = {}) {
|
|
95
|
+
if (typeof workdir !== 'string' || !workdir) throw new Error('worker context ensure needs a workdir');
|
|
96
|
+
if (typeof cli !== 'string' || !cli) throw new Error('worker context ensure needs the worker CLI id');
|
|
97
|
+
let resolved = Array.isArray(providers) && providers.length > 0 ? { providers: [...new Set(providers)], skipped: [] } : null;
|
|
98
|
+
let loaded = config ?? null;
|
|
99
|
+
if (!resolved) {
|
|
100
|
+
if (!loaded && repoRoot) loaded = await (readConfig ?? defaultReadConfig)(repoRoot);
|
|
101
|
+
if (loaded) {
|
|
102
|
+
resolved = await (resolveFromConfig ?? defaultResolveFromConfig)(loaded);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const map = mapCli ?? defaultMapCli;
|
|
106
|
+
const mapped = await map(cli, loaded?.parallelism ?? null);
|
|
107
|
+
if (!('provider' in mapped) || !mapped.provider) {
|
|
108
|
+
// Typed-shaped refusal without importing the src error class (which
|
|
109
|
+
// would couple this sync-throwing path to the compiled `dist`
|
|
110
|
+
// layout): callers discriminate on `code`, never `instanceof`.
|
|
111
|
+
throw Object.assign(
|
|
112
|
+
new Error(`cannot ensure worker context: CLI '${cli}' maps to no AIWG provider (${mapped.skipped ?? 'unmapped'})`),
|
|
113
|
+
{ code: 'OG_WORKER_CONTEXT_UNAVAILABLE',
|
|
114
|
+
remedy: `configure parallelism.clis.${cli} with a providers:<id> adapter, or use a CLI whose adapter maps to a known provider`,
|
|
115
|
+
workdir },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const provider = mapped.provider;
|
|
119
|
+
const deployProviders = resolved ? [...new Set([...resolved.providers, provider])] : [provider];
|
|
120
|
+
const ensure = ensureFn ?? (async (opts) => {
|
|
121
|
+
const { ensureWorkerContextBootstrap } = await importImpl(import.meta.url, 'config/worker-context.js');
|
|
122
|
+
return ensureWorkerContextBootstrap(opts);
|
|
123
|
+
});
|
|
124
|
+
const outcome = await ensure({
|
|
125
|
+
workdir, provider, providers: deployProviders, aiwgBin,
|
|
126
|
+
...(statusFn ? { statusFn } : {}),
|
|
127
|
+
...(deployFn ? { deployFn } : {}),
|
|
128
|
+
...(skipWorktreeFn ? { skipWorktreeFn } : {}),
|
|
129
|
+
});
|
|
130
|
+
return { ...outcome, provider, skipped: resolved?.skipped ?? [] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Materialize the union set into the worktree config through the src-side sync. */
|
|
134
|
+
async function defaultSyncProviders(worktreePath, providers) {
|
|
135
|
+
const { syncWorkerContextProviders } = await importImpl(import.meta.url, 'config/worker-context.js');
|
|
136
|
+
return syncWorkerContextProviders({ workdir: worktreePath, providers });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Single-pass argv through the src-side canonical builder (never `--providers`). */
|
|
140
|
+
async function defaultDeployArgv(providers, worktreePath) {
|
|
141
|
+
const { buildWorkerContextDeployArgs } = await importImpl(import.meta.url, 'config/worker-context.js');
|
|
142
|
+
return buildWorkerContextDeployArgs({ providers, target: worktreePath });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Default single-pass AIWG context deploy (#3685): sync the union set
|
|
147
|
+
* into the worktree config, then one `aiwg use all --target <worktree>`
|
|
148
|
+
* covering every configured provider. Never one deploy per provider:
|
|
149
|
+
* per-provider invocations rewrite AGENTS.md piecemeal and clobber each
|
|
150
|
+
* other's rule sections, while a single pass keeps them all. No
|
|
151
|
+
* `--providers` flag: it never reaches project-scope deploys
|
|
152
|
+
* (`executeCore` resolves from config), so the configured-provider path
|
|
153
|
+
* above is the supported mechanism. Injectable via
|
|
154
|
+
* `prepareWorkerWorktree()`'s `deployContextFn` for synthetic tests;
|
|
155
|
+
* `syncFn` stubs the config sync as `(worktreePath, providers)` and
|
|
156
|
+
* `argvFn` stubs the argv builder as `(providers, target)`.
|
|
157
|
+
*
|
|
158
|
+
* @returns {Promise<{providers: string[]}>}
|
|
159
|
+
*/
|
|
160
|
+
export async function defaultDeployWorkerContext({ worktreePath, providers, aiwgBin = 'aiwg', spawn = spawnSync, timeoutMs = 300000, syncFn = null, argvFn = null } = {}) {
|
|
161
|
+
if (typeof worktreePath !== 'string' || !worktreePath || typeof aiwgBin !== 'string' || !aiwgBin) {
|
|
162
|
+
throw new Error('worker context deploy requires a worktree path and an aiwg binary');
|
|
163
|
+
}
|
|
164
|
+
if (!Array.isArray(providers) || providers.length === 0) {
|
|
165
|
+
throw new Error('worker context deploy requires a non-empty provider list');
|
|
166
|
+
}
|
|
167
|
+
const sync = syncFn ?? defaultSyncProviders;
|
|
168
|
+
const synced = await sync(worktreePath, providers);
|
|
169
|
+
const syncedProviders = Array.isArray(synced?.providers) && synced.providers.length > 0 ? synced.providers : [...providers];
|
|
170
|
+
const buildArgv = argvFn ?? defaultDeployArgv;
|
|
171
|
+
const argv = await buildArgv(syncedProviders, worktreePath);
|
|
172
|
+
const result = spawn(aiwgBin, argv, { cwd: worktreePath, encoding: 'utf8', timeout: timeoutMs });
|
|
173
|
+
if (result.error) throw new Error(`aiwg ${argv.join(' ')} failed to run: ${result.error.message}`);
|
|
174
|
+
if (result.status !== 0) {
|
|
175
|
+
throw new Error(`aiwg ${argv.join(' ')} exited ${result.status}: ${String(result.stderr ?? '').trim().slice(0, 2000)}`);
|
|
176
|
+
}
|
|
177
|
+
return { providers: [...syncedProviders] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** AIWG-managed rule-section markers written into a deployed AGENTS.md. */
|
|
181
|
+
const WORKER_CONTEXT_RULE_MARKER = /<!--\s*AIWG:[A-Za-z0-9_-]+:(start|end)\s*-->/;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Verify a prepared worktree actually carries AIWG rule sections (#3685):
|
|
185
|
+
* the worktree's AGENTS.md must exist and contain AIWG-managed section
|
|
186
|
+
* markers. Throws before launch otherwise — a worktree without them would
|
|
187
|
+
* run the worker with no project rules/skills/hooks. Injectable via
|
|
188
|
+
* `prepareWorkerWorktree()`'s `verifyContextFn` for synthetic tests.
|
|
189
|
+
*
|
|
190
|
+
* @returns {{verified: boolean, providers: string[]}}
|
|
191
|
+
*/
|
|
192
|
+
export function verifyWorkerContextSections({ worktreePath, providers = [] } = {}) {
|
|
193
|
+
const agentsMd = path.join(worktreePath, 'AGENTS.md');
|
|
194
|
+
let content;
|
|
195
|
+
try {
|
|
196
|
+
content = readFileSync(agentsMd, 'utf8');
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error(`worker context verification failed: ${agentsMd} is missing; the worker would run without AIWG rule sections`);
|
|
199
|
+
}
|
|
200
|
+
if (!WORKER_CONTEXT_RULE_MARKER.test(content)) {
|
|
201
|
+
throw new Error(`worker context verification failed: ${agentsMd} has no AIWG rule sections (expected <!-- AIWG:<rule>:start --> markers for ${[...providers].join(',') || 'the configured providers'})`);
|
|
202
|
+
}
|
|
203
|
+
return { verified: true, providers: [...providers] };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Tracked files the context deploy modified, so the caller can hide them
|
|
208
|
+
* from the worker diff. Parses `git status --porcelain=v1` with untracked
|
|
209
|
+
* files excluded: every remaining entry is a tracked modification (the
|
|
210
|
+
* fresh worktree starts clean, so each one is deploy-caused).
|
|
211
|
+
*
|
|
212
|
+
* @returns {string[]} Repo-relative paths of tracked-modified files.
|
|
213
|
+
*/
|
|
214
|
+
export function listDeployDirtiedBridges({ worktreePath, spawn = spawnSync, timeoutMs = 30000 } = {}) {
|
|
215
|
+
// Raw spawn output, never a trimmed helper: trim would eat the
|
|
216
|
+
// significant leading space of the first porcelain line (` M AGENTS.md`
|
|
217
|
+
// trims to `M AGENTS.md`, shifting slice(3) one byte into the filename).
|
|
218
|
+
const result = spawn('git', ['status', '--porcelain=v1', '--untracked-files=no'], { cwd: worktreePath, encoding: 'utf8', timeout: timeoutMs });
|
|
219
|
+
if (result.error) throw new Error(`git status failed to run: ${result.error.message}`);
|
|
220
|
+
if (result.status !== 0) {
|
|
221
|
+
throw new Error(`git status exited ${result.status}: ${String(result.stderr ?? '').trim()}`);
|
|
222
|
+
}
|
|
223
|
+
const files = [];
|
|
224
|
+
for (const line of String(result.stdout ?? '').split('\n')) {
|
|
225
|
+
if (!line.trim()) continue;
|
|
226
|
+
// `XY PATH` or `XY ORIG -> NEW` for renames; take the new path, which is
|
|
227
|
+
// what the worker diff would report.
|
|
228
|
+
const raw = line.slice(3);
|
|
229
|
+
const arrow = raw.indexOf(' -> ');
|
|
230
|
+
const rel = arrow >= 0 ? raw.slice(arrow + 4) : raw;
|
|
231
|
+
// Porcelain quotes paths with special bytes; a deploy that creates such
|
|
232
|
+
// a name is refused rather than mis-flagged.
|
|
233
|
+
if (!rel || rel.startsWith('"')) throw new Error(`worker context deploy dirtied an unflaggeable path: ${line}`);
|
|
234
|
+
files.push(rel);
|
|
235
|
+
}
|
|
236
|
+
return files;
|
|
237
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Classify unreadable same-uid processes as possible escaped worker descendants or unrelated trees.
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// Ancestry walks stay bounded: a pathological ppid chain can never turn this
|
|
5
|
+
// proof into an unbounded scan. Depth and the caller's time bound both apply.
|
|
6
|
+
export const MAX_WORKER_ANCESTRY_DEPTH = 32;
|
|
7
|
+
|
|
8
|
+
const vanished = error => ['ENOENT', 'ESRCH'].includes(error?.code);
|
|
9
|
+
|
|
10
|
+
function readSmallFile(fs, filename, maxBytes = 4096) {
|
|
11
|
+
let fd;
|
|
12
|
+
try {
|
|
13
|
+
fd = fs.openSync(filename, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
14
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let bytes = 0;
|
|
17
|
+
for (;;) {
|
|
18
|
+
const size = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
19
|
+
if (!size) break;
|
|
20
|
+
chunks.push(buffer.subarray(0, size));
|
|
21
|
+
bytes += size;
|
|
22
|
+
if (bytes > maxBytes) return { tooLarge: true };
|
|
23
|
+
}
|
|
24
|
+
return { text: Buffer.concat(chunks).toString('utf8') };
|
|
25
|
+
} catch (error) { return { error }; }
|
|
26
|
+
finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse the link fields of /proc/<pid>/stat: ppid, process group, session and
|
|
30
|
+
// start time. The comm field may contain spaces and parentheses, so field 3
|
|
31
|
+
// begins after its LAST ')'. Returns null when the text is malformed.
|
|
32
|
+
export function parseProcStatLink(text) {
|
|
33
|
+
if (typeof text !== 'string') return null;
|
|
34
|
+
const end = text.lastIndexOf(')');
|
|
35
|
+
if (end < 0) return null;
|
|
36
|
+
const fields = text.slice(end + 1).trim().split(/\s+/);
|
|
37
|
+
if (fields.length < 20) return null;
|
|
38
|
+
const [ppid, pgrp, session] = [fields[1], fields[2], fields[3]].map(Number);
|
|
39
|
+
const start = fields[19];
|
|
40
|
+
if (![ppid, pgrp, session].every(Number.isSafeInteger) || typeof start !== 'string' || !/^\d+$/.test(start)) return null;
|
|
41
|
+
return { ppid, pgrp, session, start };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Read /proc/<pid>/cgroup verbatim (cgroup membership stays readable for
|
|
45
|
+
// non-dumpable processes). Normalized like sampleProcessCgroup: the bare path
|
|
46
|
+
// for the unified hierarchy, raw content otherwise. Null when unreadable.
|
|
47
|
+
export function readProcCgroup(fs, path) {
|
|
48
|
+
const read = readSmallFile(fs, `${path}/cgroup`);
|
|
49
|
+
if (read.error || read.tooLarge) return null;
|
|
50
|
+
const raw = String(read.text ?? '').trim();
|
|
51
|
+
if (!raw) return null;
|
|
52
|
+
const unified = raw.split('\n').find(line => line.startsWith('0::'));
|
|
53
|
+
const group = unified ? unified.slice(3) : raw;
|
|
54
|
+
return group || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isTicks = value => typeof value === 'string' && /^\d+$/.test(value);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Decide whether an unreadable same-uid process could be an escaped descendant
|
|
61
|
+
* of the recorded worker leader, using only evidence that stays readable for
|
|
62
|
+
* non-dumpable (hardened, e.g. Codex) processes: /proc/<pid>/stat (ppid,
|
|
63
|
+
* process group, session, start time) and /proc/<pid>/cgroup.
|
|
64
|
+
*
|
|
65
|
+
* Returns 'predates' (started strictly before the worker: cannot descend from
|
|
66
|
+
* it), 'unrelated' (its readable ancestry reaches another live process that
|
|
67
|
+
* is not ours, e.g. a different conductor's worker), 'vanished' (it
|
|
68
|
+
* disappeared mid-scan), or 'possible-descendant' (fail closed: ancestry
|
|
69
|
+
* reaches the worker leader or the worker's original session/process group,
|
|
70
|
+
* shares the worker's private cgroup, is reparented to init/a subreaper with
|
|
71
|
+
* no other link, or any link in the chain is unreadable).
|
|
72
|
+
*/
|
|
73
|
+
export function classifyUnreadableWorkerProcess({ fs, procRoot, pid, uid, workerPid,
|
|
74
|
+
workerPgid, workerSession, workerStart, workerCgroup, selfCgroup,
|
|
75
|
+
now = () => performance.now(), deadline = now() + 100, maxDepth = MAX_WORKER_ANCESTRY_DEPTH } = {}) {
|
|
76
|
+
const path = `${procRoot}/${pid}`;
|
|
77
|
+
const first = readSmallFile(fs, `${path}/stat`);
|
|
78
|
+
if (first.error) return vanished(first.error) ? 'vanished' : 'possible-descendant';
|
|
79
|
+
const link = !first.tooLarge && parseProcStatLink(first.text);
|
|
80
|
+
if (!link) return 'possible-descendant';
|
|
81
|
+
if (!isTicks(workerStart)) return 'possible-descendant';
|
|
82
|
+
if (BigInt(link.start) < BigInt(workerStart)) return 'predates';
|
|
83
|
+
// A process still in the worker's original session or process group is
|
|
84
|
+
// treated as a possible descendant even when its ppid chain is broken
|
|
85
|
+
// (an orphan that never called setsid keeps both).
|
|
86
|
+
if ((Number.isSafeInteger(workerPgid) && workerPgid > 0 && link.pgrp === workerPgid)
|
|
87
|
+
|| (Number.isSafeInteger(workerSession) && workerSession > 0 && link.session === workerSession)) {
|
|
88
|
+
return 'possible-descendant';
|
|
89
|
+
}
|
|
90
|
+
// A shared cgroup links the process to the worker only when the worker's
|
|
91
|
+
// own cgroup is private: a degraded worker shares its session cgroup with
|
|
92
|
+
// every sibling, so that match proves nothing and ancestry decides.
|
|
93
|
+
if (workerCgroup) {
|
|
94
|
+
const candidate = readProcCgroup(fs, path);
|
|
95
|
+
if (candidate !== null && candidate === workerCgroup && workerCgroup !== selfCgroup) return 'possible-descendant';
|
|
96
|
+
}
|
|
97
|
+
let current = link.ppid;
|
|
98
|
+
for (let depth = 0; ; depth++) {
|
|
99
|
+
if (now() >= deadline) return 'possible-descendant';
|
|
100
|
+
if (depth >= maxDepth) return 'possible-descendant';
|
|
101
|
+
if (current === workerPid) {
|
|
102
|
+
// A pid number alone is not proof: verify the generation, since the
|
|
103
|
+
// number may have been reused after the leader exited.
|
|
104
|
+
const leader = readSmallFile(fs, `${procRoot}/${current}/stat`);
|
|
105
|
+
if (leader.error || leader.tooLarge) return 'possible-descendant';
|
|
106
|
+
const leaderLink = parseProcStatLink(leader.text);
|
|
107
|
+
if (!leaderLink) return 'possible-descendant';
|
|
108
|
+
if (leaderLink.start === workerStart) return 'possible-descendant';
|
|
109
|
+
current = leaderLink.ppid;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Reparented to init (or the kernel): with no other link this is the
|
|
113
|
+
// honest unknown case, never proof of absence.
|
|
114
|
+
if (!Number.isSafeInteger(current) || current <= 1) return 'possible-descendant';
|
|
115
|
+
let owner;
|
|
116
|
+
try { owner = fs.statSync(`${procRoot}/${current}`).uid; }
|
|
117
|
+
catch { return 'possible-descendant'; }
|
|
118
|
+
// Another identity's tree (including container shims and managers owned
|
|
119
|
+
// by a different uid) can never be our worker's descendant. A setuid
|
|
120
|
+
// escape across uids is out of scope for this same-uid scan.
|
|
121
|
+
if (owner !== uid) return 'unrelated';
|
|
122
|
+
const next = readSmallFile(fs, `${procRoot}/${current}/stat`);
|
|
123
|
+
if (next.error || next.tooLarge) return 'possible-descendant';
|
|
124
|
+
const nextLink = parseProcStatLink(next.text);
|
|
125
|
+
if (!nextLink) return 'possible-descendant';
|
|
126
|
+
// This ancestor predates the worker, so it cannot descend from it — and
|
|
127
|
+
// neither can anything reached through it (a sibling subtree, another
|
|
128
|
+
// conductor's parallel worker, a session manager).
|
|
129
|
+
if (BigInt(nextLink.start) < BigInt(workerStart)) return 'unrelated';
|
|
130
|
+
current = nextLink.ppid;
|
|
131
|
+
}
|
|
132
|
+
}
|