@aiwg/cli 2026.10.5 → 2026.10.7
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/addons/composition-engine/lib/runtime.mjs +1401 -0
- package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
- package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
- package/agentic/code/providers/model-capabilities.v1.json +74 -50
- package/bin/aiwg.mjs +7 -3
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +57 -6
- package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
- package/dist/src/artifacts/fortemi-core-sync.js +26 -8
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +143 -14
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +114 -55
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/memory-maintenance.js +503 -0
- package/dist/src/artifacts/prebuilt-relations.js +3 -1
- package/dist/src/artifacts/query-engine.js +263 -108
- package/dist/src/artifacts/relations-builder.js +2 -2
- package/dist/src/artifacts/source-graph.js +2 -2
- package/dist/src/artifacts/types.js +9 -1
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/decision.js +22 -3
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +470 -0
- package/dist/src/cli/handlers/help.js +2 -2
- package/dist/src/cli/handlers/housekeeper.js +1110 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/init.js +41 -6
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/local-executor.js +5 -3
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/mc.js +509 -53
- package/dist/src/cli/handlers/models.js +32 -2
- package/dist/src/cli/handlers/ralph-launcher.js +76 -4
- package/dist/src/cli/handlers/rules.js +1 -1
- package/dist/src/cli/handlers/script-runner.js +73 -2
- package/dist/src/cli/handlers/serve.js +5 -3
- package/dist/src/cli/handlers/sessions.js +1 -0
- package/dist/src/cli/handlers/setup.js +432 -1
- package/dist/src/cli/handlers/skill-lint.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +118 -8
- package/dist/src/cli/handlers/use.js +335 -107
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +81 -0
- package/dist/src/cli/router.js +16 -1
- package/dist/src/config/aiwg-config.js +42 -0
- package/dist/src/config/cli.js +7 -3
- package/dist/src/config/decision-config.js +14 -1
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/native-sandbox-warnings.js +25 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +482 -0
- package/dist/src/decision/ledger.js +22 -3
- package/dist/src/decision/pricing.js +147 -0
- package/dist/src/decision/qualification/gate-evidence.js +23 -18
- package/dist/src/decision/qualification/load-staged.js +576 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +12 -5
- package/dist/src/decision/review/service.js +6 -4
- package/dist/src/decision/review/validate.js +23 -2
- package/dist/src/decision/savings.js +8 -3
- package/dist/src/discovery/index.js +492 -0
- package/dist/src/discovery/interview.js +238 -0
- package/dist/src/discovery/plan-apply.js +567 -0
- package/dist/src/discovery/plan-store.js +171 -0
- package/dist/src/discovery/plan-types.js +2 -0
- package/dist/src/discovery/plan-validation.js +139 -0
- package/dist/src/discovery/types.js +2 -0
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +49 -9
- package/dist/src/extensions/manifest.js +71 -44
- package/dist/src/extensions/project-local-discovery.js +1 -1
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/extensions/project-quickref.js +18 -17
- package/dist/src/extensions/validation.js +32 -29
- package/dist/src/housekeeper/daemon-config.js +148 -0
- package/dist/src/housekeeper/ipc.js +84 -0
- package/dist/src/housekeeper/lifecycle.js +148 -0
- package/dist/src/housekeeper/platform.js +35 -0
- package/dist/src/housekeeper/status.js +70 -0
- package/dist/src/housekeeper/units.js +114 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/jobs/flow.js +22 -21
- package/dist/src/lint/rule-activation.js +20 -5
- package/dist/src/lint/runner.js +1 -1
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +22 -96
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- package/dist/src/mcp/grok-build-config.mjs +2 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/profiles.js +18 -31
- package/dist/src/mcp/profiles.mjs +18 -32
- package/dist/src/mcp/registry.js +7 -27
- package/dist/src/mcp/registry.mjs +7 -28
- package/dist/src/mcp/server.mjs +34 -1
- package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
- package/dist/src/mcp/tools/command-run.mjs +3 -2
- package/dist/src/mcp/tools/decision.mjs +49 -48
- package/dist/src/mcp/tools/discovery.mjs +46 -21
- package/dist/src/mcp/tools/orchestration.mjs +22 -22
- package/dist/src/mcp/tools/subsystems.mjs +9 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +30 -6
- package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
- package/dist/src/memory/audit.js +719 -0
- package/dist/src/memory/canonical-context.js +392 -13
- package/dist/src/memory/canonical-maintenance.js +95 -0
- package/dist/src/memory/cli-result.js +22 -0
- package/dist/src/memory/cli.js +176 -0
- package/dist/src/memory/context-pack.js +57 -6
- package/dist/src/memory/cost.js +512 -0
- package/dist/src/memory/firewall-classification.d.mts +7 -0
- package/dist/src/memory/firewall-classification.mjs +32 -0
- package/dist/src/memory/index.js +8 -0
- package/dist/src/memory/line-maintenance.js +68 -0
- package/dist/src/memory/maintenance-cli.js +286 -0
- package/dist/src/memory/maintenance-contract.js +136 -0
- package/dist/src/memory/maintenance-index.js +18 -0
- package/dist/src/memory/maintenance-owner.js +2 -0
- package/dist/src/memory/maintenance-record.js +138 -0
- package/dist/src/memory/maintenance.js +254 -0
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/memory/provider-memory-adapter.js +154 -0
- package/dist/src/memory/recommend-cli.js +83 -0
- package/dist/src/memory/recommend.js +269 -0
- package/dist/src/memory/scheduled.js +81 -0
- package/dist/src/memory/taxonomy.js +44 -0
- package/dist/src/mission-protocol/codecs.js +19 -9
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/config-loader.js +11 -1
- package/dist/src/models/model-capabilities.v1.json +74 -50
- package/dist/src/models/provider-policy.js +125 -62
- package/dist/src/models/resolver.js +45 -8
- package/dist/src/models/worker-installation.d.mts +8 -1
- package/dist/src/models/worker-installation.mjs +231 -38
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/plugin/metadata-validator.js +1 -1
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/providers/deploy-version.d.mts +1 -0
- package/dist/src/providers/deploy-version.mjs +17 -0
- package/dist/src/providers/provider-definitions.js +19 -16
- package/dist/src/providers/skill-ownership.d.mts +11 -2
- package/dist/src/providers/skill-ownership.mjs +35 -17
- package/dist/src/rules/activation.d.mts +11 -2
- package/dist/src/rules/activation.mjs +82 -6
- package/dist/src/rules/inventory.js +1 -1
- package/dist/src/rules/rule-frontmatter.schema.json +2 -0
- package/dist/src/schema/zod-validation.d.mts +6 -0
- package/dist/src/schema/zod-validation.mjs +50 -0
- package/dist/src/serve/executor-registry.js +297 -20
- package/dist/src/serve/executor-token.d.mts +1 -0
- package/dist/src/serve/executor-token.mjs +8 -0
- package/dist/src/serve/shared-host-scheduler.js +1102 -31
- package/dist/src/serve/worker-invocation.js +124 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +10 -9
- package/dist/src/sessions/adapters/copilot.js +14 -13
- package/dist/src/sessions/adapters/cursor.js +19 -18
- package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
- package/dist/src/sessions/adapters/factory.js +11 -10
- package/dist/src/sessions/adapters/generic.js +32 -23
- package/dist/src/sessions/adapters/grokbot.js +35 -35
- package/dist/src/sessions/adapters/hermes.js +20 -19
- package/dist/src/sessions/adapters/muse.js +50 -49
- package/dist/src/sessions/adapters/omp.js +5 -4
- package/dist/src/sessions/adapters/openclaw.js +30 -29
- package/dist/src/sessions/adapters/opencode.js +26 -26
- package/dist/src/sessions/adapters/openhuman.js +24 -23
- package/dist/src/sessions/adapters/pi.js +5 -4
- package/dist/src/sessions/adapters/windsurf.js +8 -8
- package/dist/src/sessions/analytics.js +10 -9
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +63 -53
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/workspace-discovery.js +3 -4
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +46 -10
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +110 -22
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/dist/src/writing/exemplar-selection.js +6 -6
- package/dist/src/writing/voice-evaluation.js +24 -23
- package/dist/src/writing/writer-migration.js +14 -13
- package/dist/src/writing/writer-profile.js +19 -18
- package/dist/src/writing/writing-brief.js +18 -17
- package/dist/src/writing/writing-receipt.js +47 -46
- package/package.json +5 -2
- package/schemas/decision/DecisionReview.schema.json +78 -1
- package/schemas/mission-v1.schema.json +3 -0
- package/tools/agents/deploy-agents.mjs +67 -39
- package/tools/agents/providers/antigravity.mjs +6 -1
- package/tools/agents/providers/base.mjs +24 -14
- package/tools/agents/providers/claude.mjs +6 -3
- package/tools/agents/providers/codex.mjs +54 -57
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +13 -4
- package/tools/agents/providers/cursor.mjs +1 -1
- package/tools/agents/providers/deepseek-harness.mjs +1 -1
- package/tools/agents/providers/factory.mjs +7 -1
- package/tools/agents/providers/grok-build.mjs +18 -2
- package/tools/agents/providers/grokbot.mjs +1 -1
- package/tools/agents/providers/hermes.mjs +12 -15
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +31 -5
- package/tools/agents/providers/opencode.mjs +14 -5
- package/tools/agents/providers/openhuman.mjs +1 -1
- package/tools/agents/providers/pi.mjs +1 -1
- package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
- package/tools/agents/providers/rule-delivery.mjs +289 -80
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +13 -3
- package/tools/cli/workspace-status.mjs +5 -0
- package/tools/commands/deploy-prompts-codex.mjs +11 -8
- package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
- package/tools/ralph-external/process-monitor.mjs +49 -8
- package/tools/ralph-external/session-launcher.mjs +405 -35
- package/tools/skills/deploy-skills-codex.mjs +11 -7
- package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Shared installed-worker qualification owner, also used by the Ralph compatibility facade. */
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { constants } from 'node:fs';
|
|
4
|
-
import { open, realpath, lstat, mkdir, rename, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { open, realpath, lstat, mkdir, rename, writeFile, unlink } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
@@ -163,7 +163,7 @@ export async function qualifyWorkerInstallation(instance, options) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
export async function prepareInstalledWorkerInvocation(instance, invocation, qualification) {
|
|
166
|
-
const { prepareWorkerInvocation, WorkerPathScopeError } = await import('../serve/worker-invocation.js');
|
|
166
|
+
const { prepareWorkerInvocation, WorkerPathScopeError, WorkerInvocationCheckError } = await import('../serve/worker-invocation.js');
|
|
167
167
|
const { requiredWorkerControls, WorkerPolicyError, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
|
|
168
168
|
if (!isOperatorQualifiedWorkerAdapter(qualification.adapter)) qualification = { ...qualification, requiredControls: requiredWorkerControls({
|
|
169
169
|
...qualification, effort: invocation.selected.effort.value }) };
|
|
@@ -187,7 +187,7 @@ export async function prepareInstalledWorkerInvocation(instance, invocation, qua
|
|
|
187
187
|
throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker installation qualification changed');
|
|
188
188
|
}
|
|
189
189
|
if (await realpath(result.cwd) !== scope.cwd) {
|
|
190
|
-
throw new
|
|
190
|
+
throw new WorkerInvocationCheckError('OG_CONFIG_REFERENCE', 'qualified_workspace_mismatch');
|
|
191
191
|
}
|
|
192
192
|
for (const key of ['briefPath', 'stdoutPath', 'stderrPath', 'finalPath']) {
|
|
193
193
|
const target = await canonicalFuturePath(result[key]);
|
|
@@ -443,6 +443,19 @@ export function boundedVersionProbe(executable, argv, environment, cwd, timeoutM
|
|
|
443
443
|
|| !Array.isArray(argv) || argv.some(arg => typeof arg !== 'string' || arg.includes('\0'))) {
|
|
444
444
|
throw new Error('Invalid probe bounds');
|
|
445
445
|
}
|
|
446
|
+
// ETXTBSY: the executable still has a writer somewhere, typically a sibling
|
|
447
|
+
// process forked while the file was being written or a CLI mid-update. It
|
|
448
|
+
// clears in milliseconds, so retry the spawn within the same overall bound.
|
|
449
|
+
const deadline = Date.now() + timeoutMs;
|
|
450
|
+
const attempt = delayMs => runBoundedVersionProbe(executable, argv, environment, cwd, timeoutMs, maxBytes, versionPattern)
|
|
451
|
+
.catch(error => {
|
|
452
|
+
if (error?.code !== 'ETXTBSY' || Date.now() + delayMs >= deadline) throw error;
|
|
453
|
+
return new Promise(resolveDelay => setTimeout(resolveDelay, delayMs)).then(() => attempt(Math.min(delayMs * 2, 200)));
|
|
454
|
+
});
|
|
455
|
+
return attempt(10);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function runBoundedVersionProbe(executable, argv, environment, cwd, timeoutMs, maxBytes, versionPattern) {
|
|
446
459
|
return new Promise((resolve, reject) => {
|
|
447
460
|
const child = spawn(executable, argv, { cwd, env: environment, stdio: 'pipe', detached: true, shell: false });
|
|
448
461
|
let output = Buffer.alloc(0);
|
|
@@ -663,7 +676,9 @@ async function loadQualifiedModules({ registry, modules, workerWritableRoots, pl
|
|
|
663
676
|
for (const id of entry.adapterIds) {
|
|
664
677
|
const builtinConflict = canonicalIds.has(id) ? false : providerRegistry.has(id);
|
|
665
678
|
if (ids.has(id) || ((builtinConflict || installedModules.has(inspectedModule.key))
|
|
666
|
-
&& installedModules.get(id) !== inspectedModule.key))
|
|
679
|
+
&& installedModules.get(id) !== inspectedModule.key)) {
|
|
680
|
+
throw Object.assign(new Error(`Duplicate worker adapter ID: ${id}`), { code: 'WORKER_ADAPTER_CONFLICT' });
|
|
681
|
+
}
|
|
667
682
|
ids.add(id);
|
|
668
683
|
}
|
|
669
684
|
inspected.push(inspectedModule);
|
|
@@ -809,6 +824,40 @@ export async function prepareWorkerInstallationStore({ workerWritableRoots, appl
|
|
|
809
824
|
return assertTrustedStore(installationStorePath(), workerWritableRoots, true, apply);
|
|
810
825
|
}
|
|
811
826
|
|
|
827
|
+
// Use the existing delivery ledger's host-selected keyring, never public keys
|
|
828
|
+
// supplied by a registration. PAE separates this authority from effect records.
|
|
829
|
+
const qualificationPayloadType = 'application/vnd.aiwg.worker-installation-qualification+json';
|
|
830
|
+
const requalifyMessage = 'Re-run aiwg doctor --probe-workers --qualify. Existing unsigned approvals require deliberate one-time requalification.';
|
|
831
|
+
const keyRecoveryMessage = 'Worker qualification signing trust is unavailable. Check effects.keyProvider in aiwg.config, unlock or restore the existing effect-ledger key, and run aiwg effect keys init if its keyring is not initialized; then re-run aiwg doctor --probe-workers --qualify.';
|
|
832
|
+
|
|
833
|
+
async function qualificationTrust(cwd, scope) {
|
|
834
|
+
const { effectSession, openLedger } = await import('../effects/host.js');
|
|
835
|
+
const { openEffectLedger, readLedgerKeyring } = await import('../effects/ledger.js');
|
|
836
|
+
const ledger = scope ? openEffectLedger({ projectDir: cwd, scope, writer: 'cli' }) : openLedger(await effectSession(cwd));
|
|
837
|
+
const keyring = await readLedgerKeyring(ledger);
|
|
838
|
+
if (!keyring) throw new Error(keyRecoveryMessage);
|
|
839
|
+
return { ledger, keyring };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function qualificationPayload(record, signedAt, projectDir, scope) {
|
|
843
|
+
const { canonicalJson } = await import('../security/artifact-trust.js');
|
|
844
|
+
const { signature: _signature, ...body } = record;
|
|
845
|
+
return Buffer.from(canonicalJson({ record: JSON.parse(JSON.stringify(body)), signedAt, projectDir, scope }), 'utf8');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function qualificationSignatureFailure(record, trust) {
|
|
849
|
+
if (!record.signature) return 'signature-missing';
|
|
850
|
+
const { keyid, sig, signedAt, projectDir, scope } = record.signature;
|
|
851
|
+
if (typeof keyid !== 'string' || typeof sig !== 'string' || typeof signedAt !== 'string'
|
|
852
|
+
|| typeof projectDir !== 'string' || !path.isAbsolute(projectDir) || !scope) return 'signature-invalid';
|
|
853
|
+
let keyring;
|
|
854
|
+
try { ({ keyring } = await trust(projectDir, scope)); } catch { return 'signature-trust-unavailable'; }
|
|
855
|
+
const { checkSignature } = await import('../effects/keyring.js');
|
|
856
|
+
const result = checkSignature(keyring, keyid, sig, qualificationPayloadType,
|
|
857
|
+
await qualificationPayload(record, signedAt, projectDir, scope), signedAt);
|
|
858
|
+
return result === 'ok' ? null : result === 'signature-invalid' ? result : `signature-${result}`;
|
|
859
|
+
}
|
|
860
|
+
|
|
812
861
|
async function readInstallationStore(filename, expectedDigest) {
|
|
813
862
|
try {
|
|
814
863
|
const bytes = await readInstalledFile(filename);
|
|
@@ -825,6 +874,38 @@ async function readInstallationStore(filename, expectedDigest) {
|
|
|
825
874
|
}
|
|
826
875
|
}
|
|
827
876
|
|
|
877
|
+
/** Offline projection of recorded approvals only. Never imports drivers, reads auth, or probes CLIs.
|
|
878
|
+
* This is historical evidence, not current launch authority; loadRecordedWorkerInstallations
|
|
879
|
+
* performs binary/version/scope checks when dispatch actually requests a binding.
|
|
880
|
+
*/
|
|
881
|
+
export async function inspectRecordedWorkerQualifications({ cwd, policy, parallelism }) {
|
|
882
|
+
const filename = installationStorePath();
|
|
883
|
+
await assertTrustedStore(filename, [cwd]);
|
|
884
|
+
const { stored } = await readInstallationStore(filename);
|
|
885
|
+
const workspace = await realpath(cwd);
|
|
886
|
+
const results = [];
|
|
887
|
+
const { qualificationDigest } = await import('./qualification-digest.mjs');
|
|
888
|
+
for (const record of stored.registrations) {
|
|
889
|
+
try {
|
|
890
|
+
const { cli, installation, adapter, binding } = record;
|
|
891
|
+
const configured = parallelism?.clis?.[cli];
|
|
892
|
+
if (!configured || configured.enabled === false || !/^[a-z][a-z0-9-]{0,62}$/.test(cli)
|
|
893
|
+
|| await realpath(installation.cwd) !== workspace
|
|
894
|
+
|| qualificationDigest(policy) !== installation.effectiveConfigDigest
|
|
895
|
+
|| configured.adapter !== `providers:${adapter.provider}` && configured.adapter !== adapter.id
|
|
896
|
+
|| configured.accountScope !== binding.accountScope
|
|
897
|
+
|| binding.profile !== installation.profile || binding.cliVersion !== installation.cliVersion
|
|
898
|
+
|| (configured.executable && await realpath(configured.executable) !== await realpath(installation.executable))) continue;
|
|
899
|
+
const proof = adapter.invocationProfiles?.[binding.profile]?.installationQualification;
|
|
900
|
+
if (!proof || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => proof[key] !== installation.evidence?.[key])) continue;
|
|
901
|
+
if (await qualificationSignatureFailure(record, () => qualificationTrust(cwd))) continue;
|
|
902
|
+
await assertWorkerInstallationEvidence({ ...installation, adapter, now: Date.now(), policy }, proof.scopeDigest);
|
|
903
|
+
results.push({ cli, provider: adapter.provider });
|
|
904
|
+
} catch { /* Malformed, expired or unbound evidence is not qualification. */ }
|
|
905
|
+
}
|
|
906
|
+
return results.filter(row => results.filter(other => row.cli === other.cli).length === 1);
|
|
907
|
+
}
|
|
908
|
+
|
|
828
909
|
/** Explicit host operation: successful bounded qualification is required before
|
|
829
910
|
* recording; credentials are reduced to names and are never written to disk. */
|
|
830
911
|
export async function recordWorkerInstallation(instance, options) {
|
|
@@ -833,6 +914,15 @@ export async function recordWorkerInstallation(instance, options) {
|
|
|
833
914
|
const installation = structuredClone(options.installation);
|
|
834
915
|
const result = await qualifyWorkerInstallation(instance, { ...installation, now: Date.now() });
|
|
835
916
|
if (result.status !== 'ENABLED') return result;
|
|
917
|
+
let signingKey, keyring, signingProjectDir;
|
|
918
|
+
try {
|
|
919
|
+
signingProjectDir = await realpath(options.projectDir ?? installation.cwd);
|
|
920
|
+
const trust = await qualificationTrust(signingProjectDir);
|
|
921
|
+
keyring = trust.keyring;
|
|
922
|
+
signingKey = await trust.ledger.signingKey();
|
|
923
|
+
const { activeKey } = await import('../effects/keyring.js');
|
|
924
|
+
if (activeKey(keyring).keyid !== signingKey.keyid) throw new Error('inactive key');
|
|
925
|
+
} catch { return { status: 'DISABLED', reason: keyRecoveryMessage }; }
|
|
836
926
|
const adapter = structuredClone(installation.adapter);
|
|
837
927
|
adapter.invocationProfiles[installation.profile].installationQualification = {
|
|
838
928
|
scopeDigest: result.scopeDigest, fixtureDigest: result.fixtureDigest,
|
|
@@ -842,79 +932,169 @@ export async function recordWorkerInstallation(instance, options) {
|
|
|
842
932
|
|| binding.accountScope !== installation.accountScope) throw new Error('Installation binding differs from qualification');
|
|
843
933
|
const filename = installationStorePath();
|
|
844
934
|
await assertTrustedStore(filename, installation.workerWritableRoots);
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
delete installation.environment;
|
|
848
|
-
delete installation.adapter;
|
|
849
|
-
delete installation.now;
|
|
850
|
-
const record = { cli, ...(options.modelObservations ? { modelObservations: structuredClone(options.modelObservations) } : {}), binding: { ...structuredClone(binding), boundary: installation.boundary }, adapter,
|
|
851
|
-
installation: { ...installation, environmentNames, versionProbe: { rawBanner: result.rawVersionBanner, version: result.cliVersion } },
|
|
852
|
-
...(options.mcpSchemas ? { mcpSchemas: structuredClone(options.mcpSchemas) } : {}),
|
|
853
|
-
...(options.mcpProfile ? { mcpProfile: structuredClone(options.mcpProfile) } : {}) };
|
|
854
|
-
stored.registrations = stored.registrations.filter(entry => entry.cli !== cli);
|
|
855
|
-
stored.registrations.push(record);
|
|
935
|
+
// Canonical workspace identity is an exact approval key, never a prefix.
|
|
936
|
+
installation.cwd = await realpath(installation.cwd);
|
|
856
937
|
await mkdir(path.dirname(filename), { recursive: true, mode: 0o700 });
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
938
|
+
// Serialize read/modify/rename across qualifying CLI processes. A stale lock
|
|
939
|
+
// fails closed; qualification never steals another process's lock.
|
|
940
|
+
const lockPath = `${filename}.lock`;
|
|
941
|
+
let lock;
|
|
942
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
943
|
+
try { lock = await open(lockPath, 'wx', 0o600); break; }
|
|
944
|
+
catch (error) {
|
|
945
|
+
if (error.code !== 'EEXIST' || attempt === 99) throw error;
|
|
946
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
await assertTrustedStore(filename, installation.workerWritableRoots);
|
|
951
|
+
const { stored } = await readInstallationStore(filename);
|
|
952
|
+
const environmentNames = Object.keys(installation.environment ?? {});
|
|
953
|
+
delete installation.environment;
|
|
954
|
+
delete installation.adapter;
|
|
955
|
+
delete installation.now;
|
|
956
|
+
const record = { cli, ...(options.modelObservations ? { modelObservations: structuredClone(options.modelObservations) } : {}), binding: { ...structuredClone(binding), boundary: installation.boundary }, adapter,
|
|
957
|
+
installation: { ...installation, environmentNames, versionProbe: { rawBanner: result.rawVersionBanner, version: result.cliVersion } },
|
|
958
|
+
...(options.mcpSchemas ? { mcpSchemas: structuredClone(options.mcpSchemas) } : {}),
|
|
959
|
+
...(options.mcpProfile ? { mcpProfile: structuredClone(options.mcpProfile) } : {}) };
|
|
960
|
+
const signedAt = new Date().toISOString();
|
|
961
|
+
try {
|
|
962
|
+
const payload = await qualificationPayload(record, signedAt, signingProjectDir, keyring.scope);
|
|
963
|
+
const sig = signingKey.signPae(qualificationPayloadType, payload);
|
|
964
|
+
const { checkSignature } = await import('../effects/keyring.js');
|
|
965
|
+
if (checkSignature(keyring, signingKey.keyid, sig, qualificationPayloadType, payload, signedAt) !== 'ok') throw new Error('invalid signature');
|
|
966
|
+
record.signature = { keyid: signingKey.keyid, sig, signedAt, projectDir: signingProjectDir, scope: keyring.scope };
|
|
967
|
+
} catch { return { status: 'DISABLED', reason: keyRecoveryMessage }; }
|
|
968
|
+
const retained = [];
|
|
969
|
+
for (const entry of stored.registrations) {
|
|
970
|
+
const workspace = typeof entry.installation?.cwd === 'string'
|
|
971
|
+
? await realpath(entry.installation.cwd).catch(() => entry.installation.cwd) : null;
|
|
972
|
+
if (entry.cli !== cli || workspace !== installation.cwd) retained.push(entry);
|
|
973
|
+
}
|
|
974
|
+
// Bound host authority without silently evicting another workspace approval.
|
|
975
|
+
if (retained.length >= 128) throw new Error('Worker installation store is full (128 workspace approvals); remove an obsolete approval before qualifying another workspace');
|
|
976
|
+
stored.registrations = retained;
|
|
977
|
+
stored.registrations.push(record);
|
|
978
|
+
await mkdir(path.dirname(filename), { recursive: true, mode: 0o700 });
|
|
979
|
+
await assertTrustedStore(filename, installation.workerWritableRoots);
|
|
980
|
+
const temporary = `${filename}.${process.pid}.${createHash('sha256').update(String(Math.random())).digest('hex')}.tmp`;
|
|
981
|
+
await writeFile(temporary, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
982
|
+
await rename(temporary, filename);
|
|
983
|
+
return result;
|
|
984
|
+
} finally {
|
|
985
|
+
await lock.close();
|
|
986
|
+
await unlink(lockPath);
|
|
987
|
+
}
|
|
862
988
|
}
|
|
863
989
|
|
|
864
990
|
/** Read recorded host authority, validate installation bytes and the current CLI
|
|
865
991
|
* version, then load the same approved factory used by Ralph. */
|
|
866
|
-
export async function loadRecordedWorkerInstallations({ registry, cwd, policy, parallelism, storePath, storeDigest }) {
|
|
992
|
+
export async function loadRecordedWorkerInstallations({ registry, cwd, policy, parallelism, storePath, storeDigest, precheckPolicy }) {
|
|
867
993
|
const { WorkerPolicyError, parseProviderModelCapabilities } = await import('./provider-policy.js');
|
|
868
994
|
const { qualificationDigest } = await import('./qualification-digest.mjs');
|
|
869
|
-
|
|
870
|
-
|
|
995
|
+
// Only fixed stage codes and bounded record indexes may leave this boundary.
|
|
996
|
+
// Underlying errors can contain probe output, environment values or paths.
|
|
997
|
+
const rejections = [];
|
|
998
|
+
const fail = () => new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED',
|
|
999
|
+
`Trusted installation qualification is required (${[...new Set(rejections.map(row => row.reason))].join(', ')})${rejections.some(row => row.reason.startsWith('signature-'))
|
|
1000
|
+
? `. ${rejections.some(row => row.reason === 'signature-trust-unavailable') ? keyRecoveryMessage : requalifyMessage}` : ''}`,
|
|
1001
|
+
{ rejections });
|
|
1002
|
+
let reason = 'store-binding';
|
|
1003
|
+
let filename;
|
|
871
1004
|
let stored, digest;
|
|
872
1005
|
try {
|
|
1006
|
+
filename = storePath ?? installationStorePath();
|
|
873
1007
|
if (!path.isAbsolute(filename) || (storePath !== undefined && !/^sha256:[a-f0-9]{64}$/.test(storeDigest ?? ''))) throw fail();
|
|
1008
|
+
reason = 'store-untrusted';
|
|
874
1009
|
await assertTrustedStore(filename, [cwd]);
|
|
1010
|
+
reason = 'store-record';
|
|
875
1011
|
({ stored, digest } = await readInstallationStore(filename, storeDigest));
|
|
876
1012
|
}
|
|
877
|
-
catch { throw fail(); }
|
|
1013
|
+
catch { rejections.push({ registration: null, reason }); throw fail(); }
|
|
1014
|
+
if (!stored.registrations.length) {
|
|
1015
|
+
rejections.push({ registration: null, reason: 'no-records' });
|
|
1016
|
+
throw fail();
|
|
1017
|
+
}
|
|
1018
|
+
// Request policy errors remain actionable before digest/adapter checks,
|
|
1019
|
+
// while an absent approval retains the qualification contract (#3070).
|
|
1020
|
+
precheckPolicy?.();
|
|
878
1021
|
const installations = {};
|
|
879
1022
|
const drivers = {};
|
|
880
1023
|
const observations = {};
|
|
1024
|
+
const qualificationFailures = Object.create(null);
|
|
881
1025
|
const resolvedRegistry = structuredClone(registry);
|
|
1026
|
+
const requestedWorkspace = await realpath(cwd);
|
|
1027
|
+
const workspaces = await Promise.all(stored.registrations.map(async record => {
|
|
1028
|
+
try { return await realpath(record?.installation?.cwd); } catch { return null; }
|
|
1029
|
+
}));
|
|
882
1030
|
const counts = new Map();
|
|
883
|
-
|
|
884
|
-
for (const record of stored.registrations) {
|
|
1031
|
+
const selected = new Map();
|
|
1032
|
+
for (const [index, record] of stored.registrations.entries()) {
|
|
1033
|
+
const key = JSON.stringify([record?.cli, workspaces[index]]);
|
|
1034
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1035
|
+
// Prefer an exact approval. Retain the latest other-workspace record only
|
|
1036
|
+
// to preserve the typed workspace-mismatch diagnostic for unapproved dirs.
|
|
1037
|
+
const prior = selected.get(record?.cli);
|
|
1038
|
+
if (prior === undefined || workspaces[prior] !== requestedWorkspace) selected.set(record?.cli, index);
|
|
1039
|
+
}
|
|
1040
|
+
for (const [registration, record] of stored.registrations.entries()) {
|
|
1041
|
+
reason = 'record-identity';
|
|
885
1042
|
try {
|
|
886
1043
|
const { cli, binding, adapter } = record;
|
|
887
|
-
if (!/^[a-z][a-z0-9-]{0,62}$/.test(cli)
|
|
1044
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(cli)) throw fail();
|
|
1045
|
+
if (counts.get(JSON.stringify([cli, workspaces[registration]])) !== 1) throw fail();
|
|
1046
|
+
if (selected.get(cli) !== registration) continue;
|
|
1047
|
+
// A conductor-pinned store digest also binds the original config source.
|
|
1048
|
+
// Framing from a worker cwd must never read that cwd's configuration.
|
|
1049
|
+
reason = await qualificationSignatureFailure(record, (projectDir, scope) => storePath === undefined
|
|
1050
|
+
? qualificationTrust(cwd) : qualificationTrust(projectDir, scope));
|
|
1051
|
+
if (reason) throw fail();
|
|
888
1052
|
const installation = { ...record.installation, adapter, now: Date.now(), environment: {} };
|
|
1053
|
+
reason = 'environment-name';
|
|
889
1054
|
if (record.installation.environment && Object.keys(record.installation.environment).length) throw fail();
|
|
890
1055
|
for (const name of installation.environmentNames ?? []) {
|
|
891
1056
|
if (typeof name !== 'string' || process.env[name] === undefined) throw fail();
|
|
892
1057
|
installation.environment[name] = process.env[name];
|
|
893
1058
|
}
|
|
1059
|
+
reason = 'binding-mismatch';
|
|
894
1060
|
if (binding.profile !== installation.profile || binding.cliVersion !== installation.cliVersion
|
|
895
1061
|
|| binding.accountScope !== installation.accountScope
|
|
896
|
-
|| qualificationDigest(binding.boundary) !== qualificationDigest(installation.boundary)
|
|
897
|
-
|
|
1062
|
+
|| qualificationDigest(binding.boundary) !== qualificationDigest(installation.boundary)) throw fail();
|
|
1063
|
+
reason = 'effective-config-digest';
|
|
1064
|
+
if (qualificationDigest(policy) !== installation.effectiveConfigDigest) throw fail();
|
|
1065
|
+
reason = 'evidence';
|
|
898
1066
|
const proof = adapter.invocationProfiles?.[binding.profile]?.installationQualification;
|
|
899
1067
|
if (!proof || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => proof[key] !== installation.evidence?.[key])) throw fail();
|
|
900
1068
|
// Reject expired/unbound negative observations before any factory import.
|
|
901
1069
|
const { requiredWorkerControls, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
|
|
902
1070
|
if (!isOperatorQualifiedWorkerAdapter(adapter)) installation.requiredControls = requiredWorkerControls(installation);
|
|
903
1071
|
await assertWorkerInstallationEvidence(installation, proof.scopeDigest);
|
|
1072
|
+
reason = 'store-untrusted';
|
|
904
1073
|
await assertTrustedStore(filename, installation.workerWritableRoots);
|
|
1074
|
+
reason = 'adapter-metadata';
|
|
905
1075
|
const candidateRegistry = structuredClone(resolvedRegistry);
|
|
906
1076
|
if (candidateRegistry.providers[adapter.id]) candidateRegistry.providers[adapter.id].workerAdapter = adapter;
|
|
907
1077
|
else { candidateRegistry.workerAdapters ??= {}; candidateRegistry.workerAdapters[adapter.id] = adapter; }
|
|
908
1078
|
parseProviderModelCapabilities(candidateRegistry);
|
|
909
1079
|
// The loader validates package identity, bytes and static exports BEFORE import.
|
|
910
1080
|
// Only this installation's metadata is needed; unrelated absent modules do not block it.
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1081
|
+
// Preserve the canonical provider slot used to distinguish upgrades from ID conflicts.
|
|
1082
|
+
const loadingRegistry = structuredClone(candidateRegistry);
|
|
1083
|
+
const canonical = loadingRegistry.providers[adapter.id]?.workerAdapter?.id === adapter.id;
|
|
1084
|
+
for (const [name, provider] of Object.entries(loadingRegistry.providers)) {
|
|
1085
|
+
if (canonical && name === adapter.id) provider.workerAdapter = adapter;
|
|
1086
|
+
else delete provider.workerAdapter;
|
|
1087
|
+
}
|
|
1088
|
+
loadingRegistry.workerAdapters = canonical ? {} : { [adapter.id]: adapter };
|
|
1089
|
+
reason = 'module-registration';
|
|
914
1090
|
await loadInstalledWorkerAdapters({ registry: loadingRegistry, modules: installation.modules,
|
|
915
1091
|
workerWritableRoots: installation.workerWritableRoots, platform: process.platform });
|
|
1092
|
+
reason = 'binary-identity';
|
|
916
1093
|
const instance = createProvider(adapter.id);
|
|
917
|
-
const
|
|
1094
|
+
const scope = await inspectWorkerInstallation(instance, installation);
|
|
1095
|
+
reason = 'scope-digest';
|
|
1096
|
+
await assertWorkerInstallationEvidence(installation, scope.scopeDigest);
|
|
1097
|
+
reason = 'binary-identity';
|
|
918
1098
|
if (parallelism && isOperatorQualifiedWorkerAdapter(adapter)) {
|
|
919
1099
|
const { resolveWorkerGroupInstallation } = await import('../config/aiwg-config.js');
|
|
920
1100
|
const configured = resolveWorkerGroupInstallation(parallelism, cli, candidateRegistry);
|
|
@@ -931,15 +1111,19 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
|
|
|
931
1111
|
}
|
|
932
1112
|
if (!executable || await realpath(executable) !== scope.binary.realpath) throw fail();
|
|
933
1113
|
}
|
|
1114
|
+
reason = 'scope-digest';
|
|
934
1115
|
if (scope.scopeDigest !== proof.scopeDigest) throw fail();
|
|
935
1116
|
// A launcher can keep the same bytes while its CLI payload is updated.
|
|
936
1117
|
// Reuse the bounded, credential-free doctor probe before granting authority.
|
|
1118
|
+
reason = 'version';
|
|
937
1119
|
const { version } = await boundedVersionProbe(scope.binary.realpath,
|
|
938
1120
|
adapter.invocation.versionArgv, workerVersionProbeEnvironment(), cwd,
|
|
939
1121
|
workerVersionProbeDefaults.timeoutMs, workerVersionProbeDefaults.maxBytes,
|
|
940
1122
|
adapter.invocation.versionPattern);
|
|
941
|
-
if (version !== binding.cliVersion
|
|
942
|
-
|
|
1123
|
+
if (version !== binding.cliVersion) throw fail();
|
|
1124
|
+
reason = 'scope-digest';
|
|
1125
|
+
if ((await inspectWorkerInstallation(instance, installation)).scopeDigest !== scope.scopeDigest) throw fail();
|
|
1126
|
+
reason = 'driver';
|
|
943
1127
|
const profile = adapter.invocationProfiles[binding.profile];
|
|
944
1128
|
let mcpRegistry;
|
|
945
1129
|
if (profile.transport === 'mcp') {
|
|
@@ -979,8 +1163,17 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
|
|
|
979
1163
|
installations[cli] = structuredClone(binding);
|
|
980
1164
|
if (record.modelObservations) observations[cli] = structuredClone(record.modelObservations);
|
|
981
1165
|
Object.assign(resolvedRegistry, candidateRegistry);
|
|
982
|
-
} catch {
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
// One rejected installation cannot grant authority or hide qualified peers.
|
|
1168
|
+
const failureReason = reason === 'module-registration' && error?.code === 'WORKER_ADAPTER_CONFLICT'
|
|
1169
|
+
? 'adapter-conflict' : reason;
|
|
1170
|
+
rejections.push({ registration, reason: failureReason });
|
|
1171
|
+
if (typeof record?.cli === 'string' && /^[a-z][a-z0-9-]{0,62}$/.test(record.cli)) qualificationFailures[record.cli] = failureReason;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (!Object.keys(installations).length) {
|
|
1175
|
+
if (!rejections.length) rejections.push({ registration: null, reason: 'no-records' });
|
|
1176
|
+
throw fail();
|
|
983
1177
|
}
|
|
984
|
-
|
|
985
|
-
return { registry: resolvedRegistry, installations, drivers, observations };
|
|
1178
|
+
return { registry: resolvedRegistry, installations, drivers, observations, qualificationFailures };
|
|
986
1179
|
}
|
|
@@ -47,7 +47,7 @@ export function buildWrapperRouteEnvelope(request) {
|
|
|
47
47
|
? [
|
|
48
48
|
`Run this assignment through ${wrapper}.`,
|
|
49
49
|
`Selected capability: ${request.capability.type} ${request.capability.name} (${request.capability.id}).`,
|
|
50
|
-
`Load it with aiwg show ${request.capability.type === 'workflow' ? 'metadata' : request.capability.type} ${request.capability.id} after discover-first validation.`,
|
|
50
|
+
`Load it with aiwg show ${request.capability.type === 'workflow' ? 'metadata' : request.capability.type} ${request.capability.id}${request.capability.type === 'workflow' ? '' : ' --body'} after discover-first validation.`,
|
|
51
51
|
`Assignment: ${request.assignment.trim()}`,
|
|
52
52
|
'Return capability evidence, changed artifacts, validation results, and remaining risks.',
|
|
53
53
|
].join('\n')
|
|
@@ -5,6 +5,7 @@ import { createHash } from 'node:crypto';
|
|
|
5
5
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
6
6
|
import { parse, stringify } from 'yaml';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
+
import { integer } from '../schema/zod-validation.mjs';
|
|
8
9
|
import { getWritingChannelPack } from '../writing/channel-packs.js';
|
|
9
10
|
import { WriterProfileStore } from '../writing/writer-profile-store.js';
|
|
10
11
|
import { compileWriterProfile } from '../writing/writer-profile.js';
|
|
@@ -14,28 +15,28 @@ const PROTECTED = ['code', 'commands', 'citations', 'quoted-text', 'identifiers'
|
|
|
14
15
|
const STAGE_ORDER = ['semantic', 'voice', 'controlled-language', 'structure', 'presentation'];
|
|
15
16
|
const modeIdSchema = z.string().regex(MODE_ID_PATTERN, 'must start with a lowercase letter or number and contain only lowercase letters, numbers, dots, or hyphens');
|
|
16
17
|
const uniqueModeIds = z.array(modeIdSchema).refine(values => new Set(values).size === values.length, 'must not contain duplicates');
|
|
17
|
-
const outputModeProfileSchema = z.
|
|
18
|
+
const outputModeProfileSchema = z.strictObject({
|
|
18
19
|
id: modeIdSchema,
|
|
19
20
|
version: z.string().min(1),
|
|
20
21
|
description: z.string().min(1),
|
|
21
22
|
kind: z.enum(['voice', 'controlled-language', 'structure', 'presentation']),
|
|
22
23
|
stage: z.enum(STAGE_ORDER),
|
|
23
|
-
order:
|
|
24
|
+
order: integer().optional(),
|
|
24
25
|
instructions: z.string(),
|
|
25
|
-
provenance: z.
|
|
26
|
-
validation: z.
|
|
26
|
+
provenance: z.strictObject({ source: z.string().min(1), license: z.string().min(1) }),
|
|
27
|
+
validation: z.strictObject({
|
|
27
28
|
level: z.enum(['advisory', 'validated', 'conformance']),
|
|
28
29
|
hook: z.string().min(1).optional(),
|
|
29
30
|
standardVersion: z.string().min(1).optional(),
|
|
30
|
-
})
|
|
31
|
+
}),
|
|
31
32
|
compatible: uniqueModeIds.optional(),
|
|
32
33
|
conflicts: uniqueModeIds.optional(),
|
|
33
34
|
requires: uniqueModeIds.optional(),
|
|
34
35
|
supersedes: uniqueModeIds.optional(),
|
|
35
36
|
protectedContent: z.array(z.enum(PROTECTED)).refine(values => new Set(values).size === values.length, 'must not contain duplicates').optional(),
|
|
36
|
-
contextCost:
|
|
37
|
+
contextCost: integer().nonnegative().optional(),
|
|
37
38
|
mergeStrategy: z.literal('weighted-voice').optional(),
|
|
38
|
-
}).
|
|
39
|
+
}).superRefine((profile, ctx) => {
|
|
39
40
|
if (profile.validation.level !== 'advisory' && !profile.validation.hook) {
|
|
40
41
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['validation', 'hook'], message: `${profile.validation.level} modes require a validator hook` });
|
|
41
42
|
}
|