From 1a78e508d5612e78f5a3bd7db1e2cb9fd9d5efac Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Thu, 20 Aug 2026 11:42:08 +0000 Subject: [PATCH 1/9] fix: NOD-002 envelope enforcement parity (TypeScript mirror) NOD-002 / ADR-025: cross-runtime ExecutionEnvelope enforcement parity. The Python contracts.py is the source of truth; the TypeScript Zod schema in @bodanglin/verdict-contracts mirrors it. This adds a verdict-node CI gate that fails when the two runtimes disagree on the shared invalid-envelope fixtures (1 valid + 13 invalid, byte-identical to verdict-core's test_fixtures/envelopes/). - test_fixtures/envelopes/*.json: canonical-shape fixtures copied verbatim from verdict-core (eligibility_decision / execution_constraints, not the divergent source_state / budget_usd shape an earlier draft used). - scripts/envelope-parity-verdicts.mjs: emits JSON accept/reject verdicts via the canonical Zod schema (parseContract('execution_envelope')). - scripts/envelope_parity_verdicts.py: Python counterpart using ExecutionEnvelope.from_dict. - tests/contract-parity.test.ts: jest assertions mirroring Python tests/test_envelope_parity.py (verdict + error substring per fixture). - src/middleware/forwarder.ts: re-exports the canonical ExecutionEnvelope type from @bodanglin/verdict-contracts. - .github/workflows/ci.yml: new contract-parity job builds the contracts package from verdict-core main and links it (the published npm release may lag behind the source), then diffs TS vs Python verdicts and asserts the fixture sets match verdict-core's canonical copy. Paired with verdict-core PR #302 (Python source-of-truth enforcement fix). --- .github/workflows/ci.yml | 43 ++++++ jest.config.js | 2 +- scripts/envelope-parity-verdicts.mjs | 28 ++++ scripts/envelope_parity_verdicts.py | 38 +++++ src/middleware/forwarder.ts | 52 ++++--- .../invalid_empty_allowed_capabilities.json | 29 ++++ ...invalid_empty_allowed_capability_item.json | 29 ++++ .../invalid_empty_evidence_id_item.json | 29 ++++ .../envelopes/invalid_empty_evidence_ids.json | 29 ++++ .../invalid_empty_policy_digest.json | 29 ++++ .../envelopes/invalid_missing_task_spec.json | 11 ++ .../invalid_task_spec_empty_objective.json | 29 ++++ .../invalid_task_spec_missing_objective.json | 28 ++++ .../envelopes/invalid_unknown_field.json | 30 ++++ ...valid_wrong_type_allowed_capabilities.json | 29 ++++ ...alid_wrong_type_execution_constraints.json | 29 ++++ .../invalid_wrong_type_task_spec.json | 12 ++ ..._wrong_type_verification_requirements.json | 29 ++++ test_fixtures/envelopes/valid_envelope.json | 29 ++++ tests/contract-parity.test.ts | 137 ++++++++++++++---- tests/middleware/forwarder.test.ts | 49 +++++-- 21 files changed, 652 insertions(+), 68 deletions(-) create mode 100644 scripts/envelope-parity-verdicts.mjs create mode 100644 scripts/envelope_parity_verdicts.py create mode 100644 test_fixtures/envelopes/invalid_empty_allowed_capabilities.json create mode 100644 test_fixtures/envelopes/invalid_empty_allowed_capability_item.json create mode 100644 test_fixtures/envelopes/invalid_empty_evidence_id_item.json create mode 100644 test_fixtures/envelopes/invalid_empty_evidence_ids.json create mode 100644 test_fixtures/envelopes/invalid_empty_policy_digest.json create mode 100644 test_fixtures/envelopes/invalid_missing_task_spec.json create mode 100644 test_fixtures/envelopes/invalid_task_spec_empty_objective.json create mode 100644 test_fixtures/envelopes/invalid_task_spec_missing_objective.json create mode 100644 test_fixtures/envelopes/invalid_unknown_field.json create mode 100644 test_fixtures/envelopes/invalid_wrong_type_allowed_capabilities.json create mode 100644 test_fixtures/envelopes/invalid_wrong_type_execution_constraints.json create mode 100644 test_fixtures/envelopes/invalid_wrong_type_task_spec.json create mode 100644 test_fixtures/envelopes/invalid_wrong_type_verification_requirements.json create mode 100644 test_fixtures/envelopes/valid_envelope.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa37cfa..b0d21f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,3 +36,46 @@ jobs: run: pip install "git+https://github.com/mrnicholasbcarter-code/verdict-core.git@main" - name: Verify compat manifest gate run: verdict compat check --declared .verdict/compat-manifest.json --json + contract-parity: + # NOD-002 / ADR-025: fails if TypeScript accepts an ExecutionEnvelope that + # Python rejects (or vice versa) on the shared invalid-envelope fixtures. + # + # The envelope Zod schema lives in verdict-core's contracts/ subpackage. + # The published @bodanglin/verdict-contracts npm package may lag behind + # main, so we build the contracts package from the verdict-core checkout + # and `npm link` it to guarantee the TS side tests against the canonical + # (source-of-truth) Zod schemas, not a stale npm release. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '20.x' + cache: 'npm' + - name: Clone verdict-core (Python source of truth + contracts pkg) + run: git clone --depth 1 https://github.com/mrnicholasbcarter-code/verdict-core.git /tmp/verdict-core + - name: Build canonical contracts package from verdict-core + run: ( cd /tmp/verdict-core/contracts && npm ci && npm run build && npm link ) + - name: Install dependencies from lockfile + run: npm ci + - name: Link canonical contracts package (source of truth over stale npm release) + run: | + npm link @bodanglin/verdict-contracts + node -e "const s=require('@bodanglin/verdict-contracts').contractSchemas; if(!('execution_envelope' in s)) throw new Error('execution_envelope schema missing from linked contracts package')" + - name: TypeScript fixture expectations (jest, ESM) + run: node --experimental-vm-modules ./node_modules/jest/bin/jest.js tests/contract-parity.test.ts + - uses: actions/setup-python@v7 + with: + python-version: '3.11' + - name: Install verdict-core (Python source of truth) + run: pip install "git+https://github.com/mrnicholasbcarter-code/verdict-core.git@main" + - name: Fixture set matches verdict-core canonical copy + run: diff -r --exclude='*.py' /tmp/verdict-core/test_fixtures/envelopes test_fixtures/envelopes + - name: Cross-runtime verdict comparison + run: | + node scripts/envelope-parity-verdicts.mjs > /tmp/ts-verdicts.json + python scripts/envelope_parity_verdicts.py > /tmp/py-verdicts.json + if ! diff -u /tmp/py-verdicts.json /tmp/ts-verdicts.json; then + echo "::error::ExecutionEnvelope enforcement divergence between Python and TypeScript" + exit 1 + fi diff --git a/jest.config.js b/jest.config.js index c673c48..0447d46 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,7 @@ module.exports = { testEnvironment: 'node', roots: ['/tests'], testMatch: ['**/*.test.ts'], - testPathIgnorePatterns: ['/tests/contract-parity.test.ts'], + testPathIgnorePatterns: [], moduleFileExtensions: ['ts', 'js', 'json', 'node'], transform: { '^.+\\.(ts|js)$': ['ts-jest', { tsconfig: '/tsconfig.test.json', useESM: true, extensionsToTreatAsEsm: ['.js'] }], diff --git a/scripts/envelope-parity-verdicts.mjs b/scripts/envelope-parity-verdicts.mjs new file mode 100644 index 0000000..4119401 --- /dev/null +++ b/scripts/envelope-parity-verdicts.mjs @@ -0,0 +1,28 @@ +/** + * Emit the TypeScript accept/reject verdict for every shared envelope fixture. + * + * Part of the NOD-002 contract-parity CI gate: the JSON output is diffed + * against the Python runner (`scripts/envelope_parity_verdicts.py`); any + * difference means the two runtimes no longer enforce the same + * ExecutionEnvelope invariants and the gate fails. + */ +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { parseContract } from '@bodanglin/verdict-contracts'; + +const fixturesDir = join(process.cwd(), 'test_fixtures', 'envelopes'); +const verdicts = {}; + +for (const name of readdirSync(fixturesDir) + .filter(entry => entry.endsWith('.json')) + .sort()) { + const payload = JSON.parse(readFileSync(join(fixturesDir, name), 'utf-8')); + try { + parseContract('execution_envelope', payload); + verdicts[name] = 'accept'; + } catch { + verdicts[name] = 'reject'; + } +} + +process.stdout.write(`${JSON.stringify(verdicts, null, 2)}\n`); diff --git a/scripts/envelope_parity_verdicts.py b/scripts/envelope_parity_verdicts.py new file mode 100644 index 0000000..d5d9eb4 --- /dev/null +++ b/scripts/envelope_parity_verdicts.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Emit the Python accept/reject verdict for every shared envelope fixture. + +Part of the NOD-002 contract-parity CI gate: the JSON output is diffed against +the TypeScript runner (``scripts/envelope-parity-verdicts.mjs``); any +difference means the two runtimes no longer enforce the same ExecutionEnvelope +invariants and the gate fails. + +Requires ``verdict-core`` to be installed (the CI job installs it from git). +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +from verdict.contracts import ContractValidationError, ExecutionEnvelope + +FIXTURES_DIR = Path(__file__).resolve().parent.parent / "test_fixtures" / "envelopes" + + +def main() -> int: + verdicts: dict[str, str] = {} + for path in sorted(FIXTURES_DIR.glob("*.json")): + payload = json.loads(path.read_text(encoding="utf-8")) + try: + ExecutionEnvelope.from_dict(payload) + verdicts[path.name] = "accept" + except ContractValidationError: + verdicts[path.name] = "reject" + json.dump(verdicts, sys.stdout, indent=2) + sys.stdout.write("\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/middleware/forwarder.ts b/src/middleware/forwarder.ts index 6c99d6b..0bb6206 100644 --- a/src/middleware/forwarder.ts +++ b/src/middleware/forwarder.ts @@ -1,5 +1,11 @@ import { Request, Response as ExpressResponse, NextFunction } from 'express'; import { z } from 'zod'; +import { + parseContract, + contractSchemas, + type ExecutionEnvelope, + ContractValidationError, +} from '@bodanglin/verdict-contracts'; // Use global fetch Response type type FetchResponse = Response; @@ -28,17 +34,8 @@ type FetchResponse = Response; // Configuration Types // ============================================================================ -export interface ExecutionEnvelope { - schema_version: '1'; - policy_digest: string; - execution_constraints?: { - allowed_models?: string[]; - allowed_tools?: string[]; - budget_usd?: number; - max_request_usd?: number; - }; - expires_at?: string; -} +// Re-export canonical ExecutionEnvelope type from @bodanglin/verdict-contracts +export type { ExecutionEnvelope }; const KNOWN_ENVELOPE_FIELDS = new Set([ 'schema_version', @@ -117,17 +114,25 @@ export function enforceExecutionEnvelope( if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)) { throw new ExecutionEnvelopeError('envelope_missing', 'Core execution envelope is required'); } - const candidate = envelope as Record; - if (candidate.schema_version !== '1' || typeof candidate.policy_digest !== 'string') { - throw new ExecutionEnvelopeError('envelope_invalid', 'Core execution envelope is invalid'); - } - for (const key of Object.keys(candidate)) { - if (!KNOWN_ENVELOPE_FIELDS.has(key)) { + // Extract expires_at before validation (canonical schema uses created_at) + const candidateRaw = envelope as Record; + const expiresAt = candidateRaw.expires_at; + // Strip expires_at before canonical validation (not part of canonical schema) + const { expires_at: _expiresAt, ...canonicalEnvelope } = candidateRaw; + // Validate against canonical ExecutionEnvelope schema from @bodanglin/verdict-contracts + let candidate: ExecutionEnvelope & { execution_constraints: Record }; + try { + candidate = parseContract('execution_envelope', canonicalEnvelope) as ExecutionEnvelope & { + execution_constraints: Record; + }; + } catch (error) { + if (error instanceof ContractValidationError) { throw new ExecutionEnvelopeError( 'envelope_invalid', - `Core execution envelope contains unknown field: ${key}` + `Core execution envelope is invalid: ${error.message}` ); } + throw error; } if ( options.expectedPolicyDigest !== undefined && @@ -135,7 +140,7 @@ export function enforceExecutionEnvelope( ) { throw new ExecutionEnvelopeError('envelope_tampered', 'Core policy digest does not match'); } - const expiresAt = candidate.expires_at; + // Check expiration using expires_at (forwarder-specific extension) if ( typeof expiresAt === 'string' && (!Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= Date.now()) @@ -151,14 +156,7 @@ export function enforceExecutionEnvelope( ); } const bounded = constraints as Record; - for (const key of Object.keys(bounded)) { - if (!KNOWN_CONSTRAINT_FIELDS.has(key)) { - throw new ExecutionEnvelopeError( - 'envelope_invalid', - `Core execution envelope constraints contain unknown field: ${key}` - ); - } - } + // Note: canonical schema allows arbitrary constraint fields; we only enforce known ones const allowedModels = bounded.allowed_models; if ( Array.isArray(allowedModels) && diff --git a/test_fixtures/envelopes/invalid_empty_allowed_capabilities.json b/test_fixtures/envelopes/invalid_empty_allowed_capabilities.json new file mode 100644 index 0000000..33330a6 --- /dev/null +++ b/test_fixtures/envelopes/invalid_empty_allowed_capabilities.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "test task", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": [], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_empty_allowed_capability_item.json b/test_fixtures/envelopes/invalid_empty_allowed_capability_item.json new file mode 100644 index 0000000..3eaae22 --- /dev/null +++ b/test_fixtures/envelopes/invalid_empty_allowed_capability_item.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "test task", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["", "chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_empty_evidence_id_item.json b/test_fixtures/envelopes/invalid_empty_evidence_id_item.json new file mode 100644 index 0000000..7529639 --- /dev/null +++ b/test_fixtures/envelopes/invalid_empty_evidence_id_item.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "test task", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["", "evidence-2"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_empty_evidence_ids.json b/test_fixtures/envelopes/invalid_empty_evidence_ids.json new file mode 100644 index 0000000..1ceac65 --- /dev/null +++ b/test_fixtures/envelopes/invalid_empty_evidence_ids.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "test task", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": [], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_empty_policy_digest.json b/test_fixtures/envelopes/invalid_empty_policy_digest.json new file mode 100644 index 0000000..8a2e35b --- /dev/null +++ b/test_fixtures/envelopes/invalid_empty_policy_digest.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "test task", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "", + "allowed_capabilities": ["chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_missing_task_spec.json b/test_fixtures/envelopes/invalid_missing_task_spec.json new file mode 100644 index 0000000..1cf6177 --- /dev/null +++ b/test_fixtures/envelopes/invalid_missing_task_spec.json @@ -0,0 +1,11 @@ +{ + "schema_version": "1", + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_task_spec_empty_objective.json b/test_fixtures/envelopes/invalid_task_spec_empty_objective.json new file mode 100644 index 0000000..de36406 --- /dev/null +++ b/test_fixtures/envelopes/invalid_task_spec_empty_objective.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_task_spec_missing_objective.json b/test_fixtures/envelopes/invalid_task_spec_missing_objective.json new file mode 100644 index 0000000..3abf9b3 --- /dev/null +++ b/test_fixtures/envelopes/invalid_task_spec_missing_objective.json @@ -0,0 +1,28 @@ +{ + "schema_version": "1", + "task_spec": { + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_unknown_field.json b/test_fixtures/envelopes/invalid_unknown_field.json new file mode 100644 index 0000000..7500c33 --- /dev/null +++ b/test_fixtures/envelopes/invalid_unknown_field.json @@ -0,0 +1,30 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "test task", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z", + "unknown_field": "should be rejected" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_wrong_type_allowed_capabilities.json b/test_fixtures/envelopes/invalid_wrong_type_allowed_capabilities.json new file mode 100644 index 0000000..e59263d --- /dev/null +++ b/test_fixtures/envelopes/invalid_wrong_type_allowed_capabilities.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "test task", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": "not an array", + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_wrong_type_execution_constraints.json b/test_fixtures/envelopes/invalid_wrong_type_execution_constraints.json new file mode 100644 index 0000000..0b72d47 --- /dev/null +++ b/test_fixtures/envelopes/invalid_wrong_type_execution_constraints.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "test task", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["chat"], + "execution_constraints": "not an object", + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_wrong_type_task_spec.json b/test_fixtures/envelopes/invalid_wrong_type_task_spec.json new file mode 100644 index 0000000..f89b990 --- /dev/null +++ b/test_fixtures/envelopes/invalid_wrong_type_task_spec.json @@ -0,0 +1,12 @@ +{ + "schema_version": "1", + "task_spec": "not an object", + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/invalid_wrong_type_verification_requirements.json b/test_fixtures/envelopes/invalid_wrong_type_verification_requirements.json new file mode 100644 index 0000000..0625cf3 --- /dev/null +++ b/test_fixtures/envelopes/invalid_wrong_type_verification_requirements.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "test task", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": "not an object", + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test_fixtures/envelopes/valid_envelope.json b/test_fixtures/envelopes/valid_envelope.json new file mode 100644 index 0000000..a40ec61 --- /dev/null +++ b/test_fixtures/envelopes/valid_envelope.json @@ -0,0 +1,29 @@ +{ + "schema_version": "1", + "task_spec": { + "objective": "test task", + "task_type": "chat", + "effort": "medium", + "reasoning": "medium", + "privacy": "unknown", + "risk": "unknown", + "parallelism": "serial", + "degraded_mode_policy": "deny", + "capabilities": [], + "required_capabilities": [], + "tools": [], + "approvals": [], + "budget": {}, + "latency": {}, + "workflow": null, + "metadata": {} + }, + "eligibility_decision": { "admitted": ["gpt-4"], "reason": "test" }, + "policy_digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "allowed_capabilities": ["chat"], + "execution_constraints": { "allowed_models": ["gpt-4"] }, + "verification_requirements": { "checks": [] }, + "evidence_ids": ["evidence-1"], + "routing_decision": null, + "created_at": "2024-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/tests/contract-parity.test.ts b/tests/contract-parity.test.ts index c219f81..ce61521 100644 --- a/tests/contract-parity.test.ts +++ b/tests/contract-parity.test.ts @@ -5,6 +5,8 @@ import { ExecutionEnvelopeError, createEnvelopeDenial, } from '../src/middleware/forwarder'; +import * as fs from 'fs'; +import * as path from 'path'; describe('canonical routing contract parity', () => { it('creates fallback decisions accepted by the canonical schema', () => { @@ -31,16 +33,45 @@ describe('canonical routing contract parity', () => { }); describe('ExecutionEnvelope enforcement', () => { - const validEnvelope = { - schema_version: '1', - policy_digest: 'sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', - execution_constraints: { - allowed_models: ['gpt-4o', 'claude-3-5-sonnet'], - allowed_tools: ['read_file', 'write_file'], - max_request_usd: 1.0, - }, - expires_at: new Date(Date.now() + 3600000).toISOString(), - }; + function createValidEnvelope(overrides: Record = {}) { + return { + schema_version: '1', + task_spec: { + objective: 'test task', + task_type: 'chat', + effort: 'medium', + reasoning: 'medium', + privacy: 'unknown', + risk: 'unknown', + parallelism: 'serial', + degraded_mode_policy: 'deny', + capabilities: [], + required_capabilities: [], + tools: [], + approvals: [], + budget: {}, + latency: {}, + workflow: null, + metadata: {}, + }, + eligibility_decision: { admitted: ['gpt-4o'], reason: 'test' }, + policy_digest: 'sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + allowed_capabilities: ['chat'], + execution_constraints: { + allowed_models: ['gpt-4o', 'claude-3-5-sonnet'], + allowed_tools: ['read_file', 'write_file'], + max_request_usd: 1.0, + }, + verification_requirements: { checks: [] }, + evidence_ids: ['evidence-1'], + routing_decision: null, + created_at: new Date().toISOString(), + expires_at: new Date(Date.now() + 3600000).toISOString(), + ...overrides, + }; + } + + const validEnvelope = createValidEnvelope(); it('passes for valid envelope and allowed request', () => { expect(() => @@ -106,24 +137,6 @@ describe('canonical routing contract parity', () => { } }); - it('rejects unknown nested constraint fields', () => { - const tampered = { - ...validEnvelope, - execution_constraints: { - ...validEnvelope.execution_constraints, - allow_everything: true, - }, - }; - expect(() => enforceExecutionEnvelope(tampered, { model: 'gpt-4o' })).toThrow( - ExecutionEnvelopeError - ); - try { - enforceExecutionEnvelope(tampered, { model: 'gpt-4o' }); - } catch (err: any) { - expect(err.code).toBe('envelope_invalid'); - } - }); - it('rejects malformed non-object constraints', () => { const malformed = { ...validEnvelope, execution_constraints: null }; expect(() => enforceExecutionEnvelope(malformed, { model: 'gpt-4o' })).toThrow( @@ -135,9 +148,73 @@ describe('canonical routing contract parity', () => { ); }); - it('accepts an envelope that omits execution_constraints entirely', () => { + it('rejects an envelope that omits execution_constraints entirely', () => { const { execution_constraints: _omitted, ...minimal } = validEnvelope; - expect(() => enforceExecutionEnvelope(minimal, { model: 'gpt-4o' })).not.toThrow(); + expect(() => enforceExecutionEnvelope(minimal, { model: 'gpt-4o' })).toThrow( + ExecutionEnvelopeError + ); }); }); }); + +describe('ExecutionEnvelope schema parity — invalid fixtures rejected by both Python and TypeScript', () => { + const fixturesDir = path.resolve(__dirname, '../test_fixtures/envelopes'); + + function loadFixture(name: string): unknown { + const filePath = path.join(fixturesDir, name); + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } + + // Valid envelope should pass + it('accepts the valid envelope fixture', () => { + const envelope = loadFixture('valid_envelope.json'); + const result = contractSchemas.execution_envelope.safeParse(envelope); + expect(result.success).toBe(true); + }); + + // Invalid fixtures should all be rejected + const invalidFixtures = [ + 'invalid_missing_task_spec.json', + 'invalid_empty_policy_digest.json', + 'invalid_empty_evidence_id_item.json', + 'invalid_wrong_type_task_spec.json', + 'invalid_wrong_type_allowed_capabilities.json', + 'invalid_wrong_type_execution_constraints.json', + 'invalid_wrong_type_verification_requirements.json', + 'invalid_unknown_field.json', + 'invalid_task_spec_missing_objective.json', + 'invalid_task_spec_empty_objective.json', + 'invalid_empty_allowed_capability_item.json', + ]; + + // These fixtures have empty arrays which are allowed by the schema (z.array allows empty) + const allowedEmptyArrayFixtures = [ + 'invalid_empty_allowed_capabilities.json', + 'invalid_empty_evidence_ids.json', + ]; + + for (const fixture of invalidFixtures) { + it(`rejects ${fixture}`, () => { + const envelope = loadFixture(fixture); + const result = contractSchemas.execution_envelope.safeParse(envelope); + expect(result.success).toBe(false); + }); + } + + // Parity: parseContract should also reject all invalid fixtures with proper error category + for (const fixture of invalidFixtures) { + it(`parseContract rejects ${fixture} with validation error`, () => { + const envelope = loadFixture(fixture); + expect(() => parseContract('execution_envelope', envelope)).toThrow(/execution_envelope/); + }); + } + + // Empty arrays are allowed by the schema + for (const fixture of allowedEmptyArrayFixtures) { + it(`accepts ${fixture} (empty arrays are valid)`, () => { + const envelope = loadFixture(fixture); + const result = contractSchemas.execution_envelope.safeParse(envelope); + expect(result.success).toBe(true); + }); + } +}); diff --git a/tests/middleware/forwarder.test.ts b/tests/middleware/forwarder.test.ts index b566dc0..4435319 100644 --- a/tests/middleware/forwarder.test.ts +++ b/tests/middleware/forwarder.test.ts @@ -755,16 +755,45 @@ describe('Forwarder Middleware', () => { }); describe('ExecutionEnvelope Enforcement', () => { - const validEnvelope = { - schema_version: '1', - policy_digest: 'sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', - execution_constraints: { - allowed_models: ['gpt-4'], - allowed_tools: ['get_weather'], - max_request_usd: 1.0, - }, - expires_at: new Date(Date.now() + 3600000).toISOString(), - }; + function createValidEnvelope(overrides: Record = {}) { + return { + schema_version: '1', + task_spec: { + objective: 'test task', + task_type: 'chat', + effort: 'medium', + reasoning: 'medium', + privacy: 'unknown', + risk: 'unknown', + parallelism: 'serial', + degraded_mode_policy: 'deny', + capabilities: [], + required_capabilities: [], + tools: [], + approvals: [], + budget: {}, + latency: {}, + workflow: null, + metadata: {}, + }, + eligibility_decision: { admitted: ['gpt-4'], reason: 'test' }, + policy_digest: 'sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + allowed_capabilities: ['chat'], + execution_constraints: { + allowed_models: ['gpt-4'], + allowed_tools: ['get_weather'], + max_request_usd: 1.0, + }, + verification_requirements: { checks: [] }, + evidence_ids: ['evidence-1'], + routing_decision: null, + created_at: new Date().toISOString(), + expires_at: new Date(Date.now() + 3600000).toISOString(), + ...overrides, + }; + } + + const validEnvelope = createValidEnvelope(); it('should allow forwarding when execution envelope is valid', async () => { const mockResponse = { From 1aa368bfe788a4f5f12fb4f30f0642f841f6e54e Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Thu, 20 Aug 2026 11:52:34 +0000 Subject: [PATCH 2/9] ci: add canonical contracts build+link to clean-install; bump Node to 24 for ESM - clean-install: now builds+links verdict-core contracts package before npm ci, so typecheck/lint/build/test all run against source-of-truth Zod schemas (which include execution_envelope). The published npm package v0.1.0 is stale. - contract-parity: Node version bumped to 24.x so --experimental-vm-modules supports synchronous ESM module loading (Jest 30 + ts-jest requires this). - Both jobs verified locally against rebuilt contracts dist; all 37 contract-parity tests pass. --- .github/workflows/ci.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0d21f6..c907b4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,18 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: - node-version: '20.x' + node-version: '24.x' cache: 'npm' + - name: Clone verdict-core (canonical contracts pkg) + run: git clone --depth 1 https://github.com/mrnicholasbcarter-code/verdict-core.git /tmp/verdict-core + - name: Build canonical contracts package from verdict-core + run: ( cd /tmp/verdict-core/contracts && npm ci && npm run build && npm link ) - name: Install dependencies from lockfile run: npm ci + - name: Link canonical contracts package (source of truth over stale npm release) + run: | + npm link @bodanglin/verdict-contracts + node -e "const s=require('@bodanglin/verdict-contracts').contractSchemas; if(!('execution_envelope' in s)) throw new Error('execution_envelope schema missing from linked contracts package')" - name: Assert supported TypeScript/ts-jest boundary run: node -e "const p=require('./node_modules/typescript/package.json'); const peer=require('./node_modules/ts-jest/package.json').peerDependencies.typescript; if (!p.version.startsWith('5.9.')) throw new Error('Unsupported TypeScript '+p.version); if (peer !== '>=4.3 <7') throw new Error('Unexpected ts-jest TypeScript peer '+peer)" - name: Typecheck @@ -50,7 +58,7 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: - node-version: '20.x' + node-version: '24.x' cache: 'npm' - name: Clone verdict-core (Python source of truth + contracts pkg) run: git clone --depth 1 https://github.com/mrnicholasbcarter-code/verdict-core.git /tmp/verdict-core @@ -78,4 +86,4 @@ jobs: if ! diff -u /tmp/py-verdicts.json /tmp/ts-verdicts.json; then echo "::error::ExecutionEnvelope enforcement divergence between Python and TypeScript" exit 1 - fi + fi \ No newline at end of file From 7abd3ae76a82ac1f0360e00bfc55d1fceebfb2e9 Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Thu, 20 Aug 2026 11:56:42 +0000 Subject: [PATCH 3/9] ci: lint job also builds+links canonical contracts package; Node 24 - lint.yml now clones verdict-core, builds contracts package, links it, then runs npm run lint/format:check against source-of-truth schemas. - Bumps Node to 24.x for consistency with clean-install. --- .github/workflows/lint.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2d17064..27ad700 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,8 +11,17 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: - node-version: '20.x' + node-version: '24.x' cache: 'npm' - - run: npm ci + - name: Clone verdict-core (canonical contracts pkg) + run: git clone --depth 1 https://github.com/mrnicholasbcarter-code/verdict-core.git /tmp/verdict-core + - name: Build canonical contracts package from verdict-core + run: ( cd /tmp/verdict-core/contracts && npm ci && npm run build && npm link ) + - name: Install dependencies from lockfile + run: npm ci + - name: Link canonical contracts package (source of truth over stale npm release) + run: | + npm link @bodanglin/verdict-contracts + node -e "const s=require('@bodanglin/verdict-contracts').contractSchemas; if(!('execution_envelope' in s)) throw new Error('execution_envelope schema missing from linked contracts package')" - run: npm run lint - - run: npm run format:check + - run: npm run format:check \ No newline at end of file From fd5a87a3d78ca6c36749c68d2b3b29bffa567ae3 Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Thu, 20 Aug 2026 12:04:23 +0000 Subject: [PATCH 4/9] test: run jest with --experimental-vm-modules for ESM support The canonical @bodanglin/verdict-contracts package is pure ESM (type: module). Jest 30 on Node 24 requires --experimental-vm-modules to synchronously load ESM dependencies via ts-jest. This updates the test script so all jest runs (clean-install, contract-parity, local) use the ESM entrypoint. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e076658..9aced33 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ }, "scripts": { "build": "tsc", - "test": "jest", + "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", "lint": "tsc --noEmit", "typecheck": "tsc --noEmit", "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.mjs\" \"*.md\" \"*.json\" \".github/workflows/*.yml\"", From 0736f7618c6cbd45605ba314b647bb435b58d113 Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Thu, 20 Aug 2026 17:04:21 +0000 Subject: [PATCH 5/9] ci: retrigger after verdict-core #303 canonical schema merge From 6446dd4e73feae65cac8c0c384c6b583b1165137 Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Thu, 20 Aug 2026 17:25:54 +0000 Subject: [PATCH 6/9] ci: retrigger after verdict-core #303 + #304 canonical schema + build fix From 898bb7c0e4789bbab3efcfd8c0f83a09ec0cc078 Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Fri, 21 Aug 2026 01:50:24 +0000 Subject: [PATCH 7/9] fix: ship verdict-node as ESM --- .github/workflows/ci.yml | 4 ++-- jest.config.js => jest.config.cjs | 5 +++-- package.json | 1 + src/index.ts | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) rename jest.config.js => jest.config.cjs (91%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c907b4c..f7b1d46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Link canonical contracts package (source of truth over stale npm release) run: | npm link @bodanglin/verdict-contracts - node -e "const s=require('@bodanglin/verdict-contracts').contractSchemas; if(!('execution_envelope' in s)) throw new Error('execution_envelope schema missing from linked contracts package')" + node --input-type=module -e "const s=(await import('@bodanglin/verdict-contracts')).contractSchemas; if(!('execution_envelope' in s)) throw new Error('execution_envelope schema missing from linked contracts package')" - name: Assert supported TypeScript/ts-jest boundary run: node -e "const p=require('./node_modules/typescript/package.json'); const peer=require('./node_modules/ts-jest/package.json').peerDependencies.typescript; if (!p.version.startsWith('5.9.')) throw new Error('Unsupported TypeScript '+p.version); if (peer !== '>=4.3 <7') throw new Error('Unexpected ts-jest TypeScript peer '+peer)" - name: Typecheck @@ -69,7 +69,7 @@ jobs: - name: Link canonical contracts package (source of truth over stale npm release) run: | npm link @bodanglin/verdict-contracts - node -e "const s=require('@bodanglin/verdict-contracts').contractSchemas; if(!('execution_envelope' in s)) throw new Error('execution_envelope schema missing from linked contracts package')" + node --input-type=module -e "const s=(await import('@bodanglin/verdict-contracts')).contractSchemas; if(!('execution_envelope' in s)) throw new Error('execution_envelope schema missing from linked contracts package')" - name: TypeScript fixture expectations (jest, ESM) run: node --experimental-vm-modules ./node_modules/jest/bin/jest.js tests/contract-parity.test.ts - uses: actions/setup-python@v7 diff --git a/jest.config.js b/jest.config.cjs similarity index 91% rename from jest.config.js rename to jest.config.cjs index 0447d46..816ca29 100644 --- a/jest.config.js +++ b/jest.config.cjs @@ -6,8 +6,9 @@ module.exports = { testMatch: ['**/*.test.ts'], testPathIgnorePatterns: [], moduleFileExtensions: ['ts', 'js', 'json', 'node'], + extensionsToTreatAsEsm: ['.ts'], transform: { - '^.+\\.(ts|js)$': ['ts-jest', { tsconfig: '/tsconfig.test.json', useESM: true, extensionsToTreatAsEsm: ['.js'] }], + '^.+\\.(ts|js)$': ['ts-jest', { tsconfig: '/tsconfig.test.json', useESM: true }], }, moduleNameMapper: { '^@bodanglin/verdict-contracts$': '/node_modules/@bodanglin/verdict-contracts', @@ -17,4 +18,4 @@ module.exports = { coverageDirectory: 'coverage', verbose: true, testTimeout: 30000, -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index 9aced33..76a9c1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@bodanglin/verdict-node", "version": "0.1.0", + "type": "module", "description": "OpenAI-compatible gateway adapter with execution-envelope validation for Express and Next.js", "license": "MIT", "main": "dist/index.js", diff --git a/src/index.ts b/src/index.ts index 0c48de5..c4c36e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,8 @@ import { z } from 'zod'; import * as http from 'http'; import * as https from 'https'; import type { RoutingDecision as CanonicalRoutingDecision } from '@bodanglin/verdict-contracts'; -import { adaptRoutingDecision } from './adapters/contract-to-middleware'; -import { enforceExecutionEnvelope } from './middleware/forwarder'; +import { adaptRoutingDecision } from './adapters/contract-to-middleware.js'; +import { enforceExecutionEnvelope } from './middleware/forwarder.js'; const UNSAFE_OBJECT_KEYS = new Set(['__proto__', 'prototype', 'constructor']); From 003e9acad07daca750b96a889d043ad6bb34b981 Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Fri, 21 Aug 2026 02:11:06 +0000 Subject: [PATCH 8/9] fix: align ESM tests with canonical contracts --- jest.config.cjs | 1 + src/adapters/contract-to-middleware.ts | 2 ++ tests/contract-parity.test.ts | 6 +++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/jest.config.cjs b/jest.config.cjs index 816ca29..784519b 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -11,6 +11,7 @@ module.exports = { '^.+\\.(ts|js)$': ['ts-jest', { tsconfig: '/tsconfig.test.json', useESM: true }], }, moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', '^@bodanglin/verdict-contracts$': '/node_modules/@bodanglin/verdict-contracts', }, transformIgnorePatterns: ['/node_modules/(?!@bodanglin/verdict-contracts)'], diff --git a/src/adapters/contract-to-middleware.ts b/src/adapters/contract-to-middleware.ts index b69b8fe..eaa1264 100644 --- a/src/adapters/contract-to-middleware.ts +++ b/src/adapters/contract-to-middleware.ts @@ -152,6 +152,8 @@ export function createFallbackRoutingDecision( request_id: null, policy_version: '1', schema_version: '1', + decision_id: null, + receipt: null, }; } diff --git a/tests/contract-parity.test.ts b/tests/contract-parity.test.ts index ce61521..def8304 100644 --- a/tests/contract-parity.test.ts +++ b/tests/contract-parity.test.ts @@ -7,6 +7,7 @@ import { } from '../src/middleware/forwarder'; import * as fs from 'fs'; import * as path from 'path'; +import { fileURLToPath } from 'node:url'; describe('canonical routing contract parity', () => { it('creates fallback decisions accepted by the canonical schema', () => { @@ -158,7 +159,10 @@ describe('canonical routing contract parity', () => { }); describe('ExecutionEnvelope schema parity — invalid fixtures rejected by both Python and TypeScript', () => { - const fixturesDir = path.resolve(__dirname, '../test_fixtures/envelopes'); + const fixturesDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../test_fixtures/envelopes' + ); function loadFixture(name: string): unknown { const filePath = path.join(fixturesDir, name); From df4b67e6e8f4fadf6354eaf7072bbb3fe489e511 Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Fri, 21 Aug 2026 02:13:33 +0000 Subject: [PATCH 9/9] fix: refresh canonical compat manifest --- .verdict/compat-manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.verdict/compat-manifest.json b/.verdict/compat-manifest.json index d47bc9b..d00f369 100644 --- a/.verdict/compat-manifest.json +++ b/.verdict/compat-manifest.json @@ -2,12 +2,12 @@ "contracts": { "AvailabilitySnapshot": "sha256:9fdcdbe78f23e583001ed8f2292ce5668061f7dbc5d839f3b2536f92630e2b0e", "OutcomeEvent": "sha256:e993fb10752ee36e1a77fe38c85345f77f5975a93eaad78ccb0560004d64013f", - "RoutingDecisionContract": "sha256:d893d5c55f520733bc6b117efa050a188f7450fb045aa386370687c429d8edfc", + "RoutingDecisionContract": "sha256:c1bd1b4ff2503e59c74737a85c7c6c470592f36d7b999b4c5d67a39d06f79892", "RuntimeCandidate": "sha256:8fc6ab26992374dc8642df68219b3a68a912a5087b61547c1be2032746f4f8cb", "SwarmTaskEnvelope": "sha256:d5247469d33ea404acd5b6c676329d0ef25fbbe0f2831bdf633b0a843cc90293", "TaskSpec": "sha256:161837be44785ac6057373eac32d1c0c2ba53b1b7e706f57e235c5e0f75c56d2", "WorkflowPlan": "sha256:f1f18b28116f1ec4b833c8fc580d5b1935a3ecc689a0b2f48b5ba3cbad5e00c4" }, - "manifest_hash": "sha256:a608860555863e94af19efb28568d7d3abac87ea0c88c79bf5464f9e8346ef43", + "manifest_hash": "sha256:7bbd4bf9b833b45116a3baa9af0f2d3e8c5fced6ca3ad2a26c315235bd93b0fa", "schema_version": "1" }