@aiwg/cli 2026.10.3 → 2026.10.4
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/README.md +2 -2
- package/agentic/code/providers/model-capabilities.v1.json +96 -4
- package/bin/aiwg.mjs +50 -27
- package/dist/src/agents/grounding/knowledge-base.js +54 -6
- package/dist/src/artifacts/browser-export.js +6 -0
- package/dist/src/artifacts/checksum-manifest.js +1 -0
- package/dist/src/artifacts/citation-parser.js +173 -92
- package/dist/src/artifacts/citation-table-schema.js +59 -0
- package/dist/src/artifacts/cli.js +57 -18
- package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
- package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
- package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
- package/dist/src/artifacts/corpus-views/renderers.js +3 -3
- package/dist/src/artifacts/discovery-eval.js +6 -4
- package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
- package/dist/src/artifacts/fortemi-core-sync.js +166 -40
- package/dist/src/artifacts/framework-capabilities.js +171 -0
- package/dist/src/artifacts/framework-index-paths.js +73 -0
- package/dist/src/artifacts/framework-source-digest.js +75 -0
- package/dist/src/artifacts/index-builder.js +285 -131
- package/dist/src/artifacts/index-files.js +132 -18
- package/dist/src/artifacts/index-preparation.js +65 -0
- package/dist/src/artifacts/index-refresh.js +576 -0
- package/dist/src/artifacts/index-worker.mjs +9 -0
- package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
- package/dist/src/artifacts/prebuilt-deployment.js +46 -0
- package/dist/src/artifacts/prebuilt-relations.js +80 -0
- package/dist/src/artifacts/query-engine.js +410 -88
- package/dist/src/artifacts/relations-builder.js +168 -0
- package/dist/src/artifacts/relations.js +148 -0
- package/dist/src/artifacts/stats.js +5 -2
- package/dist/src/artifacts/types.js +30 -10
- package/dist/src/artifacts/watcher.js +1 -1
- package/dist/src/auth/credential-store.js +2 -1
- package/dist/src/catalog/cli.d.mts +5 -0
- package/dist/src/channel/manager.d.mts +20 -0
- package/dist/src/channel/manager.mjs +19 -2
- package/dist/src/cli/command-log.js +2 -3
- package/dist/src/cli/handlers/artifacts.js +3 -0
- package/dist/src/cli/handlers/auth.js +3 -0
- package/dist/src/cli/handlers/decision.js +19 -12
- package/dist/src/cli/handlers/diagnose.js +4 -3
- package/dist/src/cli/handlers/effect.js +4 -2
- package/dist/src/cli/handlers/evidence.js +3 -0
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/init.js +4 -1
- package/dist/src/cli/handlers/lint.js +5 -0
- package/dist/src/cli/handlers/models.js +104 -8
- package/dist/src/cli/handlers/ralph-launcher.js +2 -0
- package/dist/src/cli/handlers/refresh.js +12 -0
- package/dist/src/cli/handlers/resource-versions.js +3 -0
- package/dist/src/cli/handlers/rules.js +195 -0
- package/dist/src/cli/handlers/run.js +4 -0
- package/dist/src/cli/handlers/runtime-info.js +1 -2
- package/dist/src/cli/handlers/script-runner.js +3 -2
- package/dist/src/cli/handlers/session.js +19 -9
- package/dist/src/cli/handlers/sessions.js +98 -37
- package/dist/src/cli/handlers/setup-manifest.js +39 -44
- package/dist/src/cli/handlers/setup.js +41 -0
- package/dist/src/cli/handlers/steward.js +11 -0
- package/dist/src/cli/handlers/subcommands.js +57 -2
- package/dist/src/cli/handlers/uhp.js +3 -0
- package/dist/src/cli/handlers/use.js +294 -55
- package/dist/src/cli/handlers/utilities.js +26 -10
- package/dist/src/cli/handlers/version.js +8 -0
- package/dist/src/cli/router.js +15 -1
- package/dist/src/cli/services/deployment-verification.js +19 -9
- package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
- package/dist/src/cli/skill-usage.js +2 -3
- package/dist/src/config/aiwg-config.js +87 -37
- package/dist/src/config/cli.js +111 -5
- package/dist/src/config/decision-config.js +106 -0
- package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
- package/dist/src/config/gitignore.js +50 -0
- package/dist/src/config/index-graph-overrides.js +36 -0
- package/dist/src/config/project-artifacts-health.d.mts +37 -0
- package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
- package/dist/src/config/user-config-dir.d.mts +9 -0
- package/dist/src/config/user-config.js +2 -1
- package/dist/src/config/user-registry.js +23 -15
- package/dist/src/config/user-state.js +7 -0
- package/dist/src/decision/ask.js +203 -43
- package/dist/src/decision/context-live-qualification.js +2 -2
- package/dist/src/decision/driver.js +13 -8
- package/dist/src/decision/ensemble/aggregate.js +23 -10
- package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
- package/dist/src/decision/entry.js +2 -15
- package/dist/src/decision/graph-live-qualification.js +1 -1
- package/dist/src/decision/graph-run.js +9 -0
- package/dist/src/decision/heldout/collector.js +5 -13
- package/dist/src/decision/heldout/contract.js +30 -7
- package/dist/src/decision/jev-credentials.js +15 -1
- package/dist/src/decision/ledger.js +131 -28
- package/dist/src/decision/offload-activation.js +137 -0
- package/dist/src/decision/offload.js +550 -0
- package/dist/src/decision/qualification/load.js +4 -6
- package/dist/src/decision/savings.js +28 -0
- package/dist/src/effects/types.js +5 -1
- package/dist/src/effects/verifiers/git.js +2 -1
- package/dist/src/effects/verifiers/tracker.js +26 -14
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/extensions/project-local-remove.js +36 -1
- package/dist/src/gates/providers/loader.js +61 -17
- package/dist/src/installation/manager-command.d.mts +18 -0
- package/dist/src/installation/manager-command.mjs +14 -2
- package/dist/src/installation/manager.d.mts +49 -0
- package/dist/src/installation/manager.mjs +78 -3
- package/dist/src/issues/cli.js +4 -4
- package/dist/src/issues/local.js +24 -5
- package/dist/src/jobs/runner.js +1 -1
- package/dist/src/lint/cli.js +27 -1
- package/dist/src/lint/loader.js +2 -1
- package/dist/src/lint/reporters.js +25 -1
- package/dist/src/lint/rule-activation.js +43 -0
- package/dist/src/lint/runner.js +137 -62
- package/dist/src/mcp/adapters/codex-runtime.js +7 -1
- package/dist/src/mcp/claude-install.d.mts +4 -0
- package/dist/src/mcp/claude-install.mjs +41 -0
- package/dist/src/mcp/cli.d.mts +1 -0
- package/dist/src/mcp/cli.mjs +36 -11
- package/dist/src/mcp/config-file.d.mts +28 -0
- package/dist/src/mcp/config-layers.d.mts +36 -0
- package/dist/src/mcp/credentials.d.mts +51 -0
- package/dist/src/mcp/grok-build-config.d.mts +6 -0
- package/dist/src/mcp/grok-build-config.mjs +1 -1
- package/dist/src/mcp/helpers.mjs +3 -2
- package/dist/src/mcp/muse-native-config.d.mts +13 -0
- package/dist/src/mcp/muse-native-config.mjs +1 -0
- package/dist/src/mcp/omp-config.d.mts +4 -0
- package/dist/src/mcp/registry.js +3 -93
- package/dist/src/mcp/registry.mjs +3 -87
- package/dist/src/mcp/server-config.d.mts +3 -0
- package/dist/src/mcp/server-config.mjs +89 -0
- package/dist/src/mcp/server.mjs +37 -375
- package/dist/src/mcp/test-simple.mjs +2 -1
- package/dist/src/mcp/toml-editor.d.mts +4 -0
- package/dist/src/mcp/toml-string.d.mts +1 -0
- package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
- package/dist/src/mcp/tool-filters.d.mts +46 -0
- package/dist/src/mcp/tools/decision.mjs +12 -11
- package/dist/src/mcp/tools/discovery.mjs +62 -55
- package/dist/src/mcp/tools/orchestration.mjs +12 -2
- package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
- package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
- package/dist/src/mission-protocol/codecs.js +8 -0
- package/dist/src/models/config-loader.js +7 -2
- package/dist/src/models/model-capabilities.v1.json +96 -4
- package/dist/src/models/model-discovery.js +76 -11
- package/dist/src/models/provider-policy.js +101 -7
- package/dist/src/models/qualification-digest.d.mts +2 -0
- package/dist/src/models/resolver.js +12 -10
- package/dist/src/models/worker-direct-readiness.js +65 -44
- package/dist/src/models/worker-dispatch-offer.js +3 -3
- package/dist/src/models/worker-health-readiness.js +16 -3
- package/dist/src/models/worker-installation.d.mts +39 -0
- package/dist/src/models/worker-installation.mjs +135 -28
- package/dist/src/process/bounded-child-process.d.mts +84 -0
- package/dist/src/process/bounded-child-process.mjs +43 -0
- package/dist/src/process/cli-command.d.mts +16 -0
- package/dist/src/process/signal-child.d.mts +6 -0
- package/dist/src/process/signal-child.mjs +7 -0
- package/dist/src/providers/deployed-skill-roots.js +20 -0
- package/dist/src/providers/grok-build-acp.js +3 -2
- package/dist/src/providers/grok-build-headless.js +12 -4
- package/dist/src/providers/grok-build-inspect.js +1 -0
- package/dist/src/providers/omp-agent.d.mts +8 -0
- package/dist/src/providers/omp-diagnostics.d.mts +1 -0
- package/dist/src/providers/omp-paths.d.mts +13 -0
- package/dist/src/providers/skill-ownership.d.mts +9 -0
- package/dist/src/providers/skill-ownership.mjs +88 -0
- package/dist/src/rules/activation.d.mts +21 -0
- package/dist/src/rules/activation.mjs +62 -0
- package/dist/src/rules/inline-budget.d.mts +26 -0
- package/dist/src/rules/inline-budget.mjs +66 -0
- package/dist/src/rules/inventory.js +129 -0
- package/dist/src/rules/pin-state.js +73 -0
- package/dist/src/rules/pins.d.mts +14 -0
- package/dist/src/rules/pins.mjs +112 -0
- package/dist/src/rules/rule-frontmatter.schema.json +197 -0
- package/dist/src/serve/mission-conductor.js +38 -12
- package/dist/src/serve/worker-invocation.js +30 -10
- package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
- package/dist/src/sessions/adapter-registry.js +23 -19
- package/dist/src/sessions/adapters/claude.js +39 -54
- package/dist/src/sessions/adapters/codex.js +82 -67
- package/dist/src/sessions/adapters/copilot.js +4 -4
- package/dist/src/sessions/adapters/cursor.js +40 -57
- package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
- package/dist/src/sessions/adapters/factory.js +31 -27
- package/dist/src/sessions/adapters/generic.js +10 -10
- package/dist/src/sessions/adapters/grok-build.js +11 -11
- package/dist/src/sessions/adapters/grokbot.js +24 -13
- package/dist/src/sessions/adapters/hermes.js +27 -29
- package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
- package/dist/src/sessions/adapters/muse.js +150 -93
- package/dist/src/sessions/adapters/omp.js +28 -14
- package/dist/src/sessions/adapters/openclaw.js +89 -79
- package/dist/src/sessions/adapters/opencode.js +27 -24
- package/dist/src/sessions/adapters/openhuman.js +18 -60
- package/dist/src/sessions/adapters/pi.js +29 -14
- package/dist/src/sessions/adapters/warp.js +3 -3
- package/dist/src/sessions/adapters/windsurf.js +19 -48
- package/dist/src/sessions/batch-contracts.js +16 -8
- package/dist/src/sessions/batch-import.js +51 -11
- package/dist/src/sessions/contracts.js +40 -2
- package/dist/src/sessions/coverage-preflight.js +1 -1
- package/dist/src/sessions/export-plan.js +4 -3
- package/dist/src/sessions/import-exclusions.js +72 -0
- package/dist/src/sessions/import-lease.js +34 -18
- package/dist/src/sessions/import-limits.js +37 -0
- package/dist/src/sessions/importer.js +60 -38
- package/dist/src/sessions/lifecycle.js +45 -0
- package/dist/src/sessions/policy.js +133 -25
- package/dist/src/sessions/readers.js +106 -53
- package/dist/src/sessions/refresh.js +106 -0
- package/dist/src/sessions/repository.js +147 -52
- package/dist/src/sessions/workspace-discovery.js +221 -56
- package/dist/src/sessions/workspace-matching.js +107 -0
- package/dist/src/skills/importer.js +1 -0
- package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
- package/dist/src/smiths/context-pipeline/finalization.js +6 -5
- package/dist/src/smiths/context-pipeline/generator.js +21 -4
- package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
- package/dist/src/smiths/mcpsmith/generator.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
- package/dist/src/storage/backends/fs.js +18 -76
- package/dist/src/storage/backends/obsidian.js +1 -0
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/update/checker.d.mts +2 -0
- package/dist/src/update/service.d.mts +22 -0
- package/dist/src/utils/contained-filesystem.d.mts +37 -0
- package/dist/src/utils/contained-filesystem.mjs +683 -0
- package/dist/src/utils/temporary-directory.js +36 -0
- package/dist/src/writing/example-generator.js +1 -1
- package/package.json +15 -2
- package/schemas/models/model-catalog.v1.schema.json +1 -1
- package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
- package/tools/_resolve-impl.mjs +13 -15
- package/tools/agents/deploy-agents.mjs +49 -21
- package/tools/agents/providers/base.mjs +104 -134
- package/tools/agents/providers/claude.mjs +8 -24
- package/tools/agents/providers/codex.mjs +18 -11
- package/tools/agents/providers/cursor.mjs +19 -18
- package/tools/agents/providers/grok-build.mjs +3 -1
- package/tools/agents/providers/hermes.mjs +57 -47
- package/tools/agents/providers/openclaw.mjs +9 -4
- package/tools/agents/providers/pi.mjs +5 -0
- package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
- package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
- package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
- package/tools/agents/providers/rule-delivery.mjs +1270 -0
- package/tools/agents/providers/warp.mjs +20 -26
- package/tools/cli/decision-offload-report.mjs +40 -0
- package/tools/cli/workspace-status.mjs +862 -0
- package/tools/plugin/package-plugins.mjs +186 -8
- package/tools/plugin/plugin-links.mjs +114 -0
- package/tools/providers/antigravity-transport.mjs +3 -1
- package/tools/providers/deepseek-harness-transport.mjs +6 -4
- package/tools/providers/omp-transport.mjs +4 -2
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
- package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
- package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
- package/tools/ralph-external/lib/session-environment.mjs +28 -0
- package/tools/ralph-external/lib/systemd-client.mjs +49 -0
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
- package/tools/ralph-external/process-monitor.mjs +1017 -0
- package/tools/ralph-external/session-launcher.mjs +5057 -0
- package/tools/scripts/resource-preflight.mjs +541 -0
- package/tools/skills/deploy-skills-codex.mjs +2 -1
- package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared installed-worker qualification owner, also used by the Ralph compatibility facade. */
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
3
4
|
import { open, realpath, lstat, mkdir, rename, writeFile } from 'node:fs/promises';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { homedir } from 'node:os';
|
|
@@ -8,6 +9,21 @@ import { createHash } from 'node:crypto';
|
|
|
8
9
|
import { parse } from 'acorn';
|
|
9
10
|
import { satisfies, valid, validRange } from 'semver';
|
|
10
11
|
|
|
12
|
+
// Recovery metadata belongs to the installed driver identity, never its shape.
|
|
13
|
+
const workerRecoveryCapabilities = new WeakMap();
|
|
14
|
+
|
|
15
|
+
/** Register capabilities for an installed or caller-supplied driver. Keep the
|
|
16
|
+
* original driver when passing it to readers; copies must be registered anew. */
|
|
17
|
+
export function registerWorkerRecoveryCapabilities(driver, capabilities) {
|
|
18
|
+
workerRecoveryCapabilities.set(driver, Object.freeze({ ...capabilities }));
|
|
19
|
+
return driver;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Gated readers use this single lookup; ordinary driver properties are ignored. */
|
|
23
|
+
export function getWorkerRecoveryCapabilities(driver) {
|
|
24
|
+
return workerRecoveryCapabilities.get(driver) ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
// Shipped negative-suite definition. Evidence producers hash this definition,
|
|
12
28
|
// including case semantics and permission rows; arbitrary fixture labels cannot
|
|
13
29
|
// qualify an installation. Passed observations still come from the trusted host.
|
|
@@ -84,7 +100,7 @@ function assertWorkerControlArgv(profile, argv) {
|
|
|
84
100
|
|
|
85
101
|
|
|
86
102
|
async function assertWorkerInstallationEvidence(request, scopeDigest) {
|
|
87
|
-
const { DEFAULT_WORKER_SELECTION } = await import('./provider-policy.js');
|
|
103
|
+
const { DEFAULT_WORKER_SELECTION, WORKER_OPERATOR_APPROVAL, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
|
|
88
104
|
const { evidence, now } = request;
|
|
89
105
|
if (!Number.isFinite(now) || !evidence || !/^sha256:[a-f0-9]{64}$/.test(scopeDigest) || evidence.scopeDigest !== scopeDigest
|
|
90
106
|
|| evidence.fixtureDigest !== await workerNegativeFixtureDigest()
|
|
@@ -95,17 +111,21 @@ async function assertWorkerInstallationEvidence(request, scopeDigest) {
|
|
|
95
111
|
if (now - Date.parse(evidence.verifiedAt) >= (request.policy?.freshness?.policyDays ?? DEFAULT_WORKER_SELECTION.freshness.policyDays) * 86400000) {
|
|
96
112
|
throw new Error('Installation evidence expired');
|
|
97
113
|
}
|
|
114
|
+
// Decision 17's direct CLI baseline records operator approval, never claims
|
|
115
|
+
// that a version probe ran the negative suite. Stronger host evidence keeps
|
|
116
|
+
// its existing checks below.
|
|
117
|
+
if (evidence.evidenceRef === WORKER_OPERATOR_APPROVAL && isOperatorQualifiedWorkerAdapter(request.adapter)) return;
|
|
98
118
|
const { cases: requiredCases } = await workerPermissionRequirements(request.adapter.provider, request.requiredControls);
|
|
99
119
|
if (requiredCases.some(id => evidence.cases?.[id] !== 'passed')) {
|
|
100
120
|
throw new Error('Installation negative evidence is incomplete');
|
|
101
121
|
}
|
|
102
122
|
}
|
|
103
123
|
|
|
104
|
-
/** Read-only recheck
|
|
124
|
+
/** Read-only recheck of the approved installation bytes and bindings. */
|
|
105
125
|
async function recheckWorkerInstallation(instance, options) {
|
|
106
126
|
const request = structuredClone(options);
|
|
107
|
-
const { requiredWorkerControls } = await import('./provider-policy.js');
|
|
108
|
-
request.requiredControls = requiredWorkerControls(request);
|
|
127
|
+
const { requiredWorkerControls, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
|
|
128
|
+
if (!isOperatorQualifiedWorkerAdapter(request.adapter)) request.requiredControls = requiredWorkerControls(request);
|
|
109
129
|
const scope = await inspectWorkerInstallation(instance, request);
|
|
110
130
|
await assertWorkerInstallationEvidence(request, scope.scopeDigest);
|
|
111
131
|
return { scope, request };
|
|
@@ -143,9 +163,9 @@ export async function qualifyWorkerInstallation(instance, options) {
|
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
export async function prepareInstalledWorkerInvocation(instance, invocation, qualification) {
|
|
146
|
-
const { prepareWorkerInvocation } = await import('../serve/worker-invocation.js');
|
|
147
|
-
const { requiredWorkerControls, WorkerPolicyError } = await import('./provider-policy.js');
|
|
148
|
-
qualification = { ...qualification, requiredControls: requiredWorkerControls({
|
|
166
|
+
const { prepareWorkerInvocation, WorkerPathScopeError } = await import('../serve/worker-invocation.js');
|
|
167
|
+
const { requiredWorkerControls, WorkerPolicyError, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
|
|
168
|
+
if (!isOperatorQualifiedWorkerAdapter(qualification.adapter)) qualification = { ...qualification, requiredControls: requiredWorkerControls({
|
|
149
169
|
...qualification, effort: invocation.selected.effort.value }) };
|
|
150
170
|
const result = await prepareWorkerInvocation(invocation, { ...qualification, adapterId: instance.getName() });
|
|
151
171
|
const profile = qualification.adapter.invocationProfiles[result.selected.profileRef];
|
|
@@ -172,14 +192,16 @@ export async function prepareInstalledWorkerInvocation(instance, invocation, qua
|
|
|
172
192
|
for (const key of ['briefPath', 'stdoutPath', 'stderrPath', 'finalPath']) {
|
|
173
193
|
const target = await canonicalFuturePath(result[key]);
|
|
174
194
|
const roots = key === 'briefPath' ? scope.inputRoots : scope.outputRoots;
|
|
175
|
-
if (!roots.some(root => inside(root, target))) throw new
|
|
195
|
+
if (!roots.some(root => inside(root, target))) throw new WorkerPathScopeError(key, result[key], roots);
|
|
176
196
|
}
|
|
177
197
|
if (result.transport === 'shell') {
|
|
198
|
+
const driverInput = isOperatorQualifiedWorkerAdapter(qualification.adapter)
|
|
199
|
+
? { ...result, brief: (await readInstalledFile(result.briefPath)).toString('utf8') } : result;
|
|
178
200
|
const expectedArtifacts = typeof instance.buildWorkerArtifacts === 'function'
|
|
179
|
-
? instance.buildWorkerArtifacts({ ...
|
|
201
|
+
? instance.buildWorkerArtifacts({ ...driverInput, permissions: qualification.permissions }) : undefined;
|
|
180
202
|
if (JSON.stringify(expectedArtifacts) !== JSON.stringify(result.artifacts)) throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker artifacts differ from authorized driver mapping');
|
|
181
|
-
const workerArgs = instance.buildWorkerArgs(
|
|
182
|
-
assertWorkerControlArgv(profile, workerArgs);
|
|
203
|
+
const workerArgs = instance.buildWorkerArgs(driverInput, profile, qualification.permissions);
|
|
204
|
+
if (!isOperatorQualifiedWorkerAdapter(qualification.adapter)) assertWorkerControlArgv(profile, workerArgs);
|
|
183
205
|
const expected = [scope.binary.realpath, ...workerArgs];
|
|
184
206
|
if (JSON.stringify(expected) !== JSON.stringify(result.argv)
|
|
185
207
|
|| result.stdin !== qualification.adapter.invocation.stdin) {
|
|
@@ -215,6 +237,7 @@ async function binaryIdentity(filename, workerWritableRoots) {
|
|
|
215
237
|
const stat = await handle.stat();
|
|
216
238
|
if (!stat.isFile() || !(stat.mode & 0o111) || (stat.mode & 0o022)
|
|
217
239
|
|| (stat.nlink > 1 && (stat.mode & 0o222))) throw new Error('Untrusted binary permissions');
|
|
240
|
+
if (stat.size > 1024 * 1024 * 1024) throw new Error('Binary exceeds qualification byte bound (1 GiB)');
|
|
218
241
|
const hash = createHash('sha256');
|
|
219
242
|
// Incremental, bounded reads: never allocate the size reported by the binary.
|
|
220
243
|
const buffer = Buffer.alloc(64 * 1024);
|
|
@@ -223,7 +246,7 @@ async function binaryIdentity(filename, workerWritableRoots) {
|
|
|
223
246
|
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
224
247
|
if (!bytesRead) break;
|
|
225
248
|
total += bytesRead;
|
|
226
|
-
if (total >
|
|
249
|
+
if (total > 1024 * 1024 * 1024) throw new Error('Binary exceeds qualification byte bound');
|
|
227
250
|
hash.update(buffer.subarray(0, bytesRead));
|
|
228
251
|
}
|
|
229
252
|
return { realpath: canonical, sha256: `sha256:${hash.digest('hex')}` };
|
|
@@ -245,11 +268,12 @@ async function canonicalFuturePath(filename) {
|
|
|
245
268
|
}
|
|
246
269
|
|
|
247
270
|
export async function inspectWorkerInstallation(instance, request) {
|
|
248
|
-
const { inspectWorkerInvocationProfile, requiredWorkerControls } = await import('./provider-policy.js');
|
|
271
|
+
const { inspectWorkerInvocationProfile, requiredWorkerControls, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
|
|
249
272
|
const { qualificationDigest } = await import('./qualification-digest.mjs');
|
|
250
273
|
const { adapter, profile: profileId, cliVersion, workerWritableRoots,
|
|
251
274
|
effectiveConfigDigest, accountScope, boundary, cwd } = request;
|
|
252
|
-
const
|
|
275
|
+
const operatorApproved = isOperatorQualifiedWorkerAdapter(adapter);
|
|
276
|
+
const requiredControls = operatorApproved ? request.requiredControls ?? [] : requiredWorkerControls(request);
|
|
253
277
|
const platform = request.platform ?? process.platform;
|
|
254
278
|
if (platform !== 'linux' || adapter.id !== instance.getName()) throw new Error('Unsupported installation identity/platform');
|
|
255
279
|
if (!Array.isArray(requiredControls) || !Array.isArray(workerWritableRoots)
|
|
@@ -269,11 +293,11 @@ export async function inspectWorkerInstallation(instance, request) {
|
|
|
269
293
|
throw new Error('External control lacks an enforcing boundary');
|
|
270
294
|
}
|
|
271
295
|
const permissionRequirements = await workerPermissionRequirements(adapter.provider, requiredControls);
|
|
272
|
-
if (permissionRequirements.externalBoundary && boundary.kind === 'native') {
|
|
296
|
+
if (!operatorApproved && permissionRequirements.externalBoundary && boundary.kind === 'native') {
|
|
273
297
|
throw new Error('Profile requires an external enforcing boundary');
|
|
274
298
|
}
|
|
275
299
|
let controlArgv = null;
|
|
276
|
-
if (Object.values(profile.controls).some(hasControlArgvMapping)) {
|
|
300
|
+
if (!operatorApproved && Object.values(profile.controls).some(hasControlArgvMapping)) {
|
|
277
301
|
if (!request.invocation) throw new Error('Worker control qualification requires representative argv invocation');
|
|
278
302
|
controlArgv = instance.buildWorkerArgs(structuredClone(request.invocation), profile, request.permissions);
|
|
279
303
|
assertWorkerControlArgv(profile, controlArgv);
|
|
@@ -425,7 +449,7 @@ export function boundedVersionProbe(executable, argv, environment, cwd, timeoutM
|
|
|
425
449
|
let size = 0;
|
|
426
450
|
let failed = false;
|
|
427
451
|
const stop = () => {
|
|
428
|
-
if (child.pid) { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* already stopped */ } }
|
|
452
|
+
if (Number.isSafeInteger(child.pid) && child.pid > 0) { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* already stopped */ } }
|
|
429
453
|
};
|
|
430
454
|
// Never rely on close: an escaped descendant may retain the pipe descriptors.
|
|
431
455
|
const abort = () => {
|
|
@@ -564,7 +588,8 @@ async function inspectInstalledModule(entry, workerWritableRoots, platform) {
|
|
|
564
588
|
}
|
|
565
589
|
for (const target of [root, path.join(root, 'package.json'), ...entry.modulePath.split('/').map((_, i, parts) => path.join(root, ...parts.slice(0, i + 1)))]) {
|
|
566
590
|
const stat = await lstat(target);
|
|
567
|
-
if (stat.isSymbolicLink()
|
|
591
|
+
if (stat.isSymbolicLink()) throw new Error(`Symlink in installed module path: ${target}`);
|
|
592
|
+
if ((stat.mode & 0o022) !== 0) throw new Error(`Installed module path is group/other-writable: ${target}. Run: chmod -R go-w ${JSON.stringify(root)}`);
|
|
568
593
|
// Directory link counts reflect children, not file aliases. Hardlinked store
|
|
569
594
|
// files qualify only when no permission class can write the shared inode.
|
|
570
595
|
if (stat.isFile() && stat.nlink > 1 && (stat.mode & 0o222) !== 0) {
|
|
@@ -722,7 +747,29 @@ function installationStorePath() {
|
|
|
722
747
|
return path.join(root, 'worker-installations.json');
|
|
723
748
|
}
|
|
724
749
|
|
|
725
|
-
|
|
750
|
+
// HOME is the operator's trust boundary; custom stores outside it are checked
|
|
751
|
+
// back to /. O_NOFOLLOW on the store itself does not protect its ancestors.
|
|
752
|
+
async function assertTrustedStoreAncestors(directory) {
|
|
753
|
+
const home = path.resolve(homedir());
|
|
754
|
+
const boundary = inside(home, directory) ? home : path.parse(directory).root;
|
|
755
|
+
// The store directory itself is checked by the caller.
|
|
756
|
+
if (directory === boundary) return;
|
|
757
|
+
const ancestors = [];
|
|
758
|
+
for (let target = path.dirname(directory); ; target = path.dirname(target)) {
|
|
759
|
+
ancestors.push(target);
|
|
760
|
+
if (target === boundary) break;
|
|
761
|
+
}
|
|
762
|
+
for (const target of ancestors.reverse()) {
|
|
763
|
+
const stat = await lstat(target);
|
|
764
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.mode & 0o022
|
|
765
|
+
|| process.getuid && stat.uid !== 0 && stat.uid !== process.getuid()) {
|
|
766
|
+
throw new Error(`Untrusted installation store ancestor: ${target}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async function assertTrustedStore(filename, writableRoots, repairDirectory = false, apply = false) {
|
|
772
|
+
const changes = [];
|
|
726
773
|
const canonical = await canonicalFuturePath(filename);
|
|
727
774
|
if (canonical !== filename) throw new Error('Symlinked installation store');
|
|
728
775
|
for (const root of writableRoots) {
|
|
@@ -731,10 +778,35 @@ async function assertTrustedStore(filename, writableRoots) {
|
|
|
731
778
|
for (const target of [path.dirname(filename), filename]) {
|
|
732
779
|
try {
|
|
733
780
|
const stat = await lstat(target);
|
|
734
|
-
if (
|
|
735
|
-
|
|
781
|
+
if (process.getuid && stat.uid !== process.getuid()) throw new Error(`Installation store path is owned by another user: ${target}`);
|
|
782
|
+
if (stat.isSymbolicLink() || stat.isFile() && stat.nlink !== 1
|
|
783
|
+
|| target === path.dirname(filename) && !stat.isDirectory()
|
|
784
|
+
|| target === filename && !stat.isFile()) throw new Error(`Untrusted installation store permissions: ${target}`);
|
|
785
|
+
if (stat.mode & 0o022) {
|
|
786
|
+
if (!repairDirectory || target !== path.dirname(filename) || !process.getuid) {
|
|
787
|
+
throw new Error(`Untrusted installation store permissions: ${target}`);
|
|
788
|
+
}
|
|
789
|
+
await assertTrustedStoreAncestors(target);
|
|
790
|
+
changes.push({ path: target, action: 'chmod go-w' });
|
|
791
|
+
if (apply) {
|
|
792
|
+
const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_DIRECTORY);
|
|
793
|
+
try {
|
|
794
|
+
const current = await handle.stat();
|
|
795
|
+
if (current.uid !== process.getuid() || current.ino !== stat.ino || current.dev !== stat.dev) {
|
|
796
|
+
throw new Error(`Installation store directory changed: ${target}`);
|
|
797
|
+
}
|
|
798
|
+
await handle.chmod(current.mode & ~0o022);
|
|
799
|
+
} finally { await handle.close(); }
|
|
800
|
+
}
|
|
801
|
+
}
|
|
736
802
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
737
803
|
}
|
|
804
|
+
return changes;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Qualification's confirmed write may secure its own directory; ordinary readers never repair. */
|
|
808
|
+
export async function prepareWorkerInstallationStore({ workerWritableRoots, apply = false }) {
|
|
809
|
+
return assertTrustedStore(installationStorePath(), workerWritableRoots, true, apply);
|
|
738
810
|
}
|
|
739
811
|
|
|
740
812
|
async function readInstallationStore(filename, expectedDigest) {
|
|
@@ -775,7 +847,7 @@ export async function recordWorkerInstallation(instance, options) {
|
|
|
775
847
|
delete installation.environment;
|
|
776
848
|
delete installation.adapter;
|
|
777
849
|
delete installation.now;
|
|
778
|
-
const record = { cli, binding: { ...structuredClone(binding), boundary: installation.boundary }, adapter,
|
|
850
|
+
const record = { cli, ...(options.modelObservations ? { modelObservations: structuredClone(options.modelObservations) } : {}), binding: { ...structuredClone(binding), boundary: installation.boundary }, adapter,
|
|
779
851
|
installation: { ...installation, environmentNames, versionProbe: { rawBanner: result.rawVersionBanner, version: result.cliVersion } },
|
|
780
852
|
...(options.mcpSchemas ? { mcpSchemas: structuredClone(options.mcpSchemas) } : {}),
|
|
781
853
|
...(options.mcpProfile ? { mcpProfile: structuredClone(options.mcpProfile) } : {}) };
|
|
@@ -789,9 +861,9 @@ export async function recordWorkerInstallation(instance, options) {
|
|
|
789
861
|
return result;
|
|
790
862
|
}
|
|
791
863
|
|
|
792
|
-
/** Read recorded host authority, validate installation bytes
|
|
793
|
-
* approved factory used by Ralph.
|
|
794
|
-
export async function loadRecordedWorkerInstallations({ registry, cwd, policy, storePath, storeDigest }) {
|
|
864
|
+
/** Read recorded host authority, validate installation bytes and the current CLI
|
|
865
|
+
* version, then load the same approved factory used by Ralph. */
|
|
866
|
+
export async function loadRecordedWorkerInstallations({ registry, cwd, policy, parallelism, storePath, storeDigest }) {
|
|
795
867
|
const { WorkerPolicyError, parseProviderModelCapabilities } = await import('./provider-policy.js');
|
|
796
868
|
const { qualificationDigest } = await import('./qualification-digest.mjs');
|
|
797
869
|
const fail = () => new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED', 'Trusted installation qualification is required');
|
|
@@ -805,6 +877,7 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, s
|
|
|
805
877
|
catch { throw fail(); }
|
|
806
878
|
const installations = {};
|
|
807
879
|
const drivers = {};
|
|
880
|
+
const observations = {};
|
|
808
881
|
const resolvedRegistry = structuredClone(registry);
|
|
809
882
|
const counts = new Map();
|
|
810
883
|
for (const record of stored.registrations) counts.set(record?.cli, (counts.get(record?.cli) ?? 0) + 1);
|
|
@@ -825,8 +898,8 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, s
|
|
|
825
898
|
const proof = adapter.invocationProfiles?.[binding.profile]?.installationQualification;
|
|
826
899
|
if (!proof || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => proof[key] !== installation.evidence?.[key])) throw fail();
|
|
827
900
|
// Reject expired/unbound negative observations before any factory import.
|
|
828
|
-
const { requiredWorkerControls } = await import('./provider-policy.js');
|
|
829
|
-
installation.requiredControls = requiredWorkerControls(installation);
|
|
901
|
+
const { requiredWorkerControls, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
|
|
902
|
+
if (!isOperatorQualifiedWorkerAdapter(adapter)) installation.requiredControls = requiredWorkerControls(installation);
|
|
830
903
|
await assertWorkerInstallationEvidence(installation, proof.scopeDigest);
|
|
831
904
|
await assertTrustedStore(filename, installation.workerWritableRoots);
|
|
832
905
|
const candidateRegistry = structuredClone(resolvedRegistry);
|
|
@@ -842,7 +915,31 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, s
|
|
|
842
915
|
workerWritableRoots: installation.workerWritableRoots, platform: process.platform });
|
|
843
916
|
const instance = createProvider(adapter.id);
|
|
844
917
|
const { scope } = await recheckWorkerInstallation(instance, installation);
|
|
918
|
+
if (parallelism && isOperatorQualifiedWorkerAdapter(adapter)) {
|
|
919
|
+
const { resolveWorkerGroupInstallation } = await import('../config/aiwg-config.js');
|
|
920
|
+
const configured = resolveWorkerGroupInstallation(parallelism, cli, candidateRegistry);
|
|
921
|
+
let executable = configured.executable;
|
|
922
|
+
if (!path.isAbsolute(executable)) {
|
|
923
|
+
if (executable.includes(path.sep)) executable = path.resolve(cwd, executable);
|
|
924
|
+
else {
|
|
925
|
+
const candidates = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean).map(directory => path.resolve(directory, executable));
|
|
926
|
+
executable = null;
|
|
927
|
+
for (const candidate of candidates) {
|
|
928
|
+
try { await binaryIdentity(candidate, installation.workerWritableRoots); executable = candidate; break; } catch { /* Next PATH entry. */ }
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (!executable || await realpath(executable) !== scope.binary.realpath) throw fail();
|
|
933
|
+
}
|
|
845
934
|
if (scope.scopeDigest !== proof.scopeDigest) throw fail();
|
|
935
|
+
// A launcher can keep the same bytes while its CLI payload is updated.
|
|
936
|
+
// Reuse the bounded, credential-free doctor probe before granting authority.
|
|
937
|
+
const { version } = await boundedVersionProbe(scope.binary.realpath,
|
|
938
|
+
adapter.invocation.versionArgv, workerVersionProbeEnvironment(), cwd,
|
|
939
|
+
workerVersionProbeDefaults.timeoutMs, workerVersionProbeDefaults.maxBytes,
|
|
940
|
+
adapter.invocation.versionPattern);
|
|
941
|
+
if (version !== binding.cliVersion
|
|
942
|
+
|| (await inspectWorkerInstallation(instance, installation)).scopeDigest !== scope.scopeDigest) throw fail();
|
|
846
943
|
const profile = adapter.invocationProfiles[binding.profile];
|
|
847
944
|
let mcpRegistry;
|
|
848
945
|
if (profile.transport === 'mcp') {
|
|
@@ -870,10 +967,20 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, s
|
|
|
870
967
|
prepareWorkerInvocation: (invocation, qualification) => prepareInstalledWorkerInvocation(instance, invocation, qualification),
|
|
871
968
|
...(mcpRegistry ? { mcpRegistry, mcpProfile: record.mcpProfile } : {}),
|
|
872
969
|
};
|
|
970
|
+
registerWorkerRecoveryCapabilities(drivers[cli], {
|
|
971
|
+
...(typeof instance.parseWorkerAttemptFinal === 'function'
|
|
972
|
+
? { parseWorkerAttemptFinal: (output, options) => instance.parseWorkerAttemptFinal(output, options) } : {}),
|
|
973
|
+
// Adapters with neither an explicit signal nor an attempt parser cannot
|
|
974
|
+
// verify success (e.g. OpenHuman); never admit an unreachable canary.
|
|
975
|
+
canVerifyAttempt: typeof instance.supportsAttemptVerification === 'function'
|
|
976
|
+
? !!instance.supportsAttemptVerification()
|
|
977
|
+
: typeof instance.parseWorkerAttemptFinal === 'function',
|
|
978
|
+
});
|
|
873
979
|
installations[cli] = structuredClone(binding);
|
|
980
|
+
if (record.modelObservations) observations[cli] = structuredClone(record.modelObservations);
|
|
874
981
|
Object.assign(resolvedRegistry, candidateRegistry);
|
|
875
982
|
} catch { /* One missing/changed installation cannot grant authority or hide qualified peers. */ }
|
|
876
983
|
}
|
|
877
984
|
if (!Object.keys(installations).length) throw fail();
|
|
878
|
-
return { registry: resolvedRegistry, installations, drivers };
|
|
985
|
+
return { registry: resolvedRegistry, installations, drivers, observations };
|
|
879
986
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExecFileSyncOptions,
|
|
3
|
+
ExecFileSyncOptionsWithBufferEncoding,
|
|
4
|
+
ExecFileSyncOptionsWithStringEncoding,
|
|
5
|
+
ExecSyncOptions,
|
|
6
|
+
ExecSyncOptionsWithBufferEncoding,
|
|
7
|
+
ExecSyncOptionsWithStringEncoding,
|
|
8
|
+
SpawnSyncOptions,
|
|
9
|
+
SpawnSyncOptionsWithBufferEncoding,
|
|
10
|
+
SpawnSyncOptionsWithStringEncoding,
|
|
11
|
+
SpawnSyncReturns,
|
|
12
|
+
} from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
export type SyncProcessTier = 'quick' | 'network' | 'long';
|
|
15
|
+
|
|
16
|
+
export const AIWG_SYNC_LONG_TIMEOUT_MS_ENV: 'AIWG_SYNC_LONG_TIMEOUT_MS';
|
|
17
|
+
|
|
18
|
+
export const SYNC_PROCESS_TIER_BOUNDS_MS: Readonly<{
|
|
19
|
+
quick: number;
|
|
20
|
+
network: number;
|
|
21
|
+
long: number;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export function resolveSyncProcessTimeoutMs(tier: SyncProcessTier): number;
|
|
25
|
+
|
|
26
|
+
export function positiveTimeoutMs(value: unknown, fallbackMs: number): number;
|
|
27
|
+
|
|
28
|
+
// Overloads mirror Node's own execSync/execFileSync/spawnSync overload set
|
|
29
|
+
// (string vs Buffer return type keyed on `options.encoding`), with the
|
|
30
|
+
// required `tier` inserted right before `options` — same narrowing behavior
|
|
31
|
+
// callers get from the native APIs, e.g. `.trim()` on the result when
|
|
32
|
+
// `encoding: 'utf8'` is set.
|
|
33
|
+
|
|
34
|
+
export function boundedExecSync(command: string, tier: SyncProcessTier): Buffer;
|
|
35
|
+
export function boundedExecSync(command: string, tier: SyncProcessTier, options: ExecSyncOptionsWithStringEncoding): string;
|
|
36
|
+
export function boundedExecSync(command: string, tier: SyncProcessTier, options: ExecSyncOptionsWithBufferEncoding): Buffer;
|
|
37
|
+
export function boundedExecSync(command: string, tier: SyncProcessTier, options?: ExecSyncOptions): string | Buffer;
|
|
38
|
+
|
|
39
|
+
export function boundedExecFileSync(file: string, args: readonly string[], tier: SyncProcessTier): Buffer;
|
|
40
|
+
export function boundedExecFileSync(
|
|
41
|
+
file: string,
|
|
42
|
+
args: readonly string[],
|
|
43
|
+
tier: SyncProcessTier,
|
|
44
|
+
options: ExecFileSyncOptionsWithStringEncoding,
|
|
45
|
+
): string;
|
|
46
|
+
export function boundedExecFileSync(
|
|
47
|
+
file: string,
|
|
48
|
+
args: readonly string[],
|
|
49
|
+
tier: SyncProcessTier,
|
|
50
|
+
options: ExecFileSyncOptionsWithBufferEncoding,
|
|
51
|
+
): Buffer;
|
|
52
|
+
export function boundedExecFileSync(
|
|
53
|
+
file: string,
|
|
54
|
+
args: readonly string[],
|
|
55
|
+
tier: SyncProcessTier,
|
|
56
|
+
options?: ExecFileSyncOptions,
|
|
57
|
+
): string | Buffer;
|
|
58
|
+
|
|
59
|
+
export function boundedSpawnSync(command: string, args: readonly string[], tier: SyncProcessTier): SpawnSyncReturns<Buffer>;
|
|
60
|
+
export function boundedSpawnSync(
|
|
61
|
+
command: string,
|
|
62
|
+
args: readonly string[],
|
|
63
|
+
tier: SyncProcessTier,
|
|
64
|
+
options: SpawnSyncOptionsWithStringEncoding,
|
|
65
|
+
): SpawnSyncReturns<string>;
|
|
66
|
+
export function boundedSpawnSync(
|
|
67
|
+
command: string,
|
|
68
|
+
args: readonly string[],
|
|
69
|
+
tier: SyncProcessTier,
|
|
70
|
+
options: SpawnSyncOptionsWithBufferEncoding,
|
|
71
|
+
): SpawnSyncReturns<Buffer>;
|
|
72
|
+
export function boundedSpawnSync(
|
|
73
|
+
command: string,
|
|
74
|
+
args: readonly string[],
|
|
75
|
+
tier: SyncProcessTier,
|
|
76
|
+
options?: SpawnSyncOptions,
|
|
77
|
+
): SpawnSyncReturns<string | Buffer>;
|
|
78
|
+
|
|
79
|
+
/** Async launch with direct-child timeout and cleanup before termination. */
|
|
80
|
+
export function boundedSpawn(
|
|
81
|
+
command: string, args: readonly string[], tier: SyncProcessTier,
|
|
82
|
+
options: import('node:child_process').SpawnOptions,
|
|
83
|
+
onTermination?: () => void,
|
|
84
|
+
): Promise<{ status: number | null; signal: NodeJS.Signals | null; error?: Error; pid?: number }>;
|
|
@@ -70,10 +70,12 @@
|
|
|
70
70
|
*/
|
|
71
71
|
|
|
72
72
|
import {
|
|
73
|
+
spawn,
|
|
73
74
|
execSync as nodeExecSync,
|
|
74
75
|
execFileSync as nodeExecFileSync,
|
|
75
76
|
spawnSync as nodeSpawnSync,
|
|
76
77
|
} from 'node:child_process';
|
|
78
|
+
import { signalChild } from './signal-child.mjs';
|
|
77
79
|
|
|
78
80
|
/** Env var that raises or disables the `long` tier bound. `0` disables it (no timeout). */
|
|
79
81
|
export const AIWG_SYNC_LONG_TIMEOUT_MS_ENV = 'AIWG_SYNC_LONG_TIMEOUT_MS';
|
|
@@ -385,3 +387,44 @@ export function boundedSpawnSync(command, args, tier, options) {
|
|
|
385
387
|
enrichTimeoutError(result.error, { description: describeCommand(command, args), tier, boundMs, signal: result.signal });
|
|
386
388
|
return result;
|
|
387
389
|
}
|
|
390
|
+
|
|
391
|
+
/** Async counterpart for launches that must clean owned files on termination.
|
|
392
|
+
* Like the sync wrappers, this supervises the direct child only. */
|
|
393
|
+
export async function boundedSpawn(command, args, tier, options, onTermination = () => {}) {
|
|
394
|
+
assertValidTier(tier);
|
|
395
|
+
const boundMs = effectiveTimeoutMs(tier, options);
|
|
396
|
+
const { timeout: _timeout, killSignal: _killSignal, ...nodeOptions } = toNodeOptions(options, boundMs);
|
|
397
|
+
let child;
|
|
398
|
+
let timer;
|
|
399
|
+
let timedOut = false;
|
|
400
|
+
const handlers = new Map();
|
|
401
|
+
try {
|
|
402
|
+
child = spawn(command, args, nodeOptions);
|
|
403
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
404
|
+
const handler = () => {
|
|
405
|
+
try { onTermination(); }
|
|
406
|
+
finally {
|
|
407
|
+
signalChild(child, 'SIGKILL');
|
|
408
|
+
// Restore the signal's normal exit semantics after cleanup.
|
|
409
|
+
process.removeListener(signal, handler);
|
|
410
|
+
process.kill(process.pid, signal);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
handlers.set(signal, handler);
|
|
414
|
+
process.once(signal, handler);
|
|
415
|
+
}
|
|
416
|
+
if (boundMs > 0) timer = setTimeout(() => { timedOut = true; signalChild(child, 'SIGKILL'); }, boundMs);
|
|
417
|
+
return await new Promise(resolve => {
|
|
418
|
+
let spawnError;
|
|
419
|
+
child.once('error', error => { spawnError = error; });
|
|
420
|
+
child.once('close', (status, signal) => {
|
|
421
|
+
const error = timedOut ? Object.assign(new Error(`Command timed out after ${boundMs}ms`), { code: 'ETIMEDOUT' }) : spawnError;
|
|
422
|
+
enrichTimeoutError(error, { description: describeCommand(command, args), tier, boundMs, signal });
|
|
423
|
+
resolve({ status, signal, error, pid: child.pid, stdout: null, stderr: null, output: [] });
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
} finally {
|
|
427
|
+
clearTimeout(timer);
|
|
428
|
+
for (const [signal, handler] of handlers) process.removeListener(signal, handler);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function resolveExecutable(
|
|
2
|
+
command: string,
|
|
3
|
+
platform?: NodeJS.Platform,
|
|
4
|
+
searchPath?: string,
|
|
5
|
+
pathExt?: string,
|
|
6
|
+
cwd?: string,
|
|
7
|
+
): string;
|
|
8
|
+
|
|
9
|
+
export function cliCommand(
|
|
10
|
+
command: string,
|
|
11
|
+
args: string[],
|
|
12
|
+
platform?: NodeJS.Platform,
|
|
13
|
+
searchPath?: string,
|
|
14
|
+
pathExt?: string,
|
|
15
|
+
cwd?: string,
|
|
16
|
+
): [string, string[]];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Signal only a child that acquired a real PID. Failed spawns can retain a
|
|
2
|
+
* native handle with PID zero until their asynchronous error event arrives.
|
|
3
|
+
* Keep native return/throw behavior so callers retain their stop semantics. */
|
|
4
|
+
export function signalChild(child, signal) {
|
|
5
|
+
if (!Number.isSafeInteger(child?.pid) || child.pid <= 0) return false;
|
|
6
|
+
return !!child.kill(signal);
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { getProviderArtifactPathStrings, getProviderKernelSkillPath } from './provider-definitions.js';
|
|
3
|
+
import { resolveGrokbotSkillsDir } from './grokbot-paths.js';
|
|
4
|
+
/** All shared-copier tiers, including adapter mirrors. */
|
|
5
|
+
export function deployedSkillRoots(target, provider) {
|
|
6
|
+
const roots = [(getProviderArtifactPathStrings(provider) ?? getProviderArtifactPathStrings('claude'))?.skills,
|
|
7
|
+
getProviderKernelSkillPath(provider) || getProviderKernelSkillPath('claude')];
|
|
8
|
+
if (provider === 'muse')
|
|
9
|
+
roots.push('.agents/.aiwg/skills');
|
|
10
|
+
if (provider === 'grok-build')
|
|
11
|
+
roots.push('.grok/.aiwg/skills');
|
|
12
|
+
if (provider === 'grokbot') {
|
|
13
|
+
const root = resolveGrokbotSkillsDir();
|
|
14
|
+
if (root)
|
|
15
|
+
roots.push(root, path.join(root, '.aiwg', 'skills'));
|
|
16
|
+
}
|
|
17
|
+
return [...new Set(roots.filter((root) => Boolean(root))
|
|
18
|
+
.map(root => path.isAbsolute(root) ? root : path.join(target, root)))];
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=deployed-skill-roots.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Grok Build ACP stdio client. Separate from one-shot headless execution. */
|
|
2
|
+
import { signalChild } from '../process/signal-child.mjs';
|
|
2
3
|
import { spawn } from 'node:child_process';
|
|
3
4
|
import { redactGrokOutput } from './grok-build-headless.js';
|
|
4
5
|
export class GrokAcpClient {
|
|
@@ -182,9 +183,9 @@ export class GrokAcpClient {
|
|
|
182
183
|
this.terminated = true;
|
|
183
184
|
this.failAll(new Error('Grok ACP connection closed'));
|
|
184
185
|
this.child.stdin.end();
|
|
185
|
-
if (!this.child
|
|
186
|
+
if (!signalChild(this.child, 'SIGTERM'))
|
|
186
187
|
return;
|
|
187
|
-
const forceTimer = setTimeout(() => this.child
|
|
188
|
+
const forceTimer = setTimeout(() => signalChild(this.child, 'SIGKILL'), 2_000);
|
|
188
189
|
forceTimer.unref();
|
|
189
190
|
this.child.once('close', () => clearTimeout(forceTimer));
|
|
190
191
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/** Bounded Grok Build headless transport. The caller owns task authorization. */
|
|
2
|
+
import { signalChild } from '../process/signal-child.mjs';
|
|
2
3
|
import { spawn } from 'node:child_process';
|
|
3
4
|
const MAX_TIMEOUT_MS = 10 * 60_000;
|
|
4
5
|
const DEFAULT_TIMEOUT_MS = 2 * 60_000;
|
|
5
6
|
const DEFAULT_MAX_OUTPUT = 8 * 1024 * 1024;
|
|
7
|
+
// grok 1.0.50 --output-format streaming-json: ACP session updates.
|
|
8
|
+
// See docs/providers/grok-build-cli-evidence.md; json is a separate result object.
|
|
9
|
+
const STREAM_EVENT_TYPES = new Set(['text', 'thought', 'tool_call', 'tool_call_update',
|
|
10
|
+
'usage', 'plan', 'available_commands', 'end', 'error']);
|
|
6
11
|
export function redactGrokOutput(value, env = process.env) {
|
|
7
12
|
let redacted = value
|
|
8
13
|
.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/-]+/gi, '$1[REDACTED]')
|
|
@@ -28,14 +33,14 @@ function isError(event) {
|
|
|
28
33
|
|| (['tool_call', 'tool_call_update'].includes(String(event.type)) && event.status === 'failed');
|
|
29
34
|
}
|
|
30
35
|
function stopProcess(child) {
|
|
31
|
-
if (child.pid && process.platform !== 'win32') {
|
|
36
|
+
if (Number.isSafeInteger(child.pid) && child.pid > 0 && process.platform !== 'win32') {
|
|
32
37
|
try {
|
|
33
38
|
process.kill(-child.pid, 'SIGTERM');
|
|
34
39
|
return;
|
|
35
40
|
}
|
|
36
41
|
catch { /* process already exited */ }
|
|
37
42
|
}
|
|
38
|
-
child
|
|
43
|
+
signalChild(child, 'SIGTERM');
|
|
39
44
|
}
|
|
40
45
|
export async function runGrokHeadless(options) {
|
|
41
46
|
if (!options.prompt.trim())
|
|
@@ -90,7 +95,9 @@ export async function runGrokHeadless(options) {
|
|
|
90
95
|
if (format === 'json') {
|
|
91
96
|
try {
|
|
92
97
|
const parsed = JSON.parse(cleanStdout);
|
|
93
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
|
|
98
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
|
|
99
|
+
|| typeof parsed.text !== 'string' || isError(parsed)
|
|
100
|
+
|| parsed.stopReason !== 'end_turn')
|
|
94
101
|
throw new Error('Grok Build returned a JSON error result');
|
|
95
102
|
events.push(parsed);
|
|
96
103
|
}
|
|
@@ -110,7 +117,8 @@ export async function runGrokHeadless(options) {
|
|
|
110
117
|
const acceptEvent = (event) => {
|
|
111
118
|
if (settled)
|
|
112
119
|
return;
|
|
113
|
-
if (!event || typeof event !== 'object' || Array.isArray(event)
|
|
120
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)
|
|
121
|
+
|| typeof event.type !== 'string' || !STREAM_EVENT_TYPES.has(event.type)) {
|
|
114
122
|
finish(new Error('Invalid Grok Build streaming-json event'));
|
|
115
123
|
return;
|
|
116
124
|
}
|
|
@@ -177,6 +177,7 @@ export function runGrokInspect(envOrOptions = process.env) {
|
|
|
177
177
|
// npm exposes Grok through a .cmd shim on Windows; Node requires a shell
|
|
178
178
|
// for .cmd/.bat launchers. Native .exe installs stay shell-free.
|
|
179
179
|
shell: process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(binary),
|
|
180
|
+
killSignal: 'SIGKILL',
|
|
180
181
|
});
|
|
181
182
|
if (result.error && result.error.code === 'ENOENT') {
|
|
182
183
|
return { status: 'absent', remediation: ABSENT_REMEDIATION };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface OmpAgentOptions {
|
|
2
|
+
diagnostics?: string[];
|
|
3
|
+
quiet?: boolean;
|
|
4
|
+
modelsConfig?: Record<string, any>;
|
|
5
|
+
modelConfig?: Record<string, any>;
|
|
6
|
+
}
|
|
7
|
+
export function transformAgent(src: string, content: string, opts?: OmpAgentOptions): string;
|
|
8
|
+
export function mapModel(model: string | undefined, config?: Record<string, any>): string | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function diagnoseOmpRuntime(options?: {cwd?: string; env?: NodeJS.ProcessEnv; binary?: string; runner?: (...args: any[]) => Promise<{stdout: string}>}): Promise<{executable: string; available: boolean; version: string | null; paths: ReturnType<typeof import('./omp-paths.mjs').resolveOmpPaths>; reason: string | null}>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface OmpPathOptions {
|
|
2
|
+
env?: NodeJS.ProcessEnv;
|
|
3
|
+
home?: string;
|
|
4
|
+
cwd?: string;
|
|
5
|
+
platform?: string;
|
|
6
|
+
exists?: (path: string) => boolean;
|
|
7
|
+
}
|
|
8
|
+
export function normalizeOmpProfile(value?: string): string | undefined;
|
|
9
|
+
export function resolveOmpPaths(options?: OmpPathOptions): {
|
|
10
|
+
profile?: string; configRoot: string; agentDir: string; projectDir: string;
|
|
11
|
+
resourceDirs: { agents: string; skills: string; commands: string; rules: string; behaviors: string };
|
|
12
|
+
dataDir: string; stateDir: string; cacheDir: string; sessionsDir: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SkillReceipt {
|
|
2
|
+
skillSource?: string;
|
|
3
|
+
managed?: Record<string, { hash: string; frameworkSlug?: string; source?: string; version?: string }>;
|
|
4
|
+
}
|
|
5
|
+
export function extractFrameworkSlug(source: string): string | null;
|
|
6
|
+
export function sameSkillOwner(receipt: SkillReceipt | null, skillSource: string, frameworkSlug?: string | null): boolean;
|
|
7
|
+
export function canDeploySkill(receipt: SkillReceipt | null, skillSource: string, opts?: { force?: boolean }): boolean;
|
|
8
|
+
export function pruneRecordedSkillFiles(skillDir: string, managed: SkillReceipt['managed'], desired: Set<string>, opts?: { dryRun?: boolean }): void;
|
|
9
|
+
export function releaseSkillOwnership(skillDir: string, owner: { frameworkSlug?: string; skillSource?: string; sourceRoot?: string }, opts?: { dryRun?: boolean }): boolean | null;
|