diff --git a/packages/tasks-capability/README.md b/packages/tasks-capability/README.md index f378496..b05ade7 100644 --- a/packages/tasks-capability/README.md +++ b/packages/tasks-capability/README.md @@ -1,15 +1,19 @@ # Affected Verification capability for Opsle Tasks -`@opsle/affected-verification-tasks-capability` **0.1.0** is an independently +`@opsle/affected-verification-tasks-capability` **0.2.0** is an independently versioned, dependency-free capability owned and released by the public Affected Verification repository. AV remains the verification planning authority. Tasks owns operator grants, command execution, observed results, repair, and release. -The package neither runs catalog commands nor authorizes deployment. +The package neither runs catalog commands nor authorizes deployment. AV remains +`OBSERVE_SHADOW`: its proposal is not execution authority, and full verification +must remain authoritative. The executable ESM entry point implements the generic `opsle.capability-manifest.v1` / `opsle.capability-result.v1` contract. Both -`verification.plan` and `verification.capture` are **required deterministic -authorities**. `default_enabled` is **false**. No AV identity branches, new central +`verification.plan`, `verification.shadow`, and `verification.capture` are +**required deterministic authorities**. The shadow hook validates the complete +authoritative result, uses AV's native classifier, and returns a bound receipt. +`default_enabled` is **false**. No AV identity branches, new central Tasks dependencies, private Tasks imports, sibling checkouts, Graphify dependency, or structural-evidence contract are needed. @@ -19,7 +23,7 @@ Build from an AV checkout with Node 20+ and npm: ```sh npm run pack:tasks-capability -sha256sum opsle-affected-verification-tasks-capability-0.1.0.tgz +sha256sum opsle-affected-verification-tasks-capability-0.2.0.tgz ``` `npm pack` builds the adapter, includes the current AV core, runtime helpers, @@ -29,13 +33,13 @@ network dependencies are required. Review the artifact hash and provenance before an operator installs it outside repositories and agent-writable paths: ```sh -npm install --prefix /srv/opsle-capabilities/av-0.1.0 \ +npm install --prefix /srv/opsle-capabilities/av-0.2.0 \ --ignore-scripts --no-audit --no-fund \ - ./opsle-affected-verification-tasks-capability-0.1.0.tgz + ./opsle-affected-verification-tasks-capability-0.2.0.tgz ``` Set the operator-owned `OPSLE_CAPABILITY_PATH` to the installed directory: -`/srv/opsle-capabilities/av-0.1.0/node_modules/@opsle/affected-verification-tasks-capability`. +`/srv/opsle-capabilities/av-0.2.0/node_modules/@opsle/affected-verification-tasks-capability`. Tasks also accepts the generic `capabilityRoots` configuration. Multiple roots use the platform path delimiter. Include the individually installed roots for other required authorities and observers. **Replace** the bundled AV discovery @@ -58,9 +62,9 @@ An absent required authority blocks verification, including after a revoke. ## Compatibility and schemas The real generic runtime compatibility target is Opsle Tasks revision -`e1207c5264c59e14efe9838bba3a33ba504665d2` (Node 24+ for its complete regression +`b76d6253b405469b79d30b260f7ad09827052a4a` (Node 24+ for its complete regression suite). Its `src/capabilities.js` SHA-256 is -`d8568872a1d76e5ac78e134154683b43c7ed46c2b583eaef0e9aac3569a09f30`. +`62dca002d729c82ca00a66fdb6edcbac692eca770ca6f771dea0c7e68ffd3408`. Tests import that unmodified runtime only as a compatibility test dependency; the installed capability never imports Tasks source. Execution metadata comes from generic `services.executionConfig`; project/task/attempt/execution bindings @@ -70,6 +74,7 @@ operator-owned. | Hook | Request | Response | | --- | --- | --- | | `verification.plan` | `opsle.execution.verification-request.v1` | `opsle.execution.verification-analysis.v1` | +| `verification.shadow` | `opsle.execution.verification-shadow-request.v1` | `opsle.execution.verification-shadow-result.v1` | | `verification.capture` | `opsle.execution.change-capture-request.v1` | `opsle.execution.change-set.v1` | All JSON Schemas are in `schemas/`. They include the immutable task manifest, @@ -82,7 +87,10 @@ Structural validation supplements AV's native cross-field semantic validation; it cannot establish completeness or provenance by itself. The compatibility analysis envelope retains `change`, `decision`, `error`, -`evidencePath`, `inputPath`, `receiptPath`, and `record`. An `ok` capability envelope +`evidencePath`, `inputPath`, `receiptPath`, and `record`. Planning does not create +a value receipt: one exists only after exact full-catalog results are validated +and shadow-classified. The shadow response returns it through the ordinary +`receipts` array and retains the matching private sidecar. An `ok` capability envelope means analysis completed, **not that verification passed**. A failed analysis has `decision: null`, an explicit error, and `ANALYSIS_FAILED` evidence. The Tasks consumer must use full configured verification or stop. Missing manifests use @@ -97,9 +105,12 @@ and staged Git tree identities. The manifest is read from the immutable base, not agent-modified content. Every action must match the exact immutable catalog partition and command. The adapter validates canonical decision identity and recomputes the AV decision to check provenance/completeness. Unknown, incomplete, -or opaque evidence cannot justify an unexplained skip. Tasks must compare the -post-verification capture tree with the planned tree before accepting changes; -the package never decides that command execution passed. +or opaque evidence cannot justify an unexplained skip. Tasks must execute the +full catalog, pass its exact results to the shadow hook, and compare the +post-verification capture tree with the planned tree before accepting changes. +Unknown trust state, stale execution identity, source drift, incomplete results, +and invalid receipts fail closed; the package never decides that command +execution passed. SSH target validation, argv quoting, strict host-key checking, connection and remote process deadlines, bounded Git output, and private evidence are retained @@ -111,7 +122,8 @@ all packaged files that affect planning. Operator-protected installation roots remain the trust boundary; hashes do not make writable installations trustworthy. OBSERVE/SHADOW observations and historical benchmarks retain their existing -limits. No observation is promoted to execution authority or production trust. +limits. Proposed skips are proposals, not savings or avoided executions. No +observation is promoted to execution authority or production trust. External structural evidence is not accepted by this interface and cannot narrow verification; only AV's own provenance and completeness decision can justify selection. The core remains separately usable without Tasks. @@ -151,7 +163,7 @@ recorded revision, then run: npm run verify OPSLE_TASKS_RUNTIME_ROOT=/path/to/pinned/opsle-tasks npm run verify:tasks-capability npm run pack:tasks-capability -sha256sum opsle-affected-verification-tasks-capability-0.1.0.tgz +sha256sum opsle-affected-verification-tasks-capability-0.2.0.tgz ``` The second command is mandatory for release: it refuses to skip if Tasks is diff --git a/packages/tasks-capability/opsle-capability.json b/packages/tasks-capability/opsle-capability.json index 3330d72..515c6cf 100644 --- a/packages/tasks-capability/opsle-capability.json +++ b/packages/tasks-capability/opsle-capability.json @@ -2,7 +2,7 @@ "schema": "opsle.capability-manifest.v1", "id": "opsle.affected-verification", "name": "Affected Verification", - "version": "0.1.0", + "version": "0.2.0", "adapter": "adapter.js", "default_enabled": false, "configuration_schema": "opsle.affected-verification.tasks-config.v2", @@ -23,6 +23,14 @@ "role": "authority", "execution": "deterministic", "failure": "required" + }, + { + "name": "verification.shadow", + "input_schema": "opsle.execution.verification-shadow-request.v1", + "output_schema": "opsle.execution.verification-shadow-result.v1", + "role": "authority", + "execution": "deterministic", + "failure": "required" } ] } diff --git a/packages/tasks-capability/package.json b/packages/tasks-capability/package.json index 5b8a85f..cdde4de 100644 --- a/packages/tasks-capability/package.json +++ b/packages/tasks-capability/package.json @@ -1,6 +1,6 @@ { "name": "@opsle/affected-verification-tasks-capability", - "version": "0.1.0", + "version": "0.2.0", "description": "Independent Affected Verification authority for the generic Opsle Tasks capability contract", "type": "module", "license": "Apache-2.0", diff --git a/packages/tasks-capability/schemas/evidence-v1.schema.json b/packages/tasks-capability/schemas/evidence-v1.schema.json index a7747f7..9c8b38d 100644 --- a/packages/tasks-capability/schemas/evidence-v1.schema.json +++ b/packages/tasks-capability/schemas/evidence-v1.schema.json @@ -21,6 +21,8 @@ "analysis_error", "verification_results", "fallback", + "trust_stage", + "shadow", "limitations" ], "properties": { @@ -71,7 +73,7 @@ } } }, - "status": { + "status": { "enum": [ "ANALYSIS_FAILED", "ANALYZED", @@ -81,7 +83,13 @@ "VERIFICATION_RUNNING", "VERIFICATION_FAILED", "NO_AUTOMATED_VERIFICATION", - "VERIFICATION_PENDING" + "VERIFICATION_PENDING", + "SHADOW_HEALTHY", + "FULL_VERIFICATION_REQUIRED", + "SHADOW_BROADENED", + "SHADOW_MISS_REVIEW_REQUIRED", + "SHADOW_INDETERMINATE", + "AV_INVALID_DEGRADED" ] }, "mechanism": { @@ -292,6 +300,14 @@ "FAILED" ] }, + "command_outcome": { + "enum": [ + "PASSED", + "FAILED", + "SIGNALED", + "INTERRUPTED" + ] + }, "exit_code": { "anyOf": [ { @@ -302,7 +318,7 @@ } ] }, - "signal": { + "signal": { "anyOf": [ { "type": "string", @@ -311,8 +327,22 @@ { "type": "null" } - ] - }, + ] + }, + "interrupted": { + "type": "boolean" + }, + "interruption_reason": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, "duration_ms": { "anyOf": [ { @@ -346,7 +376,7 @@ } ] }, - "evidence_path": { + "evidence_path": { "anyOf": [ { "type": "string", @@ -355,8 +385,29 @@ { "type": "null" } - ] - } + ] + }, + "evidence_status": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "evidence_limitation": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } } } }, @@ -396,6 +447,18 @@ } ] }, + "trust_stage": { + "anyOf": [ + { "enum": ["OBSERVE_SHADOW", "UNKNOWN"] }, + { "type": "null" } + ] + }, + "shadow": { + "anyOf": [ + { "type": "object" }, + { "type": "null" } + ] + }, "limitations": { "type": "array", "items": { diff --git a/packages/tasks-capability/schemas/plan-v2.schema.json b/packages/tasks-capability/schemas/plan-v2.schema.json new file mode 100644 index 0000000..9cfcf90 --- /dev/null +++ b/packages/tasks-capability/schemas/plan-v2.schema.json @@ -0,0 +1,140 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/opsle/affected-verification/schemas/plan-v2.schema.json", + "title": "Affected Verification plan v2", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", "plan_identity", "change", "provenance", + "dependency_completeness", "affected_components", "selected_checks", + "skipped_checks", "risk", "uncertainty", "escalation", + "sufficiency", "argument" + ], + "properties": { + "schema": { "const": "opsle.affected-verification.plan.v2" }, + "plan_identity": { "$ref": "#/$defs/identity" }, + "change": { "type": "object" }, + "provenance": { "type": "object" }, + "dependency_completeness": { + "type": "object", + "additionalProperties": false, + "required": ["checks_assessed", "forced_check_ids", "states", "agreement_implies_completeness"], + "properties": { + "checks_assessed": { "type": "integer", "minimum": 0 }, + "forced_check_ids": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "states": { "type": "object", "additionalProperties": { "type": "integer", "minimum": 0 } }, + "agreement_implies_completeness": { "const": false } + } + }, + "affected_components": { "type": "array" }, + "selected_checks": { "type": "array", "items": { "$ref": "#/$defs/check" } }, + "skipped_checks": { "type": "array", "items": { "$ref": "#/$defs/check" } }, + "risk": { "type": "object" }, + "uncertainty": { "type": "object" }, + "escalation": { "type": "object" }, + "sufficiency": { + "enum": [ + "SUFFICIENT_TARGETED", "SUFFICIENT_BROADENED", + "FULL_VERIFICATION_REQUIRED", "INSUFFICIENT_EVIDENCE" + ] + }, + "argument": { + "type": "object", + "additionalProperties": false, + "required": [ + "claim", "unknown_is_safe_to_skip", "every_skip_explained", + "every_skip_dependency_complete" + ], + "properties": { + "claim": { "type": "string", "minLength": 1 }, + "unknown_is_safe_to_skip": { "const": false }, + "every_skip_explained": { "const": true }, + "every_skip_dependency_complete": { "const": true } + } + } + }, + "$defs": { + "identity": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "reason": { + "type": "object", + "additionalProperties": false, + "required": ["code", "detail", "evidence_refs"], + "properties": { + "code": { "type": "string", "minLength": 1 }, + "detail": { "type": "string", "minLength": 1 }, + "evidence_refs": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } } + } + }, + "boundary": { + "type": "object", + "additionalProperties": false, + "required": ["id", "kind", "status", "relevant", "explanation", "evidence_refs", "source"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "kind": { "$ref": "#/$defs/boundaryKind" }, + "status": { "enum": ["OPEN", "CLOSED", "IRRELEVANT"] }, + "relevant": { "type": "boolean" }, + "explanation": { "type": "string", "minLength": 1 }, + "evidence_refs": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } }, + "source": { + "type": "object", + "additionalProperties": false, + "required": ["path", "line", "construct"], + "properties": { + "path": { "type": "string", "minLength": 1 }, + "line": { "type": "integer", "minimum": 1 }, + "construct": { "type": "string", "minLength": 1 } + } + } + } + }, + "boundaryKind": { + "enum": [ + "SUBPROCESS", "CHILD_INTERPRETER", "DYNAMIC_IMPORT", + "PLUGIN_OR_ENTRY_POINT_DISCOVERY", "EXEC_EVAL_OR_CODE_GENERATION", + "RUNTIME_LOADED_MODULE", "REFLECTION_OR_REGISTRATION" + ] + }, + "dependency": { + "type": "object", + "additionalProperties": false, + "required": [ + "state", "mechanisms", "boundaries", "evidence_coverage", + "unresolved_mechanisms", "explanation", "action", "forced_selection" + ], + "properties": { + "state": { + "enum": [ + "COMPLETE_FOR_CHECK", "COMPLETE_WITH_DECLARED_BOUNDARIES", + "INCOMPLETE", "OPAQUE_BOUNDARY", "UNKNOWN" + ] + }, + "mechanisms": { "type": "array", "items": { "type": "object" } }, + "boundaries": { "type": "array", "items": { "$ref": "#/$defs/boundary" } }, + "evidence_coverage": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "unresolved_mechanisms": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "explanation": { "type": "string", "minLength": 1 }, + "action": { "enum": ["SELECT", "SKIP"] }, + "forced_selection": { "type": "boolean" } + } + }, + "check": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", "type", "command", "scope", "tags", "test_executions", + "reasons", "dependency_completeness" + ], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "type": { "type": "string", "minLength": 1 }, + "command": { "type": "string", "minLength": 1 }, + "scope": { "type": "object" }, + "tags": { "type": "array" }, + "test_executions": { "type": "integer", "minimum": 0 }, + "reasons": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/reason" } }, + "dependency_completeness": { "$ref": "#/$defs/dependency" } + } + } + } +} diff --git a/packages/tasks-capability/schemas/shadow-request-v1.schema.json b/packages/tasks-capability/schemas/shadow-request-v1.schema.json new file mode 100644 index 0000000..aa61ca8 --- /dev/null +++ b/packages/tasks-capability/schemas/shadow-request-v1.schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/opsle/affected-verification/packages/tasks-capability/schemas/shadow-request-v1.schema.json", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "task", + "attemptId", + "executionId", + "generation", + "trustStage", + "results" + ], + "properties": { + "schema": { + "const": "opsle.execution.verification-shadow-request.v1" + }, + "task": { + "type": "object", + "additionalProperties": true, + "required": [ + "id", + "repo_name", + "repo_path", + "worktree_path", + "base_commit" + ], + "properties": { + "id": { "type": "integer", "minimum": 1 }, + "repo_id": { "type": "integer", "minimum": 1 }, + "repo_name": { "type": "string", "minLength": 1 }, + "repo_path": { "type": "string", "pattern": "^/[^\\u0000-\\u001f]+$" }, + "worktree_path": { "type": "string", "pattern": "^/[^\\u0000-\\u001f]+$" }, + "base_commit": { "type": "string", "pattern": "^(?:[0-9a-f]{40}|[0-9a-f]{64})$" }, + "ssh_host": { "type": "string" }, + "ssh_user": { "type": "string" }, + "test_command": { "type": "string" } + } + }, + "attemptId": { "type": "integer", "minimum": 1 }, + "executionId": { "type": "string", "minLength": 1 }, + "generation": { "type": "integer", "minimum": 1 }, + "trustStage": { + "enum": ["OBSERVE_SHADOW", "UNKNOWN"] + }, + "results": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "required": [ + "id", + "type", + "command", + "status", + "exit_code", + "signal", + "interrupted", + "duration_ms" + ], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "type": { "type": "string", "minLength": 1 }, + "command": { "type": "string", "minLength": 1 }, + "status": { "enum": ["PASSED", "FAILED"] }, + "exit_code": { "type": "integer" }, + "signal": { "type": ["string", "null"] }, + "interrupted": { "type": "boolean" }, + "duration_ms": { "type": "number", "minimum": 0 } + } + } + } + } +} diff --git a/packages/tasks-capability/schemas/shadow-result-v1.schema.json b/packages/tasks-capability/schemas/shadow-result-v1.schema.json new file mode 100644 index 0000000..449e2ee --- /dev/null +++ b/packages/tasks-capability/schemas/shadow-result-v1.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/opsle/affected-verification/packages/tasks-capability/schemas/shadow-result-v1.schema.json", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "status", + "shadow", + "receipt", + "receiptPath", + "evidencePath", + "record" + ], + "properties": { + "schema": { + "const": "opsle.execution.verification-shadow-result.v1" + }, + "status": { + "enum": [ + "SHADOW_HEALTHY", + "FULL_VERIFICATION_REQUIRED", + "SHADOW_BROADENED", + "SHADOW_MISS_REVIEW_REQUIRED", + "SHADOW_INDETERMINATE", + "AV_INVALID_DEGRADED" + ] + }, + "shadow": { "type": "object" }, + "receipt": { "type": "object" }, + "receiptPath": { "type": "string", "minLength": 1 }, + "evidencePath": { "type": "string", "minLength": 1 }, + "record": { "type": "object" } + } +} diff --git a/packages/tasks-capability/src/adapter.js b/packages/tasks-capability/src/adapter.js index cacdefd..844428f 100644 --- a/packages/tasks-capability/src/adapter.js +++ b/packages/tasks-capability/src/adapter.js @@ -1,39 +1,74 @@ -import { analyzeAffectedVerification, captureBuildChange } from './verification.js'; +import { + analyzeAffectedVerification, + captureBuildChange, + finalizeAffectedVerification, +} from './verification.js'; import { validateSchema } from './schema.js'; const hooks = { 'verification.plan': ['verification-request-v1', 'verification-analysis-v1'], + 'verification.shadow': ['shadow-request-v1', 'shadow-result-v1'], 'verification.capture': ['capture-request-v1', 'change-set-v1'], }; export function createAdapter({ manifest, configuration, services }, packageIdentity) { validateSchema('tasks-config-v2', configuration); - const binding = structuredClone(services.task); + // Tasks keeps this execution-scoped object current when it allocates or + // refreshes the retained worktree. Holding the reference preserves the + // original execution binding without freezing stale pre-BUILD fields. + const binding = services.task; const config = { ...services.executionConfig, packageIdentity }; return { invoke(hook, payload) { if (!Object.hasOwn(hooks, hook)) throw new Error(`Unsupported capability hook: ${hook}`); const [input, output] = hooks[hook]; validateSchema(input, payload); - for (const key of ['id', 'repo_id', 'repo_name', 'repo_path', 'base_commit', 'worktree_path', 'ssh_host', 'ssh_user', 'sshHost', 'sshUser', 'test_command']) { + // Bind the capability to one task and repository. The base revision and + // retained worktree are lifecycle state: Tasks may advance them after a + // verified merge or restore them during same-attempt continuation. + // Planning/finalization bind their exact source tree independently. + for (const key of ['id', 'repo_id', 'repo_name', 'repo_path', 'ssh_host', 'ssh_user', 'sshHost', 'sshUser']) { if (payload.task[key] !== binding[key]) { throw new Error(`Verification request has a different task binding: ${key}`); } } - if (hook === 'verification.plan' && (payload.attemptId !== services.attemptId + if (['verification.plan', 'verification.shadow'].includes(hook) + && (payload.attemptId !== services.attemptId || payload.executionId !== services.executionId)) throw new Error('Different attempt or execution binding'); const native = hook === 'verification.capture' ? captureBuildChange(payload.task, config) - : analyzeAffectedVerification({ ...payload, config }); + : hook === 'verification.shadow' + ? finalizeAffectedVerification({ + ...payload, + config, + mechanismVersion: manifest.version, + }) + : analyzeAffectedVerification({ ...payload, config }); const contract = manifest.hooks.find(item => item.name === hook); const outputSchema = contract.outputSchema ?? contract.output_schema; const value = { schema: outputSchema, ...native }; validateSchema(output, value); return { schema: 'opsle.capability-result.v1', capability: manifest.id, hook, - output_schema: outputSchema, status: 'ok', value, receipts: [], events: [], + output_schema: outputSchema, status: 'ok', value, + receipts: hook === 'verification.shadow' ? [native.receipt] : [], + events: hook === 'verification.shadow' ? [{ + kind: 'AFFECTED_VERIFICATION_SHADOW', + message: JSON.stringify({ + status: native.status, + classification: native.shadow.classification, + selection_misses: native.shadow.selection_misses.length, + full_run_complete: native.shadow.full_run_complete, + }), + }] : [], artifacts: hook === 'verification.plan' ? [{ kind: 'verification-analysis', path: native.evidencePath, schema: 'opsle.tasks.affected-verification-evidence.v1', + }] : hook === 'verification.shadow' ? [{ + kind: 'verification-analysis', path: native.evidencePath, + schema: 'opsle.tasks.affected-verification-evidence.v1', + }, { + kind: 'value-receipt', path: native.receiptPath, + schema: 'opsle.value-receipt.v1', }] : [], }; }, diff --git a/packages/tasks-capability/src/verification.js b/packages/tasks-capability/src/verification.js index 6d0783a..042ead4 100644 --- a/packages/tasks-capability/src/verification.js +++ b/packages/tasks-capability/src/verification.js @@ -1,11 +1,11 @@ import { executionTarget, projectGit } from './execution.js'; -import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { existsSync, renameSync, writeFileSync } from 'node:fs'; +import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; import { contentIdentity } from './core/canonical.js'; +import { classifyShadow } from './core/shadow.js'; import { planTaskVerification } from './core/task.js'; +import { buildShadowValueReceipt } from './core/value-receipt.js'; import { validateSchema } from './schema.js'; export const AV_MANIFEST_PATH = '.opsle/affected-verification.json'; @@ -16,7 +16,6 @@ const AV_MANIFEST_SCHEMA = 'opsle.affected-verification.manifest.v1'; const AV_EVIDENCE_SCHEMA = 'opsle.tasks.affected-verification-evidence.v1'; const MAX_MANIFEST_BYTES = 1_000_000; const MAX_ARTIFACT_BYTES = 5_000_000; -const packageRoot = fileURLToPath(new URL('..', import.meta.url)); const hash = value => `sha256:${createHash('sha256').update(value).digest('hex')}`; @@ -189,20 +188,7 @@ export function analyzeAffectedVerification({ config, task, attemptId, execution if (manifest.checks.length === 0) { throw new Error('Affected Verification has no catalogued checks; full configured verification is required or execution must stop.'); } - const binary = resolve(packageRoot, 'runtime', 'planner-cli.js'); - if (!existsSync(binary)) throw new Error('Affected Verification CLI is unavailable.'); - const result = spawnSync(process.execPath, [ - binary, 'task-plan', inputPath, '--receipt', receiptPath, - ], { encoding: 'utf8', timeout: 10_000, maxBuffer: MAX_ARTIFACT_BYTES }); - if (result.error) throw new Error(`Affected Verification failed internally: ${result.error.message}`); - if (result.status !== 0) { - throw new Error(`Affected Verification rejected the task change: ${(result.stdout || result.stderr || 'unknown error').trim().slice(0, 2000)}`); - } - try { decision = validateDecision(JSON.parse(result.stdout), request); } - catch (cause) { - if (cause instanceof SyntaxError) throw new Error('Affected Verification returned invalid JSON.'); - throw cause; - } + decision = validateDecision(planTaskVerification(request), request); } catch (cause) { error = String(cause.message || cause).slice(0, 3000); } @@ -236,11 +222,13 @@ export function analyzeAffectedVerification({ config, task, attemptId, execution catalog_complete: manifest.catalog_complete, } : null, input_path: existsSync(inputPath) ? inputPath : null, - value_receipt_path: existsSync(receiptPath) ? receiptPath : null, + value_receipt_path: null, decision, analysis_error: error, verification_results: [], fallback: null, + trust_stage: null, + shadow: null, limitations: [ 'Changed regions are not inferred; path ownership and check completeness come from the immutable base-revision manifest.', 'Passing selected commands proves only their observed process results, not global correctness.', @@ -251,6 +239,150 @@ export function analyzeAffectedVerification({ config, task, attemptId, execution return { change, decision, error, evidencePath, inputPath, receiptPath, record }; } +function readJson(path, label) { + const raw = readFileSync(path); + if (raw.length > MAX_ARTIFACT_BYTES) throw new Error(`${label} exceeds the bounded artifact limit.`); + try { return JSON.parse(raw); } + catch { throw new Error(`${label} is not valid JSON.`); } +} + +function exactResults(plan, results) { + if (!Array.isArray(results)) throw new Error('Authoritative verification results must be an array.'); + const expected = new Map([...plan.selected_checks, ...plan.skipped_checks] + .map((item) => [item.id, { command: item.command, type: item.type }])); + if (results.length !== expected.size || new Set(results.map((item) => item?.id)).size !== results.length) { + throw new Error('Authoritative verification results do not cover the exact full catalog.'); + } + return results.map((item) => { + const action = expected.get(item?.id); + if (!action || item.command !== action.command || item.type !== action.type + || !['PASSED', 'FAILED'].includes(item.status) + || !['PASSED', 'FAILED', 'SIGNALED', 'INTERRUPTED'].includes( + item.command_outcome ?? item.status, + ) + || !Number.isInteger(item.exit_code) + || (item.signal !== null && typeof item.signal !== 'string') + || typeof item.interrupted !== 'boolean' + || !Number.isFinite(item.duration_ms) || item.duration_ms < 0) { + throw new Error('Authoritative verification result is invalid or does not match the immutable catalog.'); + } + return { + id: item.id, + type: item.type, + command: item.command, + status: item.status, + command_outcome: item.command_outcome ?? item.status, + exit_code: item.exit_code, + signal: item.signal, + interrupted: item.interrupted, + interruption_reason: item.interruption_reason ?? null, + duration_ms: item.duration_ms, + stdout_path: item.stdout_path ?? null, + stderr_path: item.stderr_path ?? null, + evidence_path: item.evidence_path ?? null, + evidence_status: item.evidence_status ?? null, + evidence_limitation: item.evidence_limitation ?? null, + }; + }); +} + +function shadowStatus(plan, shadow, trustStage) { + if (trustStage !== 'OBSERVE_SHADOW') return 'AV_INVALID_DEGRADED'; + if (shadow.classification === 'SELECTION_MISS') return 'SHADOW_MISS_REVIEW_REQUIRED'; + if (shadow.classification !== 'NO_SELECTION_MISS') return 'SHADOW_INDETERMINATE'; + if (['FULL_VERIFICATION_REQUIRED', 'INSUFFICIENT_EVIDENCE'] + .includes(plan.sufficiency)) return 'FULL_VERIFICATION_REQUIRED'; + if (plan.sufficiency === 'SUFFICIENT_BROADENED' + || plan.uncertainty.state !== 'NONE' + || plan.dependency_completeness?.forced_check_ids?.length) return 'SHADOW_BROADENED'; + return 'SHADOW_HEALTHY'; +} + +export function finalizeAffectedVerification({ + config, + task, + attemptId, + executionId, + generation, + trustStage, + results, + mechanismVersion, +}) { + const inputPath = resolve(config.logsDir, `task-${task.id}-attempt-${attemptId}-av-${generation}-input.json`); + const receiptPath = resolve(config.logsDir, `task-${task.id}-attempt-${attemptId}-av-${generation}-value-receipt.json`); + const evidencePath = resolve(config.logsDir, `task-${task.id}-attempt-${attemptId}-av-${generation}.json`); + const request = validateSchema('task-request-v1', readJson(inputPath, 'Affected Verification request')); + const previous = readJson(evidencePath, 'Affected Verification evidence'); + if (previous?.schema !== AV_EVIDENCE_SCHEMA + || previous.task_id !== task.id + || previous.attempt_id !== attemptId + || previous.execution_id !== executionId + || previous.generation !== generation + || previous.mechanism?.revision !== config.packageIdentity + || !previous.decision) { + throw new Error('Affected Verification evidence identity does not match this execution.'); + } + const decision = validateDecision(previous.decision, request); + const current = captureBuildChange(task, config); + if (current.identity !== previous.change.identity + || current.base_revision !== previous.repository.base_revision + || current.target_revision !== previous.repository.target_revision + || current.repository_identity !== previous.repository.identity) { + const mismatches = [ + current.identity !== previous.change.identity && 'change', + current.base_revision !== previous.repository.base_revision && 'base', + current.target_revision !== previous.repository.target_revision && 'target', + current.repository_identity !== previous.repository.identity && 'repository', + ].filter(Boolean).join(','); + throw new Error(`Affected Verification source identity drifted after planning (${mismatches}).`); + } + const authoritativeResults = exactResults(decision.plan, results); + const shadow = classifyShadow(decision.plan, { + schema: 'opsle.affected-verification.shadow-input.v1', + change_identity: decision.plan.change.identity, + executed_check_ids: authoritativeResults.map((item) => item.id), + failures: authoritativeResults.filter((item) => item.status === 'FAILED').map((item) => ({ + check_id: item.id, + relevant: true, + reason: 'Authoritative full verification failed for this catalogued check.', + })), + }); + const status = shadowStatus(decision.plan, shadow, trustStage); + const manifest = { + source_path: previous.manifest.source_path, + source_identity: previous.manifest.source_identity, + evidence_complete: previous.manifest.evidence_complete, + catalog_complete: previous.manifest.catalog_complete, + }; + const receipt = buildShadowValueReceipt(decision.plan, shadow, { + mechanismRevision: config.packageIdentity, + mechanismVersion, + runId: executionId, + repository: decision.repository.identity, + taskId: task.id, + attemptId, + executionId, + generation, + trustStage, + decisionIdentity: decision.decision_identity, + manifest, + authoritativeResults, + }); + receipt.extensions.affected_verification.status = status; + atomicJson(receiptPath, receipt); + const record = { + ...previous, + status, + value_receipt_path: receiptPath, + verification_results: authoritativeResults, + trust_stage: trustStage, + shadow, + }; + validateSchema('evidence-v1', record); + atomicJson(evidencePath, record); + return { status, shadow, receipt, receiptPath, evidencePath, record }; +} + export function saveAffectedVerificationRecord(path, record) { atomicJson(path, record); } diff --git a/src/index.js b/src/index.js index 5db141d..b694e34 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,11 @@ export { validateScenarioManifest, validateShadowBenchmarkResult, } from './benchmark.js'; -export { buildValueReceipt, operatorIndicator } from './value-receipt.js'; +export { + buildShadowValueReceipt, + buildValueReceipt, + operatorIndicator, +} from './value-receipt.js'; export { TASK_MANIFEST_SCHEMA, TASK_PLAN_SCHEMA, diff --git a/src/value-receipt.js b/src/value-receipt.js index 26ae17b..f906203 100644 --- a/src/value-receipt.js +++ b/src/value-receipt.js @@ -91,6 +91,138 @@ export function buildValueReceipt(plan, { mechanismRevision = null, runId = null }; } +export function buildShadowValueReceipt(plan, shadow, { + mechanismRevision, + mechanismVersion, + runId, + repository, + taskId, + attemptId, + executionId, + generation, + trustStage, + decisionIdentity, + manifest, + authoritativeResults, +} = {}) { + if (!mechanismRevision || !mechanismVersion || !runId || !repository + || !taskId || !attemptId || !executionId || !generation + || !decisionIdentity || !manifest || !Array.isArray(authoritativeResults)) { + throw new Error('A production shadow receipt requires complete execution and mechanism identity.'); + } + if (shadow?.plan_identity !== plan.plan_identity) { + throw new Error('Shadow result does not match the verification plan.'); + } + const all = [...plan.selected_checks, ...plan.skipped_checks]; + const availableTests = all.filter(isTest).reduce((sum, item) => sum + item.test_executions, 0); + const selectedTests = plan.selected_checks.filter(isTest).reduce((sum, item) => sum + item.test_executions, 0); + const proposedSkippedTests = availableTests - selectedTests; + const proposedSkippedChecks = plan.skipped_checks.length; + const broadening = plan.sufficiency === 'SUFFICIENT_BROADENED' + || plan.sufficiency === 'FULL_VERIFICATION_REQUIRED' + || plan.sufficiency === 'INSUFFICIENT_EVIDENCE' + || plan.dependency_completeness?.forced_check_ids?.length > 0; + const evidence = [ + { id: 'verification_plan', kind: 'CONTENT_HASH', locator: plan.plan_identity, trust: 'VERIFIED' }, + { id: 'shadow_result', kind: 'CONTENT_HASH', locator: shadow.observation_identity, trust: 'VERIFIED' }, + ]; + const measured = input => measurement({ + ...input, + limitation: input.limitation ?? [], + }); + const measurements = [ + measured({ id: 'checks_proposed_selected', baseline: null, result: plan.selected_checks.length, + delta: null, unit: 'count', direction: 'NEUTRAL', operatorDisplay: true }), + measured({ id: 'checks_proposed_skipped', baseline: null, result: proposedSkippedChecks, + delta: null, unit: 'count', direction: 'NEUTRAL', operatorDisplay: true }), + measured({ id: 'test_executions_available', baseline: null, result: availableTests, + delta: null, unit: 'count', direction: 'NEUTRAL', operatorDisplay: false }), + measured({ id: 'test_executions_proposed_selected', baseline: null, result: selectedTests, + delta: null, unit: 'count', direction: 'NEUTRAL', operatorDisplay: true }), + measured({ id: 'test_executions_proposed_skipped', baseline: null, result: proposedSkippedTests, + delta: null, unit: 'count', direction: 'NEUTRAL', operatorDisplay: true }), + measurement({ id: 'full_catalog_checks_executed', baseline: null, + result: shadow.full_run_executed_check_ids.length, delta: null, unit: 'count', + direction: 'PROTECTION_SIGNAL', operatorDisplay: true }), + measurement({ id: 'shadow_misses', baseline: null, result: shadow.selection_misses.length, + delta: null, unit: 'count', direction: 'PROTECTION_SIGNAL', operatorDisplay: true }), + measurement({ id: 'full_verification_authoritative', baseline: null, result: true, + delta: null, unit: 'boolean', direction: 'PROTECTION_SIGNAL', operatorDisplay: true }), + measured({ id: 'broadening_or_escalation', baseline: null, result: broadening, + delta: null, unit: 'boolean', direction: 'PROTECTION_SIGNAL', operatorDisplay: false }), + ]; + for (const item of measurements.slice(5)) item.evidence_refs = ['shadow_result']; + const selectedByType = Object.fromEntries( + [...new Set(plan.selected_checks.map((item) => item.type))].sort().map((type) => [ + type, + plan.selected_checks.filter((item) => item.type === type).length, + ]), + ); + return { + schema: RECEIPT_SCHEMA, + mechanism: { + id: 'opsle.affected-verification', + name: 'Affected Verification', + version: mechanismVersion, + revision: mechanismRevision, + }, + run: { + id: runId, + repository, + task_classification: `task-${taskId}`, + work_classification: 'DETERMINISTIC_VERIFICATION_SHADOW', + }, + operation: { + id: shadow.observation_identity, + name: 'verification-shadow', + configuration_id: plan.provenance.verification_catalog_identity, + policy_id: plan.provenance.policy_identity, + }, + measurements, + evidence, + limitations: [ + 'Full verification remained authoritative; proposed skips were not execution savings.', + 'No time, token, cost, correctness, avoided-execution, or causal savings claim is made.', + 'A shadow miss is a failed check that the targeted plan proposed skipping; full results remain authoritative.', + ], + extensions: { + affected_verification: { + trust_stage: trustStage, + authoritative_verification: 'FULL', + task: { id: taskId, attempt_id: attemptId, execution_id: executionId, generation }, + mechanism: { packaged_revision: mechanismRevision }, + source: { + repository, + base_revision: plan.change.base_revision, + target_revision: plan.change.target_revision, + change_identity: plan.change.identity, + }, + manifest, + plan: { + identity: plan.plan_identity, + decision_identity: decisionIdentity, + sufficiency: plan.sufficiency, + uncertainty: plan.uncertainty, + dependency_completeness: plan.dependency_completeness, + selected_check_ids: plan.selected_checks.map((item) => item.id), + skipped_check_ids: plan.skipped_checks.map((item) => item.id), + selected_by_type: selectedByType, + selected_test_executions: selectedTests, + available_test_executions: availableTests, + proposed_skipped_test_executions: proposedSkippedTests, + }, + policy: { + identity: plan.provenance.policy_identity, + revision: plan.provenance.policy_version, + }, + catalog: { identity: plan.provenance.verification_catalog_identity }, + shadow, + authoritative_results: authoritativeResults, + }, + }, + }; +} + export function operatorIndicator(plan) { const all = [...plan.selected_checks, ...plan.skipped_checks]; const availableTests = all.filter(isTest).reduce((sum, item) => sum + item.test_executions, 0); diff --git a/tests/tasks-capability.test.js b/tests/tasks-capability.test.js index c8f1a14..3f76faf 100644 --- a/tests/tasks-capability.test.js +++ b/tests/tasks-capability.test.js @@ -8,13 +8,19 @@ import { resolve } from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; const root = fileURLToPath(new URL('..', import.meta.url)); -const compatRevision = 'e1207c5264c59e14efe9838bba3a33ba504665d2'; +const compatRevision = 'b76d6253b405469b79d30b260f7ad09827052a4a'; const id = 'opsle.affected-verification'; const schema = 'opsle.execution.verification-request.v1'; const emptySelection = { schema: 'opsle.capability-selection.v1', enable: [], disable: [], configuration: {} }; const grant = { schema: 'opsle.capability-grants.v1', allow: [id] }; const hash = value => createHash('sha256').update(value).digest('hex'); -const run = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000 }); +const commandEnvironment = { ...process.env }; +delete commandEnvironment.npm_config_allow_scripts; +delete commandEnvironment.NPM_CONFIG_ALLOW_SCRIPTS; +commandEnvironment.NODE_ENV = 'test'; +const run = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, + env: commandEnvironment, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + timeout: 60_000 }); const git = (cwd, args) => run('git', args, cwd).trim(); let temp, tarball, packed, upgradeTarball; before(() => { @@ -33,7 +39,7 @@ before(() => { // Build a second, synthetic compatible patch tarball for the restart contract. for (const name of ['package.json', 'opsle-capability.json']) { const path = resolve(packagePath, name); - const value = JSON.parse(readFileSync(path)); value.version = '0.1.1'; + const value = JSON.parse(readFileSync(path)); value.version = '0.2.1'; writeFileSync(path, JSON.stringify(value)); } upgradeTarball = resolve(temp, JSON.parse(run('npm', ['pack', '--json', '--pack-destination', temp], packagePath))[0].filename); @@ -77,6 +83,32 @@ async function direct(directory, f) { executionId: 'exec-1', executionConfig: { logsDir: f.logsDir } } }); } const request = f => ({ schema, task: f.task, attemptId: 1, executionId: 'exec-1', generation: 1 }); +const shadowRequest = (f, plan, overrides = {}) => ({ + schema: 'opsle.execution.verification-shadow-request.v1', + task: f.task, + attemptId: 1, + executionId: 'exec-1', + generation: 1, + trustStage: 'OBSERVE_SHADOW', + results: [...plan.selected_checks, ...plan.skipped_checks].map(item => ({ + id: item.id, + type: item.type, + command: item.command, + status: 'PASSED', + command_outcome: 'PASSED', + exit_code: 0, + signal: null, + interrupted: false, + interruption_reason: null, + duration_ms: 1, + stdout_path: '/private/stdout', + stderr_path: '/private/stderr', + evidence_path: '/private/evidence', + evidence_status: 'VERIFIED', + evidence_limitation: null, + })), + ...overrides, +}); test('installable artifact contains planner and schemas, plans deterministically, captures drift and retains private evidence', async t => { const directory = install('standalone'); @@ -88,7 +120,23 @@ test('installable artifact contains planner and schemas, plans deterministically assert.equal(result.value.error, null); assert.deepEqual(result.value.decision.plan.selected_checks.map(x => x.id), ['a']); assert.deepEqual(result.value.decision.plan.skipped_checks.map(x => x.id), ['b']); - for (const path of [result.value.inputPath, result.value.receiptPath, result.value.evidencePath]) assert.equal(statSync(path).mode & 0o777, 0o600); + for (const path of [result.value.inputPath, result.value.evidencePath]) { + assert.equal(statSync(path).mode & 0o777, 0o600); + } + assert.equal(existsSync(result.value.receiptPath), false, + 'planning cannot emit a receipt before authoritative comparison'); + const finalized = adapter.invoke('verification.shadow', + shadowRequest(f, result.value.decision.plan)); + assert.equal(finalized.receipts.length, 1); + assert.equal(finalized.value.status, 'SHADOW_HEALTHY'); + assert.equal(finalized.value.shadow.classification, 'NO_SELECTION_MISS'); + assert.equal(statSync(finalized.value.receiptPath).mode & 0o777, 0o600); + const receipt = finalized.receipts[0]; + assert.equal(receipt.run.id, 'exec-1'); + assert.equal(receipt.extensions.affected_verification.task.attempt_id, 1); + assert.match(receipt.mechanism.revision, /^sha256:[0-9a-f]{64}$/); + assert.equal(receipt.extensions.affected_verification.plan.identity, + result.value.decision.plan.plan_identity); const repeat = adapter.invoke('verification.plan', { ...request(f), generation: 2 }); assert.deepEqual(repeat.value.decision, result.value.decision); const capture = () => adapter.invoke('verification.capture', { schema: 'opsle.execution.change-capture-request.v1', task: f.task }).value; @@ -177,6 +225,27 @@ test('canonical decisions reject wrong task/change, catalog injection, and forge ]) { const decision = structuredClone(result.decision); mutate(decision); assert.throws(() => validateDecision(decision, input)); } }); +test('shadow finalization rejects stale execution evidence, source drift, and incomplete full results', async t => { + const directory = install('shadow-identity'); + const f = fixture(t, 'shadow-identity-project'); + const adapter = await direct(directory, f); + const analysis = adapter.invoke('verification.plan', request(f)).value; + const full = shadowRequest(f, analysis.decision.plan); + assert.throws(() => adapter.invoke('verification.shadow', { + ...full, + results: full.results.slice(0, 1), + }), /exact full catalog/); + const stored = JSON.parse(readFileSync(analysis.evidencePath)); + writeFileSync(analysis.evidencePath, JSON.stringify({ ...stored, execution_id: 'stale' })); + assert.throws(() => adapter.invoke('verification.shadow', full), + /evidence identity/); + writeFileSync(analysis.evidencePath, JSON.stringify(stored)); + writeFileSync(resolve(f.task.repo_path, 'b.js'), 'drift\n'); + assert.throws(() => adapter.invoke('verification.shadow', full), + /source identity drifted/); + assert.equal(existsSync(analysis.receiptPath), false); +}); + test('SSH failures, invalid targets and quoting remain bounded and fail closed', async t => { const directory = install('ssh'); const { executionTarget, projectGit, sshArguments } = await import(pathToFileURL(resolve(directory, 'runtime/execution.js'))); @@ -233,6 +302,11 @@ test('real generic Tasks lifecycle: discover, grant, dispatch, revoke, remove, r const enabled = await runtime(); const analysis = await enabled.authority('verification.plan', request(f)); assert.equal(analysis.error, null); + const shadow = await enabled.authority('verification.shadow', + shadowRequest(f, analysis.decision.plan)); + assert.equal(shadow.status, 'SHADOW_HEALTHY'); + assert.equal(shadow.receipt.run.id, 'exec-1'); + assert.ok(events.some(x => x.kind === 'AFFECTED_VERIFICATION_SHADOW')); const capture = await enabled.authority('verification.capture', { schema: 'opsle.execution.change-capture-request.v1', task: f.task }); assert.equal(capture.identity, analysis.change.identity); assert.ok(events.some(x => x.kind === 'CAPABILITY_ARTIFACT')); @@ -269,7 +343,7 @@ const runtime = await createCapabilityRuntime(context); const value = await runtime.authority('verification.capture', {schema:'opsle.execution.change-capture-request.v1',task:context.task}); process.stdout.write(JSON.stringify({version:runtime.status[0].version,value}));`); const restarted = JSON.parse(run(process.execPath, [restart, JSON.stringify({ config: { ...config, capabilityRoots: [upgraded] }, task: f.task, attemptId: 1, executionId: 'exec-1', selection: emptySelection })], temp)); - assert.equal(restarted.version, '0.1.1'); + assert.equal(restarted.version, '0.2.1'); assert.equal(restarted.value.identity, capture.identity); assert.deepEqual(readFileSync(analysis.evidencePath), history); assert.equal(git(tasksRoot, ['diff', 'HEAD', '--', 'src']), before); diff --git a/tools/verify-tasks-capability.js b/tools/verify-tasks-capability.js index db2eeec..f5ad436 100644 --- a/tools/verify-tasks-capability.js +++ b/tools/verify-tasks-capability.js @@ -5,7 +5,7 @@ const root = fileURLToPath(new URL('..', import.meta.url)); const runtime = process.env.OPSLE_TASKS_RUNTIME_ROOT; if (!runtime) throw new Error('OPSLE_TASKS_RUNTIME_ROOT must name the trusted Tasks compatibility checkout; lifecycle verification cannot be skipped for release.'); const revision = execFileSync('git', ['-C', runtime, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); -if (revision !== 'e1207c5264c59e14efe9838bba3a33ba504665d2') throw new Error('Unreviewed Tasks compatibility revision'); +if (revision !== 'b76d6253b405469b79d30b260f7ad09827052a4a') throw new Error('Unreviewed Tasks compatibility revision'); const env = { ...process.env, NODE_ENV: 'test', OPSLE_AFFECTED_VERIFICATION_REPO: root }; execFileSync(process.execPath, ['--test', 'tests/tasks-capability.test.js'], { cwd: root, env, stdio: 'inherit' }); // These are the existing generic contract and AV regressions, not a replacement runtime.