diff --git a/.github/workflows/cint-r0-verification.yml b/.github/workflows/cint-r0-verification.yml index 742e443..991d7ae 100644 --- a/.github/workflows/cint-r0-verification.yml +++ b/.github/workflows/cint-r0-verification.yml @@ -29,7 +29,7 @@ jobs: - macos-26 - windows-2025 node: - - "20" + - "22" - "24" - "26" @@ -71,3 +71,20 @@ jobs: echo "Verification matrix result: $MATRIX_RESULT" >&2 exit 1 fi + + typescript-verification: + name: CINT-R1 TypeScript verification + if: ${{ always() }} + needs: + - verification-matrix + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require every TypeScript verification lane + env: + MATRIX_RESULT: ${{ needs.verification-matrix.result }} + run: | + if [ "$MATRIX_RESULT" != "success" ]; then + echo "Verification matrix result: $MATRIX_RESULT" >&2 + exit 1 + fi diff --git a/.gitignore b/.gitignore index 6c144b0..1efb2f8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,8 @@ artifacts/generated/ artifacts/runs/ artifacts/live*/ node_modules/ +dist/ +.test-dist/ +*.tsbuildinfo *.log .DS_Store diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa5c46..3021ace 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## v0.2.0-cint-r1 — unreleased candidate + +- Adopted Node.js 24 as the normative baseline, retained Node.js 22 as a + temporary compatibility lane, and added Node.js 26 as a forward-compatibility + lane. Node.js 20 is historical only. +- Migrated the CINT control-plane production source and CLI to strict + TypeScript while preserving the legacy Agent Floor JavaScript adapter. +- Preserved all 13 JSON Schemas, R0 protocols, error codes, canonical digests, + HMAC vectors, trusted-time checks, one-shot semantics, rollback, and AF-G0 + behavior. +- Added branded authority contracts, exhaustive state unions, compile-time + negative cases, escape-hatch and import-boundary scans, portable builds, + declarations, and package-export verification. +- Passed 18 deterministic behavioral-equivalence scenarios against + `v0.1.0-cint-r0` with no normalized behavioral drift. +- Added no Rust, WASM, Three.js, browser, web-framework, UI, runtime TypeScript, + or publication dependency. +- Kept package version `0.1.0-cint-r0`, package state private, and all release, + tag, merge, npm-publication, and deployment actions outside this candidate. + ## v0.1.0-cint-r0 — 2026-08-28 - Made SI1 CINT and `sntllgnc/CINT` the active repository and default-source diff --git a/CINT_EVALUATION.md b/CINT_EVALUATION.md index 5f303ab..42c262c 100644 --- a/CINT_EVALUATION.md +++ b/CINT_EVALUATION.md @@ -25,8 +25,9 @@ Require all of the following: - `artifacts/cint-r0/gate-ledger.json` contains one ordered pass per completed gate; - Git status is clean after verification; -- the `CINT-R0 remote verification` check passes for Node.js 20, 24, and 26 on - Linux, macOS, and Windows. +- the `CINT-R0 remote verification` and `CINT-R1 TypeScript verification` + checks pass over the same Node.js 22, 24, and 26 matrix on Linux, macOS, and + Windows. Review the proof matrix in `docs/cint-r0/05_SYNTHETIC_ACTION_PROOF.md`, the adapter boundary in `docs/cint-r0/06_CODEX_ADAPTER.md`, and the active threat diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d6e4ef7..bcf438e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,9 @@ ## Development setup -SI1 CINT requires Node.js 20 or newer and has no runtime package dependencies. +SI1 CINT uses the latest security-patched Node.js 24 release as its normative +development baseline. Node.js 22 is a temporary compatibility lane and Node.js +26 is a forward-compatibility lane. ```sh npm ci diff --git a/README.md b/README.md index 44206be..2c5a04f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,40 @@ request -> evidence seal or rollback ``` +## R1 candidate state + +The `cint-r1-typescript` branch is a review candidate that migrates the CINT +control plane to strict TypeScript while preserving the R0 runtime contract. +TypeScript makes authority-state and adapter-boundary distinctions visible to +the compiler; it does not become runtime authority. All untrusted records still +enter as `unknown` and must pass the unchanged JSON Schema/AJV, exact-key, +canonical-byte, digest, HMAC, revalidation, one-shot, trusted-time, outcome, +and seal controls. + +The package remains private and versioned `0.1.0-cint-r0`. R1 grants no merge, +tag, release, npm-publication, or production-deployment authority. + +The following table records the C2 acceptance snapshot from 2026-08-28: + +| Field | Verified value | +|---|---| +| Branch | `cint-r1-typescript` | +| Verified C2 implementation head | `97dac5e80609ba6522f15bb5ecc0a4c0aa5ef022` | +| Required parent | `42465b0192d167f419e782674fd4675c4049e4d9` | +| Pull request at C2 acceptance | [#2](https://github.com/sntllgnc/CINT/pull/2); it was open and draft at C2 acceptance | +| Qualifying C2 workflow | [33175315187](https://github.com/sntllgnc/CINT/actions/runs/33175315187); nine lanes plus both aggregates passed | +| Terminal | `READY-FOR-CINT-R1-TYPESCRIPT-REVIEW` | + +Live head, workflow, and review-readiness state are maintained on +[PR #2](https://github.com/sntllgnc/CINT/pull/2) and its GitHub checks. + +The C2 correction changes only package-verification process launch. It prefers +the active Node executable plus npm's CLI path, uses bounded platform fallbacks +when that metadata is absent, and treats spawn errors or a missing exit status +as terminal verification failures. This is build assurance, not CINT execution +authority: it cannot decide, issue or consume a receipt, invoke an action +adapter, or seal an outcome. + ## R0 state `CINT-R0` is the public source and verification baseline for the SI1 CINT @@ -50,10 +84,11 @@ v0.1.0-cint-r0 Remote verification: CINT-R0 remote verification -Remote verification is enforced by a pinned GitHub Actions matrix covering the -declared Node.js 20 compatibility boundary, Node.js 24 LTS, current Node.js 26, -and Linux, macOS, and Windows. The stable aggregate gate is -`CINT-R0 remote verification`. +Remote verification is enforced by a pinned GitHub Actions matrix covering +temporary Node.js 22 compatibility, the normative Node.js 24 LTS baseline, +forward Node.js 26 compatibility, and Linux, macOS, and Windows. The stable +aggregate gates are `CINT-R0 remote verification` and +`CINT-R1 TypeScript verification`; both depend on the same complete matrix. The R0 proof establishes: @@ -82,11 +117,13 @@ The R0 proof establishes: ## Verify locally +Use the latest security-patched Node.js 24 release for normative development. +Node.js 22 and 26 are tested compatibility lanes, not the development baseline. + ```sh npm ci -npm test -npm run cint:identity npm run verify +npm run cint:identity ``` The historical `npm run demo` remains available as the AF-G0 Adapter 01 @@ -95,14 +132,19 @@ regression. It is not the CINT product definition. ## Repository map ```text -src/cint/ adapter-independent CINT core and execution boundary -src/cint/adapters/ explicit, separately imported R0 action adapters +src/cint/**/*.ts strict TypeScript CINT control plane +src/cint/adapters/ explicit, separately imported typed action adapters src/adapters/codex-delegation/ preserved Agent Floor compatibility kernel schemas/cint/ strict authority-bearing JSON schemas -tests/cint-*.test.js CINT conformance and end-to-end proofs +tests/cint-*.test.ts six strictly typed CINT conformance suites +tests/npm-pack-launch.test.mjs deterministic cross-platform package-launch proof +tests/types/ compile-time positive and negative contracts +dist/ ignored generated JavaScript and declarations docs/cint-r0/ gate evidence and public R0 release notes +docs/cint-r1/ R1 gate maps and review evidence docs/archive/ historical Agent Floor and competition material artifacts/cint-r0/ gate ledger and sanitized gate receipts +artifacts/cint-r1/ R1 baseline and behavioral-equivalence records ``` ## Design and review @@ -112,9 +154,13 @@ artifacts/cint-r0/ gate ledger and sanitized gate receipts - [Threat model](docs/THREAT-MODEL.md) - [Privacy](docs/PRIVACY.md) - [Limitations](docs/LIMITATIONS.md) +- [Runtime support](docs/RUNTIME-SUPPORT.md) +- [TypeScript R1 boundary](docs/TYPESCRIPT-R1.md) +- [R1 C2 state, logic, and architecture](docs/cint-r1/10_CORRECTION_C2.md) +- [Framework boundary](docs/FRAMEWORK-BOUNDARY.md) - [Codex Adapter 01](docs/CODEX-ADAPTER.md) - [CINT-R0 gate ledger](artifacts/cint-r0/gate-ledger.json) -- [Public R0 source release](docs/RELEASE.md) +- [Historical R0 source release](docs/RELEASE.md) - [Public release notes](docs/cint-r0/PUBLIC-RELEASE-NOTES.md) The immutable Agent Floor baseline remains the tagged release diff --git a/SECURITY.md b/SECURITY.md index d509cfa..cb90cf7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,9 +2,19 @@ ## Supported line -Security corrections for the public CINT-R0 source prerelease are developed -through protected changes to `main`. The immutable Agent Floor release remains -historical evidence. +The public CINT-R0 source prerelease remains the released line. The R1 +TypeScript branch is a review candidate, not a release or production baseline. +Security corrections enter `main` only through separately authorized protected +changes. The immutable Agent Floor release remains historical evidence. + +The active security baseline is the latest security-patched Node.js 24 release. +Node.js 22 is retained temporarily for compatibility testing, Node.js 26 is a +forward-compatibility lane, and Node.js 20 is historical and unsupported. + +Strict TypeScript compilation is a development control only. Runtime security +continues to depend on the unchanged JSON Schema/AJV, exact-key, canonical +digest, HMAC, revalidation, one-shot, trusted-time, outcome, rollback, and seal +checks. ## Report a vulnerability diff --git a/artifacts/cint-r1/behavioral-equivalence.json b/artifacts/cint-r1/behavioral-equivalence.json new file mode 100644 index 0000000..a62aaa5 --- /dev/null +++ b/artifacts/cint-r1/behavioral-equivalence.json @@ -0,0 +1,106 @@ +{ + "protocol": "cint-r1/behavioral-equivalence/1", + "baseline_release": "v0.1.0-cint-r0", + "baseline_commit": "acc857b5d6fadd363816ad9dbf2fdc710a35ba15", + "candidate_branch": "cint-r1-typescript", + "candidate_preseal_head": "026e0b8719d061df1becafd005d5b1c8dc75b4b5", + "normalization": { + "excluded": [ + "generated UUIDs", + "temporary absolute paths", + "runtime-generated timestamps" + ], + "retained": [ + "statuses", + "error codes", + "canonical digests", + "HMAC signature", + "store states", + "target hashes", + "AF-G0 counters" + ] + }, + "scenario_count": 18, + "passed": 18, + "failed": 0, + "scenarios": [ + { + "id": "ADMIT", + "normalized_sha256": "29b71839b32817a567922ee25329013f6d02faee4ab44e117d67546ba5cf4ab6" + }, + { + "id": "DENY", + "normalized_sha256": "27bdd775d96a430c1d34223b6701c3bd7ab154b99f860fa986b9460a4acfb444" + }, + { + "id": "REVIEW", + "normalized_sha256": "ce3ef7d283d69a9ab7293055a4d10f90fa8ab97e65a94226d10b35b52c2a8c9e" + }, + { + "id": "RECEIPT_ISSUE_AND_VERIFICATION", + "normalized_sha256": "7e3a957bde2341e01a0c8622f02516da5c77320bfb006fb12f04d56b8a43d64f" + }, + { + "id": "ONE_SHOT_CONSUMPTION_RACE", + "normalized_sha256": "660cd04e3156c21f7be552635e09c88e8bcff31cacfcd82a560b678477b326a7" + }, + { + "id": "REPLAY_REJECTION", + "normalized_sha256": "e29cd66b63ccd882f10ac350e2a2e16691f0b70ca883dae9ebb31a0011d1ca87" + }, + { + "id": "EXPIRY_DURING_PREPARATION", + "normalized_sha256": "abd3497076357f9c9f5688e55c47bdf37029030715c059d0eb492b951c7da67f" + }, + { + "id": "POLICY_DRIFT", + "normalized_sha256": "b6d7d36693901f1999618f2999ee5b64a8d640f9c5f7371ce9469b1deef70898" + }, + { + "id": "AUTHORITY_DRIFT", + "normalized_sha256": "96e4a1abb244eed410855f60aa1378a36b2c15e8e215e3c862a2b4ca4a0c97f2" + }, + { + "id": "MACHINE_STATE_DRIFT", + "normalized_sha256": "a3da547603cc74454f9b829c844ea218364c948c11801db8d30d0bc9cc584207" + }, + { + "id": "ADAPTER_CAPABILITY_DRIFT", + "normalized_sha256": "29570fce9a942018db6417b34367feeca71bac1444d9a837eaa9c0f5e253a861" + }, + { + "id": "ALIEN_PROTOCOL_REJECTION", + "normalized_sha256": "2c707f6c4f0138bf43cace7f069a3917b9df221e330008f3a3b0f9d1bcfe938a" + }, + { + "id": "FORBIDDEN_FIELD_REJECTION", + "normalized_sha256": "cd7eb0a2d83159cdd2642683368aa317be9201bf318df556dd5ac76b294d23a7" + }, + { + "id": "OUTCOME_VERIFICATION", + "normalized_sha256": "8b0737e980795f7fc934b460817ece522dae5e3aca7d0b09deefd91e4e9a3180" + }, + { + "id": "ROLLBACK_AND_RESTORED_HASH", + "normalized_sha256": "b1fd54774d7a38d9eab5f1af50f9e14fb0522926456afc060120346daed38d49" + }, + { + "id": "FAIL_CLOSED_DEPENDENCY_CHECKS", + "normalized_sha256": "3319667da83ddc9768ec40fae5be87d0fccad965b9e84a60adc7b396abab5b14" + }, + { + "id": "ADAPTER_01_EVIDENCE_ADMISSION", + "normalized_sha256": "87dcb291297178aa1571339208e711d5bb1b4005ae0c49c87784a6a0177083df" + }, + { + "id": "AF_G0_TELEMETRY_REGRESSION", + "normalized_sha256": "dbd3e6664d7aa97f6f80b3748ddc4464c5d6012614cc2a388f4ba698c1d60ca8" + } + ], + "frozen_surfaces": { + "runtime_schema_files_identical": "13/13", + "protocol_identifier_set": "24/24 identical", + "error_code_set": "103/103 identical" + }, + "verdict": "PASS" +} diff --git a/artifacts/cint-r1/independent-verdict.json b/artifacts/cint-r1/independent-verdict.json new file mode 100644 index 0000000..fcb4470 --- /dev/null +++ b/artifacts/cint-r1/independent-verdict.json @@ -0,0 +1,59 @@ +{ + "protocol": "cint-r1/independent-verdict/1", + "reviewed_at": "2026-08-28T12:40:43Z", + "baseline_commit": "acc857b5d6fadd363816ad9dbf2fdc710a35ba15", + "candidate_pre_review_head": "b549414c4c88682c5e1b56e3a29a8723bfbdb416", + "reviewer_count": 2, + "reviewers": [ + { + "id": "C", + "scope": "authority-and-runtime-boundary", + "context_mode": "clean", + "fork_turns": "none", + "model": "gpt-5.6-terra", + "reasoning_effort": "medium", + "child_agents": 0, + "tracked_mutations": 0, + "finding_count": 0, + "verdict": "ACCEPT-CINT-R1-TYPESCRIPT-CANDIDATE" + }, + { + "id": "D", + "scope": "build-package-node-and-legacy", + "context_mode": "clean", + "fork_turns": "none", + "model": "gpt-5.6-terra", + "reasoning_effort": "medium", + "child_agents": 0, + "tracked_mutations": 0, + "finding_count": 0, + "verdict": "ACCEPT-CINT-R1-TYPESCRIPT-CANDIDATE" + } + ], + "root_attestation": { + "ordered_verification": "PASS", + "node_24_runtime": "24.20.0", + "compiled_cint_tests": "56/56", + "legacy_tests": "16/16", + "total_runtime_tests": "72/72", + "compile_time_negative_contracts": "14/14", + "escape_hatch_findings": 0, + "import_boundary_findings": 0, + "runtime_schemas_identical": "13/13", + "protocol_identifiers_identical": "24/24", + "error_codes_identical": "103/103", + "behavioral_equivalence": "18/18 PASS", + "af_g0_evidence": "53/53", + "dependency_vulnerabilities": 0, + "publication_findings": 0, + "runtime_exports_resolved": "4/4", + "declaration_exports_resolved": "4/4", + "private_source_map_paths": 0, + "typescript_source_files": 29, + "javascript_files_under_src_cint": 0, + "tracked_generated_files": 0, + "prohibited_dependency_count": 0 + }, + "findings": [], + "verdict": "ACCEPT-CINT-R1-TYPESCRIPT-CANDIDATE" +} diff --git a/artifacts/cint-r1/r0-baseline.json b/artifacts/cint-r1/r0-baseline.json new file mode 100644 index 0000000..c2db5e6 --- /dev/null +++ b/artifacts/cint-r1/r0-baseline.json @@ -0,0 +1,77 @@ +{ + "protocol": "cint-r1/r0-baseline/1", + "recorded_at": "2026-08-28T11:38:52Z", + "task_id": "CINT-R1-TYPESCRIPT-CONTROL-PLANE", + "verdict": "PASS-CINT-R1-G0", + "c3_receipt": { + "filename": "CINT-R0-PUBLIC-CUTOVER-C3.json", + "bytes": 8551, + "sha256": "de0e948d9650f0b1f1d4d43910950cb4d428991c407244f7386a1679a442fc63", + "sidecar_verified": true, + "json_valid": true, + "r1_precondition": "SATISFIED", + "verdict": "CINT-R0-PUBLIC-CUTOVER-PASS" + }, + "repository": { + "full_name": "sntllgnc/CINT", + "visibility": "public", + "default_branch": "main", + "origin": "https://github.com/sntllgnc/CINT.git", + "base_commit": "acc857b5d6fadd363816ad9dbf2fdc710a35ba15", + "base_tree": "e156a3a150152961ff3aa027ceb69b9f97f3c780", + "source_worktree_clean": true, + "r1_branch": "cint-r1-typescript" + }, + "tags": { + "v0.1.0-cint-r0": { + "type": "annotated", + "commit": "acc857b5d6fadd363816ad9dbf2fdc710a35ba15" + }, + "v0.1.0-af-g0": { + "type": "annotated", + "commit": "d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49" + } + }, + "remote_verification": { + "workflow": "CINT R0 verification", + "run_id": 33131135677, + "head_sha": "acc857b5d6fadd363816ad9dbf2fdc710a35ba15", + "status": "completed", + "conclusion": "success" + }, + "local_r0_verification": { + "node": "v25.9.0", + "npm": "11.12.1", + "dependency_install": "PASS", + "tests": { + "total": 72, + "passed": 72, + "failed": 0 + }, + "af_g0_demo": "PASS", + "af_g0_evidence": { + "verified": 53, + "total": 53 + }, + "schemas": { + "verified": 13, + "total": 13 + }, + "dependency_vulnerabilities": 0, + "publication_findings": 0 + }, + "non_actions": { + "main_merge": 0, + "main_direct_update": 0, + "push": 0, + "tag_creation": 0, + "github_release": 0, + "npm_publication": 0, + "schema_change": 0, + "rust_code": 0, + "wasm_code": 0, + "threejs_code": 0, + "web_ui": 0, + "production_deployment": 0 + } +} diff --git a/artifacts/cint-r1/schema-sha256.json b/artifacts/cint-r1/schema-sha256.json new file mode 100644 index 0000000..f51e286 --- /dev/null +++ b/artifacts/cint-r1/schema-sha256.json @@ -0,0 +1,21 @@ +{ + "protocol": "cint-r1/schema-sha256/1", + "algorithm": "sha256", + "source_commit": "acc857b5d6fadd363816ad9dbf2fdc710a35ba15", + "count": 13, + "schemas": { + "schemas/cint/adapter-capability.schema.json": "8b2ae648726c9e17a4f3f8f5e71884afe3e52aadd81c2fce58d7b1077aace6b2", + "schemas/cint/authority.schema.json": "c7d01456cf80960cab8151e203335425f4892272644269333cf4c9d893f983e7", + "schemas/cint/challenge.schema.json": "2fb88e17a5e2c3bdbb67fcb1e7b57ea3b0337ca4c352fa1ae269c8fb1c67bd23", + "schemas/cint/decision.schema.json": "f32ada548ed0ccf413c91fb9c515e04f748f60fac486582c596504ca9d63a069", + "schemas/cint/execution-result.schema.json": "2e600707e9446da76498e90b81f2233ccc0d445f58cf155b697d2e1c5211ae3b", + "schemas/cint/intent.schema.json": "7aaea767fd5ea7268a06f66b8f1d31c9079f2ed65b92ff019c1d621c36a0380a", + "schemas/cint/machine-state.schema.json": "bafa5f607873e5c619cfd204bf3503cd7f992eee67aebf8bad5c7fb02625b15c", + "schemas/cint/outcome.schema.json": "c00d8fa48a6456d0dcfa6c096620d04493a91cc312e3339124652e932344a4c9", + "schemas/cint/policy.schema.json": "d3fd4df0e511298891ea2b675ef8e1f50e0eb4625c62807116ace96cc3e7b83f", + "schemas/cint/principal.schema.json": "cbf04d18fed155928c5ce1fdbe6b985fc3f9dd66089771d16d3da3d961a479cb", + "schemas/cint/receipt.schema.json": "ee8318a2e890c2db4471a289232762120c6b56e3f4b13aafb9f7b064ca9402bd", + "schemas/cint/revalidation.schema.json": "a42281c970621b734903c2a8b79bfd7f33cdc8fe708223be2b0985316ed3c72f", + "schemas/cint/seal.schema.json": "3c7912c93795da8a36d6539683e602d254ce5369cfd1d18ab2beea0acadaa1fc" + } +} diff --git a/bin/cint.js b/bin/cint.js deleted file mode 100755 index 389c5f7..0000000 --- a/bin/cint.js +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env node -import { main } from "../src/cint/cli.js"; - -main().catch((error) => { - process.stderr.write(`${JSON.stringify({ - protocol: "cint/error/1", - code: error?.code ?? "CINT_INTERNAL_ERROR", - message: error?.message ?? String(error) - })}\n`); - process.exitCode = 1; -}); diff --git a/bin/cint.ts b/bin/cint.ts new file mode 100755 index 0000000..146f5c8 --- /dev/null +++ b/bin/cint.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env node +import { main } from "../src/cint/cli.js"; + +function property(value: unknown, name: string): unknown { + return value !== null && (typeof value === "object" || typeof value === "function") + ? Reflect.get(value, name) + : undefined; +} + +main().catch((error) => { + process.stderr.write(`${JSON.stringify({ + protocol: "cint/error/1", + code: property(error, "code") ?? "CINT_INTERNAL_ERROR", + message: property(error, "message") ?? String(error) + })}\n`); + process.exitCode = 1; +}); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index eeea9b0..e6476d8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -8,6 +8,32 @@ CINT separates interpretation, authority, execution, and evidence. A request is not authority. An `ADMIT` decision is not authority. A signed receipt becomes executable only after immediate revalidation and atomic one-shot consumption. +## R1 source and runtime boundary + +R1 expresses the CINT control plane in strict TypeScript and emits JavaScript, +declarations, and source maps into ignored `dist/` output. TypeScript types are +erased during compilation. They make invalid state substitutions fail during +development, but they do not validate a runtime value or confer execution +authority. + +Every external record therefore begins as `unknown` and crosses the same R0 +runtime controls: exact keys, one of the 13 unchanged JSON Schemas, canonical +JSON, content digests, protocol checks, and the applicable HMAC and gate +verification. Runtime schemas remain authoritative. + +## Two assurance planes + +CINT keeps runtime authority and build assurance mechanically separate: + +| Plane | Establishes | Does not establish | +|---|---|---| +| Runtime authority | Current decision bindings, signed receipt eligibility, one-shot consumption, execution admission, verified outcome, rollback, and evidence seal | Package portability, publication, deployment, or host-wide enforcement | +| Build assurance | Strict compilation, test execution, schema packaging, package inventory, export and declaration resolution, source-map hygiene, and supported-platform verification | A decision, receipt, consumed authority, adapter invocation, or seal | + +The package verifier can fail a candidate build. It cannot authorize a CINT +action. Conversely, a valid CINT receipt cannot bypass package, CI, or release +policy. + ## Control planes | Plane | Responsibility | Cannot do | @@ -66,6 +92,28 @@ rollback function, ledger, or seal issuer/verifier is unavailable. If failure occurs after a synthetic action begins, the gate invokes rollback and verifies the restored hash before sealing the restored outcome. +## Package-verification boundary + +Package verification runs the immutable argument vector +`npm pack --dry-run --json --ignore-scripts`. Launch selection is ordered and +fail closed: + +1. When `npm_execpath` exists, the verifier invokes it through + `process.execPath` without a shell. +2. On Windows without npm CLI metadata, it invokes the fixed command through + `ComSpec` or `cmd.exe` with `/d /s /c`; no untrusted value enters the command + text. +3. On POSIX without npm CLI metadata, it invokes `npm` directly without a + shell. +4. A spawn error, `status: null`, non-zero status, malformed JSON, unexpected + report cardinality, or missing file inventory terminates verification. +5. Only a successful report proceeds to package-target, runtime-export, + declaration-export, source-map, and private-package assertions. + +This boundary is implemented in `scripts/npm-pack-launch.mjs` and consumed by +`scripts/verify-package.mjs`. Its deterministic regression is independent of +the six compiled CINT test suites. + ## Adapter boundary Adapters implement preparation, execution, outcome verification, and optional @@ -75,10 +123,11 @@ during preparation. Adapters receive no receipt or seal key. They cannot resolve principals, change authority, issue decisions, consume receipts, or seal outcomes through the adapter interface. -The supported core entrypoint, `src/cint/index.js`, imports no action adapter or -legacy Agent Floor CLI. Adapters are available only through explicit subpath -entrypoints. The `cint legacy` command loads Adapter 01 lazily after explicit -invocation. +The TypeScript core entrypoint, `src/cint/index.ts`, imports no action adapter or +legacy Agent Floor CLI. Package consumers resolve compiled JavaScript and +declarations under `dist/`. Adapters are available only through explicit +subpath entrypoints. The `cint legacy` command loads Adapter 01 lazily through +the typed boundary after explicit invocation. The Codex delegation adapter binds the preserved Agent Floor packet and task digests, then executes the existing clean, bounded, read-only runner. Its @@ -101,3 +150,15 @@ under `src/adapters/codex-delegation/`. Root module re-exports and the `agent-floor` CLI retain the frozen API and AF-G0 regression semantics. The historical tag and evidence manifest remain the source of immutable release identity. + +## C2 acceptance snapshot — 2026-08-28 + +At C2 acceptance, the reviewed candidate head was +`97dac5e80609ba6522f15bb5ecc0a4c0aa5ef022`. Workflow `33175315187` had passed +Linux, macOS, and Windows on Node.js 22, 24, and 26, plus both aggregate checks. +The branch was ready for TypeScript review, and PR #2 was open and draft at C2 +acceptance. `main` remained the R0 baseline, and no merge, release, package +publication, framework migration, or deployment followed from that state. + +Live head, workflow, and review-readiness state are maintained on +[PR #2](https://github.com/sntllgnc/CINT/pull/2) and its GitHub checks. diff --git a/docs/FRAMEWORK-BOUNDARY.md b/docs/FRAMEWORK-BOUNDARY.md new file mode 100644 index 0000000..c57279b --- /dev/null +++ b/docs/FRAMEWORK-BOUNDARY.md @@ -0,0 +1,37 @@ +# CINT framework boundary + +## Trusted runtime + +CINT R1 is a Node.js control plane with strict TypeScript source and emitted +JavaScript runtime. Its trusted paths are authority evaluation, receipt +authentication and one-shot consumption, revalidation, execution gating, +outcome verification, rollback, evidence, and sealing. + +Three.js is absent and irrelevant to these paths. The official +[Three.js fundamentals](https://threejs.org/manual/en/fundamentals.html) +describe a browser/WebGL 3D rendering surface; it supplies no CINT authority, +integrity, validation, or execution primitive. + +## Excluded frameworks and targets + +R1 contains no Three.js, React, React Three Fiber, Next.js, Vite, webpack, +Electron, Tauri, Rust, WASM, `wasm-bindgen`, `napi-rs`, Neon, web server, +browser bundle, canvas, WebGL scene, UI shell, or visualization code. + +A future visualization surface, if separately authorized, must remain outside +authority, receipt, execution, revalidation, and seal paths. It may consume +sanitized terminal evidence; it may not become a control or trust boundary. + +An isolated Rust trusted core is deferred to a later decision and must be +justified by a bounded security or correctness requirement. R1 neither begins +nor implies a Rust rewrite. + +The C2 package-launch correction is process portability inside the existing +Node.js verification plane. It introduces no framework, UI, browser, Rust, +WASM, or deployment surface and changes no CINT runtime-authority path. + +## Non-claims + +The R1 candidate does not establish production readiness, machine-wide +enforcement, hostile-adapter sandboxing, web-product status, deployment +authority, or external-service activation. diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 54d1f00..a9033c1 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -1,4 +1,9 @@ -# CINT-R0 limitations and non-claims +# CINT limitations and non-claims + +- R1 TypeScript types provide compile-time contract visibility only. They are + erased from emitted JavaScript and never replace runtime schema validation, + exact-key rejection, canonical digests, HMAC verification, revalidation, + one-shot consumption, or trusted-time checks. - R0 is a local protocol and proof runtime, not a distributed authorization service. @@ -13,7 +18,7 @@ adapters that touch concurrent external systems need an adapter-specific transaction or conditional-write mechanism. - Adapter capability records require preparation to be side-effect-free, and - the core revalidates after preparation. R0 trusts the selected in-process + the core revalidates after preparation. CINT trusts the selected in-process adapter implementation to honor that contract; it does not sandbox hostile adapter code or prove the absence of unrelated side effects. - The synthetic adapter proves one disposable existing-file mutation. It does @@ -29,6 +34,17 @@ - CINT does not establish account billing, provider quota causation, hostile-code containment, operating-system integrity, or protection after receipt/seal key compromise. +- Node.js 24 is the normative runtime major. Node.js 22 is a temporary + compatibility lane, Node.js 26 is a forward-compatibility lane, and Node.js + 20 is historical only. Support for a major still requires its latest + security-patched release. +- The cross-platform `npm pack --dry-run` proof establishes launch portability, + expected package inventory, export/declaration resolution, and source-map + hygiene. It is not an npm publication, clean-install guarantee, deployment + proof, signature-transparency proof, or production-readiness claim. +- R1 contains no Rust, WASM, Three.js, browser, WebGL, web-server, or UI trusted + runtime. An isolated Rust core and any visualization surface require separate + future authority; neither is implied by this migration. - `v0.1.0-cint-r0` is a public source prerelease. It does not authorize npm publication, production deployment, third-party operational reliance, machine-wide enforcement, or physical autonomous action. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 796f7fe..779f077 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -32,7 +32,7 @@ Every protected `main` update must pass `CINT-R0 remote verification`. The aggregate gate covers the complete verification command and the 13-schema package check -on Linux, macOS, and Windows across Node.js 20, 24, and 26. +on Linux, macOS, and Windows across Node.js 22, 24, and 26. ## Release boundary @@ -44,3 +44,17 @@ release remain unchanged. The complete technical limitations and non-claims are maintained in [`docs/LIMITATIONS.md`](LIMITATIONS.md). + +## R1 successor candidate + +The R1 TypeScript candidate is not part of the R0 release. At C2 acceptance on +2026-08-28, its verified implementation head was +`97dac5e80609ba6522f15bb5ecc0a4c0aa5ef022`, and workflow +[33175315187](https://github.com/sntllgnc/CINT/actions/runs/33175315187) had passed +all nine platform/runtime lanes and both aggregate checks. PR #2 was open and +draft at C2 acceptance. That snapshot established +`READY-FOR-CINT-R1-TYPESCRIPT-REVIEW`; it did not alter `main`, either release +tag, the prerelease, npm, or deployment state. + +Live head, workflow, and review-readiness state are maintained on +[PR #2](https://github.com/sntllgnc/CINT/pull/2) and its GitHub checks. diff --git a/docs/RUNTIME-SUPPORT.md b/docs/RUNTIME-SUPPORT.md new file mode 100644 index 0000000..0fda68c --- /dev/null +++ b/docs/RUNTIME-SUPPORT.md @@ -0,0 +1,55 @@ +# Runtime support + +## Normative baseline + +SI1 CINT R1 is developed and operated against the latest security-patched +release in the Node.js 24 LTS major. `.nvmrc` and `.node-version` select major +24; they do not freeze an old patch release. + +## Tested majors + +| Major | Status | Meaning | +|---|---|---| +| Node.js 22 | Temporary compatibility | R1 avoids new behavior unavailable in this lane unless separately accepted. | +| Node.js 24 | Normative LTS baseline | Development and operational reference runtime. | +| Node.js 26 | Forward compatibility | Passing does not make Node 26 normative before later authority. | +| Node.js 20 | Historical only | EOL, rejected by active engines, absent from active CI and security support. | + +The supported/tested major identifies a compatibility lane. Deployments and +development environments must still track the latest security-patched release +within that major. + +The package engine policy is: + +```text +^22.0.0 || ^24.0.0 || ^26.0.0 +``` + +`engine-strict=true` makes unsupported majors a package-install failure. + +The policy follows the official [Node.js release table](https://nodejs.org/en/about/previous-releases). +Historical release notes, frozen evidence, and archived Agent Floor material +retain their original runtime statements as immutable history rather than +active support claims. + +## Portable npm process contract + +Package verification must not depend on direct `.cmd` execution behavior. +When npm exposes `npm_execpath`, CINT launches that CLI with the active Node +executable on every platform. If the metadata is absent, Windows uses a fixed +command-processor invocation and POSIX uses direct `npm`; both fallbacks keep +the package arguments fixed and avoid caller-derived command text. + +The verifier distinguishes four states: + +| Process result | Verification state | +|---|---| +| Spawn error | Fail before status or JSON handling | +| `status: null` | Fail as an indeterminate process result | +| Non-zero status | Fail with npm stderr or the bounded fallback message | +| Zero status and valid one-item JSON report | Continue package-content verification | + +Workflow [33175315187](https://github.com/sntllgnc/CINT/actions/runs/33175315187) +proved this contract across all nine Linux, macOS, and Windows lanes on Node.js +22, 24, and 26. Both stable aggregate checks passed at candidate head +`97dac5e80609ba6522f15bb5ecc0a4c0aa5ef022`. diff --git a/docs/SECURITY-MODEL.md b/docs/SECURITY-MODEL.md index 847eed6..22e4d66 100644 --- a/docs/SECURITY-MODEL.md +++ b/docs/SECURITY-MODEL.md @@ -1,8 +1,17 @@ # CINT security model +## Type-system boundary + +R1 uses strict TypeScript to expose nominal identities, authority states, and +adapter transitions at compile time. TypeScript types are erased and are never +accepted as runtime proof. Untrusted inputs remain `unknown` until exact-key, +JSON Schema/AJV, canonical-record, digest, protocol, and signature checks have +completed. The 13 unchanged JSON Schemas remain the runtime admission +authority. + ## Security properties -CINT-R0 enforces these properties for its implemented local adapters: +CINT enforces these properties for its implemented local adapters: - explicit intent precedes authority evaluation; - principal, authority, policy, adapter, target, action, context, and machine @@ -24,23 +33,23 @@ CINT-R0 enforces these properties for its implemented local adapters: | Boundary | Enforcing component | |---|---| -| Strict JSON, runtime schemas, and record integrity | `src/cint/schema.js` and `src/cint/canonical.js` | -| Exact action scope and time-bound authority | `src/cint/authority.js` | -| Silent request and counter-intent challenge | `src/cint/challenge.js` | -| Zero-authority decision | `src/cint/decision.js` | -| Receipt authentication and lifetime | `src/cint/receipt.js` | -| One-shot state and replay rejection | `src/cint/store.js` | -| Current binding checks | `src/cint/revalidation.js` | -| Fail-closed action boundary | `src/cint/execution.js` | -| Verified/restored terminal outcome | `src/cint/outcome.js` and `src/cint/rollback.js` | -| Hash-chained evidence and terminal authentication | `src/cint/evidence.js` and `src/cint/seal.js` | +| Strict JSON, runtime schemas, and record integrity | `src/cint/schema.ts` and `src/cint/canonical.ts` | +| Exact action scope and time-bound authority | `src/cint/authority.ts` | +| Silent request and counter-intent challenge | `src/cint/challenge.ts` | +| Zero-authority decision | `src/cint/decision.ts` | +| Receipt authentication and lifetime | `src/cint/receipt.ts` | +| One-shot state and replay rejection | `src/cint/store.ts` | +| Current binding checks | `src/cint/revalidation.ts` | +| Fail-closed action boundary | `src/cint/execution.ts` | +| Verified/restored terminal outcome | `src/cint/outcome.ts` and `src/cint/rollback.ts` | +| Hash-chained evidence and terminal authentication | `src/cint/evidence.ts` and `src/cint/seal.ts` | ## Key custody Receipt and seal authorities accept caller-supplied key material of at least 32 bytes and retain it in private class fields. Keys do not enter canonical records, ledger payloads, adapter configuration digests, output artifacts, or errors. -R0 does not persist or distribute keys; the embedding runtime owns generation, +CINT does not persist or distribute keys; the embedding runtime owns generation, storage, rotation, and process isolation. ## Adapter containment @@ -70,3 +79,7 @@ The publication audit continues to reject local absolute paths, raw logs, credential formats, email addresses, session identifiers, hidden metadata, and private project names. The exact public product phrase `SI1 CINT` is the sole new identity exception. + +This source candidate establishes local protocol behavior only. It does not +claim production readiness, machine-wide enforcement, or hostile-adapter +containment. diff --git a/docs/THREAT-MODEL.md b/docs/THREAT-MODEL.md index 9ff2689..932d5da 100644 --- a/docs/THREAT-MODEL.md +++ b/docs/THREAT-MODEL.md @@ -8,20 +8,25 @@ challenges policy and machine state, and returns `ADMIT`, `DENY`, or `REVIEW`. Only `ADMIT` is eligible for a signed one-shot receipt, and only a currently valid consumed receipt can reach an action adapter. +R1 expresses these controls in strict TypeScript, but the emitted runtime is +JavaScript and the types are erased. The compiler is a development control, +not a trust boundary: every external record still requires the unchanged +runtime schema and cryptographic checks before use. + ### Components and source | Component | Security role | Source evidence | |---|---|---| -| Canonical records | Execute all 13 public schemas; reject unknown fields, wrong protocols, and tampered digests | `src/cint/schema.js`, `src/cint/canonical.js` | -| Intent reconstruction | Bind request, action, target, context, effects, and uncertainty | `src/cint/intent.js:33-67` | -| Authority and policy | Enforce exact grants, epochs, validity, allowed adapters, and rollback policy | `src/cint/authority.js:24-110`, `src/cint/policy.js:12-97` | -| Counter-intent and decision | Deny silent or mismatched intent; route uncertainty; keep decisions non-executable | `src/cint/challenge.js:54-127`, `src/cint/decision.js:11-67` | -| Receipt authority | Authenticate eligible decision bindings and lifetime | `src/cint/receipt.js:22-119` | -| Receipt store | Register, lock, revalidate, consume once, and terminally reject | `src/cint/store.js:39-190` | -| Execution gate | Preflight verifiers, revalidate inside the lock, before preparation, and immediately after preparation before execution | `src/cint/execution.js` | -| Outcome, ledger, and seal | Verify or restore state; hash-chain and authenticate terminal evidence | `src/cint/outcome.js:3-31`, `src/cint/evidence.js:16-85`, `src/cint/seal.js:18-110` | -| Synthetic adapter | Confine a disposable patch and restore exact original bytes | `src/cint/adapters/synthetic-file-patch.js:34-125` | -| Codex Adapter 01 | Bind and execute the preserved read-only Agent Floor packet | `src/cint/adapters/codex/index.js:49-128` | +| Canonical records | Execute all 13 public schemas; reject unknown fields, wrong protocols, and tampered digests | `src/cint/schema.ts`, `src/cint/canonical.ts` | +| Intent reconstruction | Bind request, action, target, context, effects, and uncertainty | `src/cint/intent.ts` | +| Authority and policy | Enforce exact grants, epochs, validity, allowed adapters, and rollback policy | `src/cint/authority.ts`, `src/cint/policy.ts` | +| Counter-intent and decision | Deny silent or mismatched intent; route uncertainty; keep decisions non-executable | `src/cint/challenge.ts`, `src/cint/decision.ts` | +| Receipt authority | Authenticate eligible decision bindings and lifetime | `src/cint/receipt.ts` | +| Receipt store | Register, lock, revalidate, consume once, and terminally reject | `src/cint/store.ts` | +| Execution gate | Preflight verifiers, revalidate inside the lock, before preparation, and immediately after preparation before execution | `src/cint/execution.ts` | +| Outcome, ledger, and seal | Verify or restore state; hash-chain and authenticate terminal evidence | `src/cint/outcome.ts`, `src/cint/evidence.ts`, `src/cint/seal.ts` | +| Synthetic adapter | Confine a disposable patch and restore exact original bytes | `src/cint/adapters/synthetic-file-patch.ts` | +| Codex Adapter 01 | Bind and execute the preserved read-only Agent Floor packet through a typed boundary | `src/cint/adapters/codex/index.ts`, `src/cint/adapters/codex/legacy-adapter-boundary.ts` | ```mermaid flowchart LR @@ -45,13 +50,13 @@ flowchart LR | Deployment or workflow | Resource or capability | Configuration and precedence | Safe effective value or location | Readers, writers, or recipients | Enforcing control | Evidence or unknowns | |---|---|---|---|---|---|---| -| Embedded CINT runtime | Receipt key | Constructor input owned by embedding process | Private in-memory field; never serialized | Receipt authority only | Minimum 32 bytes and HMAC-SHA256 | `src/cint/receipt.js:22-39`; external key storage and rotation are caller-owned | -| Embedded CINT runtime | Seal key | Constructor input owned by embedding process | Private in-memory field; never serialized | Seal authority only | Minimum 32 bytes and HMAC-SHA256 | `src/cint/seal.js:18-35`; external key storage and rotation are caller-owned | -| One-shot execution | Receipt state root | Caller supplies store root | Hashed filenames under pending, locks, consumed, rejected | CINT store process | Exclusive creation and terminal records | `src/cint/store.js:39-64`, `src/cint/store.js:107-190` | -| Evidence workflow | Ledger file | Caller supplies file path | Canonical local JSONL with adjacent exclusive lock | CINT execution gate | Digest chain, exclusive append, fsync | `src/cint/evidence.js:16-85`; retention is caller-owned | -| Synthetic proof | Writable target | Adapter root plus receipt-bound relative path | Existing regular file resolved inside disposable root | Synthetic adapter only | Containment, expected pre-hash, atomic write, verify, rollback | `src/cint/adapters/synthetic-file-patch.js:48-125` | +| Embedded CINT runtime | Receipt key | Constructor input owned by embedding process | Private in-memory field; never serialized | Receipt authority only | Minimum 32 bytes and HMAC-SHA256 | `src/cint/receipt.ts`; external key storage and rotation are caller-owned | +| Embedded CINT runtime | Seal key | Constructor input owned by embedding process | Private in-memory field; never serialized | Seal authority only | Minimum 32 bytes and HMAC-SHA256 | `src/cint/seal.ts`; external key storage and rotation are caller-owned | +| One-shot execution | Receipt state root | Caller supplies store root | Hashed filenames under pending, locks, consumed, rejected | CINT store process | Exclusive creation and terminal records | `src/cint/store.ts` | +| Evidence workflow | Ledger file | Caller supplies file path | Canonical local JSONL with adjacent exclusive lock | CINT execution gate | Digest chain, exclusive append, fsync | `src/cint/evidence.ts`; retention is caller-owned | +| Synthetic proof | Writable target | Adapter root plus receipt-bound relative path | Existing regular file resolved inside disposable root | Synthetic adapter only | Containment, expected pre-hash, atomic write, verify, rollback | `src/cint/adapters/synthetic-file-patch.ts` | | Codex Adapter 01 | Source projection | Legacy spec allowlist | Temporary run-local copy | Ephemeral Codex child | Read-only sandbox and clean packet | `src/adapters/codex-delegation/runner.js:198-258`, `src/adapters/codex-delegation/runner.js:376-501` | -| Codex Adapter 01 | Run evidence directory | Adapter constructor input | Caller-selected local output directory | Legacy runner and local operator | Adapter task/packet digest plus CINT receipt | `src/cint/adapters/codex/index.js:49-103`; output retention and disclosure are caller-owned | +| Codex Adapter 01 | Run evidence directory | Adapter constructor input | Caller-selected local output directory | Legacy runner and local operator | Adapter task/packet digest plus CINT receipt | `src/cint/adapters/codex/index.ts`; output retention and disclosure are caller-owned | ## 2. Threat Model, Trust Boundaries, and Assumptions @@ -107,9 +112,11 @@ flowchart LR by a trusted local embedding process with appropriate filesystem permissions. - R0 does not provide distributed consensus, cross-host atomicity, hardware key protection, automatic stale-lock recovery, or hostile-code containment. -- R0 trusts the selected in-process adapter implementation to honor its signed +- CINT trusts the selected in-process adapter implementation to honor its signed side-effect-free preparation contract. The core detects bound-state drift after preparation but does not sandbox arbitrary adapter code. +- Strict TypeScript compilation is trusted to detect declared type violations + during development; it is not trusted to establish runtime input validity. - The Codex adapter is read-only toward its source projection. Its selected evidence directory is intentionally writable and remains sensitive local state. @@ -121,19 +128,19 @@ flowchart LR | Priority | Scenario and capability gain | Prerequisites | Impact | Existing controls | Mitigation | Evidence | |---|---|---|---|---|---|---| -| P0 | Forge or alter a receipt to execute a different action | Attacker obtains a valid receipt but not the key | Unauthorized action or target | Record digest, exact binding digest, HMAC, issuer, status, and expiry checks | Keep keys outside adapters; rotate on suspected exposure | `src/cint/receipt.js:40-119`; forgery test in `tests/cint-receipt.test.js` | -| P0 | Replay one receipt concurrently or after success | Copy of an issued receipt | Duplicate consequential action | Hashed identifier, exclusive lock, consumed/rejected terminal record | Protect store permissions; preserve terminal records through retention window | `src/cint/store.js:66-190`; parallel replay tests | -| P0 | Change action, target, policy, authority, adapter, or machine state after decision | Local mutation between decision and execution | Stale authority becomes action | Revalidation inside the receipt lock, before preparation, and after preparation immediately before execution | Embed policy and state providers with authoritative current snapshots | `src/cint/revalidation.js`, `src/cint/execution.js`; preparation-drift regressions | -| P0 | Mutate synthetic target after receipt consumption | Local writer can change the disposable file | Wrong bytes overwritten | Expected pre-action hash checked during prepare and immediately before atomic write | Use exclusive target ownership where stronger serialization is required | `src/cint/adapters/synthetic-file-patch.js:48-98` | -| P0 | Adapter reports success despite divergent state | Buggy or hostile adapter result | False terminal success | Independent adapter verification; synthetic divergence invokes rollback; core seal requires verified/restored outcome | Add independent verifiers for future consequential adapters | `src/cint/execution.js:203-235`, `src/cint/outcome.js:3-31` | -| P1 | Crash leaves receipt lock behind | Process interruption while holding lock | Local denial of service, not unauthorized execution | Ambiguous lock remains fail closed | Define a separately authorized recovery receipt and operator procedure before production use | `src/cint/store.js:107-190`; crash-lock test | -| P1 | Tamper with ledger or terminal evidence | Local filesystem write access | Audit confusion | Canonical entries, sequence, previous digest, evidence seal over ledger head | Store sealed bundles on append-only or separately protected storage | `src/cint/evidence.js:24-85`, `src/cint/seal.js:37-110` | +| P0 | Forge or alter a receipt to execute a different action | Attacker obtains a valid receipt but not the key | Unauthorized action or target | Record digest, exact binding digest, HMAC, issuer, status, and expiry checks | Keep keys outside adapters; rotate on suspected exposure | `src/cint/receipt.ts`; forgery test in `tests/cint-receipt.test.js` | +| P0 | Replay one receipt concurrently or after success | Copy of an issued receipt | Duplicate consequential action | Hashed identifier, exclusive lock, consumed/rejected terminal record | Protect store permissions; preserve terminal records through retention window | `src/cint/store.ts`; parallel replay tests | +| P0 | Change action, target, policy, authority, adapter, or machine state after decision | Local mutation between decision and execution | Stale authority becomes action | Revalidation inside the receipt lock, before preparation, and after preparation immediately before execution | Embed policy and state providers with authoritative current snapshots | `src/cint/revalidation.ts`, `src/cint/execution.ts`; preparation-drift regressions | +| P0 | Mutate synthetic target after receipt consumption | Local writer can change the disposable file | Wrong bytes overwritten | Expected pre-action hash checked during prepare and immediately before atomic write | Use exclusive target ownership where stronger serialization is required | `src/cint/adapters/synthetic-file-patch.ts` | +| P0 | Adapter reports success despite divergent state | Buggy or hostile adapter result | False terminal success | Independent adapter verification; synthetic divergence invokes rollback; core seal requires verified/restored outcome | Add independent verifiers for future consequential adapters | `src/cint/execution.ts`, `src/cint/outcome.ts` | +| P1 | Crash leaves receipt lock behind | Process interruption while holding lock | Local denial of service, not unauthorized execution | Ambiguous lock remains fail closed | Define a separately authorized recovery receipt and operator procedure before production use | `src/cint/store.ts`; crash-lock test | +| P1 | Tamper with ledger or terminal evidence | Local filesystem write access | Audit confusion | Canonical entries, sequence, previous digest, evidence seal over ledger head | Store sealed bundles on append-only or separately protected storage | `src/cint/evidence.ts`, `src/cint/seal.ts` | | P1 | Malicious allowed source instructs Codex to escape policy | Hostile content inside source allowlist | False model conclusion or attempted tool use | Temporary allowlisted projection, read-only sandbox, disabled fan-out/features, schema and legacy evidence verification, then CINT outcome verification | Use stronger process/container isolation for hostile content | `src/adapters/codex-delegation/runner.js:198-258`, `src/adapters/codex-delegation/runner.js:376-501` | -| P2 | Legacy `ADMITTED` is mistaken for CINT authority by a consumer | Consumer ignores the wrapper contract | Incorrect downstream authorization | Wrapper exposes no authority methods; CINT independently requires decision and receipt | Consumers must use only `cint/execution-result/1` and evidence seals | `src/adapters/codex-delegation/index.js:7-24`, `src/cint/adapters/codex/index.js:49-128` | +| P2 | Legacy `ADMITTED` is mistaken for CINT authority by a consumer | Consumer ignores the wrapper contract | Incorrect downstream authorization | Wrapper exposes no authority methods; CINT independently requires decision and receipt | Consumers must use only `cint/execution-result/1` and evidence seals | `src/adapters/codex-delegation/index.js`, `src/cint/adapters/codex/index.ts` | ### Control-status discipline -| Classification | R0 meaning | Examples | +| Classification | CINT meaning | Examples | |---|---|---| | Mechanically prevented | The runtime blocks the transition before action | Missing verifier preflight, schema/protocol rejection, receipt replay, stale authority or policy at final revalidation | | Detected and contained | The action began, but verification detected divergence and the implemented adapter restored the bounded target | Synthetic outcome divergence or interruption followed by hash-verified rollback | @@ -155,4 +162,5 @@ exposure, durable target impact, and cross-principal authority gain. It falls when the effect is restricted to caller-owned disposable state, an independent control blocks execution, or the scenario requires an already fully compromised trusted host. Confidence depends on verified deployment and key/storage custody; -R0 source tests establish local protocol behavior, not every future embedding. +R1 candidate tests establish local protocol behavior, not every future +embedding, production readiness, or machine-wide enforcement. diff --git a/docs/TYPESCRIPT-R1.md b/docs/TYPESCRIPT-R1.md new file mode 100644 index 0000000..e1ec0f6 --- /dev/null +++ b/docs/TYPESCRIPT-R1.md @@ -0,0 +1,119 @@ +# CINT R1 strict TypeScript boundary + +## Candidate status + +R1 migrates the CINT control plane from JavaScript source to strict TypeScript +source without changing the R0 wire protocols, error codes, canonical bytes, +receipt signatures, execution ordering, or schemas. The package remains private +and versioned `0.1.0-cint-r0` until separate release authority exists. + +The exact development toolchain is: + +```text +typescript 7.0.2 +@types/node 26.4.0 +``` + +Neither package is a runtime dependency. + +At C2 acceptance on 2026-08-28, the verified implementation candidate was +`97dac5e80609ba6522f15bb5ecc0a4c0aa5ef022` on `cint-r1-typescript`. It preserved +the C1 parent `42465b0192d167f419e782674fd4675c4049e4d9` and established the +terminal state `READY-FOR-CINT-R1-TYPESCRIPT-REVIEW`. PR #2 was open and draft +at C2 acceptance; that was a reviewable branch state, not a merge or release +state. + +Live head, workflow, and review-readiness state are maintained on +[PR #2](https://github.com/sntllgnc/CINT/pull/2) and its GitHub checks. + +## Compiler policy + +`tsconfig.base.json` sets ES2022 with NodeNext modules and enables every R1 +safety option: `strict`, `noImplicitAny`, `noUncheckedIndexedAccess`, +`exactOptionalPropertyTypes`, `useUnknownInCatchVariables`, +`noImplicitOverride`, `noImplicitReturns`, `noFallthroughCasesInSwitch`, +`noPropertyAccessFromIndexSignature`, `forceConsistentCasingInFileNames`, +`verbatimModuleSyntax`, `isolatedModules`, `noEmitOnError`, source maps, +declarations, declaration maps, and `skipLibCheck: false`. + +The escape-hatch gate rejects production `any`, `as any`, `@ts-ignore`, +`@ts-nocheck`, unchecked double assertions, and non-null assertions. Deliberate +`@ts-expect-error` cases exist only under `tests/types/` to prove invalid +authority substitutions fail compilation. + +All six `cint-*.test.ts` suites are included in strict typechecking, compiled +into ignored `.test-dist/` JavaScript, and executed by both `test:cint` and the +complete test path. The candidate contains 29 TypeScript and zero JavaScript +production files under `src/cint/`, and six TypeScript and zero JavaScript CINT +test suites. + +## Mechanical distinctions + +Branded identities and digests prevent structural substitution between +principals, authorities, policies, decisions, receipts, actions, contexts, +targets, and outcomes. Discriminated unions separate decision, revalidation, +receipt-store, authority, execution, and outcome states. Exhaustive switches +route impossible states through `assertNever`-equivalent checks. + +The compile-time suite rejects, among other cases: + +- a decision where an issued receipt is required; +- `DENY` or `REVIEW` at receipt issuance; +- an issued receipt as consumed-receipt evidence; +- an outcome digest as an action digest; +- a policy ID as an authority ID; +- unverified adapter output as a verified outcome; +- legacy Agent Floor `ADMITTED` evidence as CINT authority; +- an unknown protocol record used before runtime validation. + +## Runtime authority remains unchanged + +[TypeScript types are erased](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch) +when JavaScript is emitted. They cannot authenticate or validate an untrusted +record. CINT therefore retains: + +- exact protocol and unknown-field rejection; +- all 13 JSON Schema/AJV admission checks; +- canonical JSON and SHA-256 digest verification; +- HMAC receipt and seal verification; +- decision-bound revalidation and atomic one-shot consumption; +- fresh trusted-time checks immediately before execution; +- untrusted adapter-output verification, rollback, and evidence sealing. + +Every exported CINT boundary explicitly types its parameters and return value. +Untrusted inputs enter as `unknown` and narrow only through the corresponding +runtime validator. + +## Build and compatibility surface + +`src/cint/**/*.ts` and `bin/cint.ts` compile to ignored `dist/` JavaScript, +declarations, and source maps. Tests compile to ignored `.test-dist/`. There are +no JavaScript production sources under `src/cint/`. + +The historical Agent Floor kernel remains JavaScript under +`src/adapters/codex-delegation/`, the root compatibility modules, and +`bin/agent-floor.js`. CINT can reach that surface only through +`src/cint/adapters/codex/legacy-adapter-boundary.ts`; the legacy result remains +evidence and receives no decision, receipt, consumption, or seal authority. + +Package exports resolve compiled JavaScript and declarations for the core, +CLI, synthetic adapter, and Codex delegation adapter. `agent-floor` remains the +historical compatibility CLI. + +## Package-launch portability correction + +C2 does not change TypeScript types, emitted runtime semantics, protocols, +schemas, exports, or package identity. It changes how the verification process +starts npm: + +```text +npm_execpath present -> process.execPath + npm CLI path, shell false +metadata absent / win -> fixed cmd.exe invocation, shell false +metadata absent / POSIX -> direct npm invocation, shell false +spawn error or null status -> verification failure before JSON parsing +status 0 + valid report -> continue existing package assertions +``` + +Six deterministic launch regressions cover those branches. The existing 72 +runtime tests, 13 schemas, 18 behavioral-equivalence cases, and 53 historical +evidence entries remain unchanged. diff --git a/docs/cint-r1/00_BASELINE.md b/docs/cint-r1/00_BASELINE.md new file mode 100644 index 0000000..9257d31 --- /dev/null +++ b/docs/cint-r1/00_BASELINE.md @@ -0,0 +1,66 @@ +# CINT R1 G0 — admitted public baseline + +Recorded at `2026-08-28T11:38:52Z`. + +## Authority admission + +- Task: `CINT-R1-TYPESCRIPT-CONTROL-PLANE` +- Repository: `sntllgnc/CINT` +- Public base: `main` +- Accepted base: `acc857b5d6fadd363816ad9dbf2fdc710a35ba15` +- Base tree: `e156a3a150152961ff3aa027ceb69b9f97f3c780` +- R1 branch: `cint-r1-typescript` +- Package version held at `0.1.0-cint-r0` + +The external C3 receipt `CINT-R0-PUBLIC-CUTOVER-C3.json` was admitted only +after its SHA-256 sidecar verified. The receipt is 8,551 bytes, has SHA-256 +`de0e948d9650f0b1f1d4d43910950cb4d428991c407244f7386a1679a442fc63`, +parses as JSON, states `r1_precondition: SATISFIED`, and terminates +`CINT-R0-PUBLIC-CUTOVER-PASS`. + +## Public identity + +Read-only live verification established: + +- public repository and default branch: `sntllgnc/CINT`, `main`; +- live `refs/heads/main`: `acc857b5d6fadd363816ad9dbf2fdc710a35ba15`; +- `v0.1.0-cint-r0^{}`: `acc857b5d6fadd363816ad9dbf2fdc710a35ba15`; +- `v0.1.0-af-g0^{}`: `d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49`; +- both release refs are annotated tags; +- main workflow run `33131135677` completed successfully at the accepted base; +- the source worktree was clean before R1 branch creation. + +## R0 verification before branch creation + +The frozen lockfile was installed before Node runtime checks. On the available +local R0 runtime (`node v25.9.0`, `npm 11.12.1`): + +- `npm ci`: PASS, 0 vulnerabilities; +- `npm run verify`: PASS, 72/72 tests; +- AF-G0 deterministic demo: PASS; +- AF-G0 evidence manifest: 53/53; +- publication audit: PASS, 0 findings; +- dependency audit: 0 vulnerabilities; +- `npm run schema:verify`: PASS, 13/13 schemas. + +This local R0 execution is baseline evidence, not an R1 support claim. R1 +support is separately gated on Node 22, 24, and 26. + +## Frozen boundary + +The 13 authoritative schema hashes are recorded in +`artifacts/cint-r1/schema-sha256.json`. Any byte change requires +`HOLD-CINT-R1-SCHEMA-CHANGE`. + +The upstream runtime-policy basis is the Node.js release table: Node 20 is EOL, +Node 22 and Node 24 are LTS, and Node 26 is Current as of this admission. The +TypeScript basis is its erased compile-time type model; runtime schemas remain +authoritative. Three.js describes itself as a browser-facing 3D/WebGL library +and has no place in the trusted CINT runtime. + +## G0 verdict + +`PASS-CINT-R1-G0` + +No main write, push, tag, release, merge, npm publication, deployment, schema +change, framework migration, Rust, WASM, Three.js, or UI action occurred. diff --git a/docs/cint-r1/01_CONTRADICTION_REGISTER.md b/docs/cint-r1/01_CONTRADICTION_REGISTER.md new file mode 100644 index 0000000..449913a --- /dev/null +++ b/docs/cint-r1/01_CONTRADICTION_REGISTER.md @@ -0,0 +1,20 @@ +# CINT R1 G1 — contradiction and risk register + +Baseline: `acc857b5d6fadd363816ad9dbf2fdc710a35ba15`. + +| ID | Observation | Adjudication | +|---|---|---| +| G1-C01 | Reviewer B recommended separate runtime and package matrices. | Rejected. Both required aggregates must depend on the same complete nine-lane R1 matrix. | +| G1-C02 | The available local baseline runtime is Node 25, which the R1 engine expression intentionally rejects and which upstream marks EOL. | Switch to an installed/current Node 24 patch before adding `engine-strict=true`; all G2+ local gates run on Node 24. | +| G1-C03 | Active R0 text and frozen R0 release notes both mention Node 20. | Update active support claims; preserve `docs/archive/**`, public R0 release notes, frozen evidence, tagged history, and historical changelog facts byte-for-byte. | +| G1-C04 | G3 requires type contracts while the JavaScript runtime remains active. | Compile a type-only R1 layer and negative contracts without redirecting runtime imports or exports until the migration gates. | +| G1-C05 | Strict CINT compilation must consume a retained JavaScript Adapter 01 kernel. | Admit it only through an explicit typed CINT boundary; allow/copy only the enumerated legacy surface with `allowJs: true`, `checkJs: false`. No arbitrary deep legacy imports enter CINT production code. | +| G1-C06 | `npm audit` is network-sensitive, but it is a mandatory verification stage. | Keep it in the exact ordered gate after deterministic tests; report infrastructure failure distinctly and never weaken or omit it. | +| G1-C07 | Dry-run packing can resemble publication work while release authority is absent. | `npm pack --dry-run --json --ignore-scripts` is structure verification only. Package stays private; no registry command is authorized. | +| G1-C08 | TypeScript types are erased and cannot enforce untrusted runtime records. | Preserve all AJV, canonical JSON, digest, HMAC, revalidation, one-shot, trusted-time, outcome, and seal checks as runtime authority. | +| G1-C09 | Three.js is a browser/WebGL 3D library; Rust/WASM/UI are unrelated to this migration. | Keep dependency and source scans that fail if any prohibited framework, Rust, WASM, browser, server, canvas, WebGL, or visualization surface enters CINT. | + +No contradiction requires a HOLD at G1. Any later schema-byte, protocol, error, +canonical-byte, receipt-signature, legacy-behavior, or authority-order drift does. + +G1 verdict: `PASS-CINT-R1-G1`. diff --git a/docs/cint-r1/01_MIGRATION_MAP.md b/docs/cint-r1/01_MIGRATION_MAP.md new file mode 100644 index 0000000..fa1e4c7 --- /dev/null +++ b/docs/cint-r1/01_MIGRATION_MAP.md @@ -0,0 +1,117 @@ +# CINT R1 G1 — TypeScript migration and trust map + +Baseline: `acc857b5d6fadd363816ad9dbf2fdc710a35ba15`. + +Reviewer A ran read-only in a clean, history-free context with +`gpt-5.6-terra`, medium reasoning, no child agents, and the prescribed bounded +scope. Root independently reproduced the cited imports, hashes, status, and +control boundaries before admitting this map. + +## Dependency direction + +The observed CINT graph is acyclic: + +```text +schemas/cint/* -> schema.js +legacy util.js -> canonical.js -> record constructors +schema.js -----^ + +authority.js --\ +policy.js ------> challenge.js -> decision.js +principal.js ---/ | +intent.js ---------------------------+ + +canonical.js -> receipt.js -> store.js +challenge.js -> revalidation.js +outcome.js ----\ +rollback.js ----> execution.js -> evidence.js + seal.js +revalidation.js/ + +core -> synthetic adapter +core -> typed Adapter 01 boundary -> legacy codex-delegation kernel +core -> index.js -> cli.js -> bin/cint.js +``` + +Evidence: + +- `src/cint/schema.js:6-42` loads, checks, and compiles the fixed 13-schema + protocol registry. +- `src/cint/canonical.js:131-175` applies schema validation during sealing and + protocol-specific verification. +- `src/cint/challenge.js:1-11` depends on authority and policy evaluation; + `src/cint/decision.js:1-11` depends on challenge, not the reverse. +- `src/cint/execution.js:3-13` depends on canonical validation, outcome, + revalidation, and rollback. +- `src/cint/index.js:1-19` is the public barrel; it does not initialize adapters. +- `src/cint/cli.js:49-51` loads the legacy CLI only for an explicit `legacy` + command. + +No reverse import may create `canonical <-> schema`, `challenge <-> policy`, or +`execution <-> revalidation` cycles. Adapters remain outside core initialization. + +## Trust boundaries + +| Boundary | Untrusted input | Runtime authority retained | +|---|---|---| +| Canonical JSON | Text and arbitrary values | JSON-only values, canonical-byte equality, exact digests | +| Protocol admission | Caller-supplied records | Exact protocol and one of the 13 AJV schemas | +| Decision | Intent, principal, authority, policy, capability, machine state, time | Exact keys, schema validation, challenge status | +| Receipt | Decision, issuance time, key material | ADMIT-only issuance, HMAC over canonical bytes, expiry | +| Store | Files, pending receipt, concurrent consumers | Exclusive lock, digest binding, one terminal transition | +| Revalidation | Current snapshots and receipt | Fresh schema checks, digest/epoch/status comparison | +| Execution | Injected clock, store, callbacks, adapter, signals | Capability preflight, consumed receipt, fresh final time | +| Adapter | Prepared and executed records | Output remains untrusted until verification | +| Evidence/seal | Ledger files and outcome records | Hash chain, verified outcome, HMAC seal | +| CLI | `argv` and output sink | Fixed commands; legacy load is explicit and lazy | + +The decisive execution ordering is preserved from +`src/cint/execution.js:153-176`, `189-218`, and `231-286`: consume with locked +revalidation, revalidate after consumption, prepare without effect, take a +fresh trusted time, revalidate again, invoke the adapter with no intervening +await, verify its untrusted result, then seal or roll back. + +## Mechanical type distinctions + +R1 will make these nominal rather than interchangeable strings or objects: + +- `CanonicalInstant`, `Sha256Digest`, `ProtocolIdentifier`; +- `PrincipalId`, `AuthorityId`, `PolicyId`, `IntentId`, `DecisionId`, + `ReceiptId`, `AdapterId`; +- `TargetDigest`, `ActionDigest`, `ContextDigest`; +- `AdmitDecision`, `DenyDecision`, `ReviewDecision`; +- `IssuedDecisionReceipt`, `ConsumedReceiptRecord`, `RejectedReceiptRecord`; +- `ValidRevalidation`, `RevokedRevalidation`, `RejectedRevalidation`, + `FailClosedRevalidation`; +- `PreparedAction`, untrusted `AdapterExecution`, `VerifiedOutcome`, + `RestoredOutcome`, and `EvidenceSeal`. + +Unknown inputs become typed records only after the same runtime validation that +R0 applies. Legacy `ADMITTED` telemetry is not CINT authority. + +## Gate-ordered migration + +1. G3 introduces type-only brands, protocol/state unions, adapter contracts, + negative compile-time tests, and build mechanics. R0 JavaScript remains live. +2. G4 migrates schema/canonical together, then the leaf decision records, + challenge, and decision. +3. G5 migrates receipt, revalidation, store, and state machine. +4. G6 migrates execution, outcome, rollback, evidence, and seal. +5. G7 migrates the synthetic adapter, typed Adapter 01 bridge, Codex wrapper, + barrel, CLI, bin, and CINT tests. Package exports switch only here. + +Until their named gate, the schemas, canonical serializer, HMAC authorities, +store, execution/revalidation path, evidence/seal path, public exports, CLI, and +legacy adapter kernel remain byte-unchanged. + +## Source anchors + +The root reproduced these baseline SHA-256 values: + +- `src/cint/canonical.js`: `f9f99a1999c617a3049102b190d77ec06bb82750d27d519dde72a43f35fa5765` +- `src/cint/schema.js`: `853a85cd99cb0521dbd0e0424b097e04bc483958deb24915cbfb780ff0ca4bdb` +- `src/cint/execution.js`: `9cdb80a6db392e4a8672d1d25c5898d03411a9754e3ab65413741e789207a716` +- `src/cint/receipt.js`: `f11e5707b8adb25be37945291a91cda29580d60f53a86f8cfce8178c2ad67c95` +- `src/cint/store.js`: `3a8a91a91ec18a4968bae1c69dd5a1be0bab0f01962ff5b3df1bbdf6c7b40249` +- `src/cint/adapters/codex/index.js`: `76dbfc177104b258656356daf0d8bd9f8f90eb61d633241e77011829b6217908` + +G1 verdict: `PASS-CINT-R1-G1-A`. diff --git a/docs/cint-r1/01_NODE_AND_BUILD_POLICY.md b/docs/cint-r1/01_NODE_AND_BUILD_POLICY.md new file mode 100644 index 0000000..3f01bdc --- /dev/null +++ b/docs/cint-r1/01_NODE_AND_BUILD_POLICY.md @@ -0,0 +1,102 @@ +# CINT R1 G1 — Node, build, CI, and package policy + +Baseline: `acc857b5d6fadd363816ad9dbf2fdc710a35ba15`. + +Reviewer B ran read-only in a clean, history-free context with +`gpt-5.6-terra`, medium reasoning, no child agents, and the prescribed bounded +scope. Root independently reproduced the cited files and hashes and adjudicated +the matrix design against the controlling task. + +## Runtime policy + +The official Node.js release table at admission time reports Node 20 EOL, Node +22 LTS, Node 24 LTS, and Node 26 Current. CINT R1 therefore declares: + +| Major | R1 role | +|---|---| +| 20 | historical only; rejected by active engines and absent from active CI | +| 22 | temporary compatibility lane | +| 24 | normative development and operational baseline | +| 26 | forward-compatibility lane; not the normative baseline | + +`engines.node` will be exactly `^22.0.0 || ^24.0.0 || ^26.0.0`. +`.nvmrc` and `.node-version` select major 24; `.npmrc` enables +`engine-strict=true`. A supported/tested major is not a frozen patch release: +operators must use the latest security-patched release available within that +major. + +Observed active Node 20 declarations are in `package.json:36-38`, the generated +root metadata in `package-lock.json:19-21`, the CI matrix at +`.github/workflows/cint-r0-verification.yml:26-34`, and active root guidance. +Frozen release notes, `docs/archive/**`, evidence, and tagged material retain +their historical Node 20 statements unchanged. + +## One complete matrix, two aggregates + +The complete R1 matrix remains nine lanes: + +```text +ubuntu-24.04 x Node 22, 24, 26 +macos-26 x Node 22, 24, 26 +windows-2025 x Node 22, 24, 26 +``` + +Every lane installs the lockfile and runs the complete ordered R1 verification. +Both stable aggregate jobs depend on that same matrix: + +- `CINT-R0 remote verification` +- `CINT-R1 TypeScript verification` + +Each aggregate uses `if: always()` and fails unless the shared matrix result is +`success`. There is no second or reduced matrix. + +## Cross-platform build + +Shell-specific deletion, copy, and glob behavior are prohibited. Node scripts +will implement: + +- `clean`: remove only declared generated roots; +- `build`: compile strict CINT TypeScript and copy only the allowlisted legacy + JavaScript compatibility surface; +- `test:cint`: compile tests to `.test-dist/` and run them there; +- `pack:verify`: inspect a dry-run tarball manifest and resolve JS plus + declaration targets. + +The baseline already demonstrates a portable `npm.cmd` branch for Windows at +`scripts/verify-cint-schema-package.mjs:40-55`. + +Generated and never committed: + +```text +dist/ +.test-dist/ +*.tsbuildinfo +``` + +## Distribution contract + +At G7, CINT exports move from source paths (`package.json:19-24`) to conditional +`types` and `import` targets in `dist/` for: + +- `.`; +- `./cli`; +- `./adapters/synthetic-file-patch`; +- `./adapters/codex-delegation`. + +The `cint` bin resolves to compiled JavaScript. The `agent-floor` bin and legacy +JavaScript kernel remain preserved. The package remains `private: true`; dry-run +packing proves structure only and grants no publication authority. + +Historical AF-G0 evidence scripts retain their existing identity and semantics. +R1 build output is excluded from the immutable historical evidence manifest. + +## Source anchors + +- `package.json`: `e334fbfc29c3ef28eea299fd597ecc40303d2bb323db9197681b7d1fdc5a367b` +- `package-lock.json`: `6a8a7a9c7deb0972ea3a1b4532f8fd2c195a1fa9a8bf528dd3963e210d68e139` +- workflow: `3f0684fadbff12e7e62327e70e71b201e4c64a91c25a87575b111d57339bad6f` +- package verifier: `d961acb8126edb6219a348520c4541c10a4012a690104d25bc0b4d36ffae5054` +- `bin/cint.js`: `76ba84e79816d5b4af4b20373a03a614d50fa6fb64d6bde8c0d3194998450ba6` +- `bin/agent-floor.js`: `9513e632c1b229d7bd5bb4d69211900cd728494014671c0fd00e11fe89a74394` + +G1 verdict: `PASS-CINT-R1-G1-B`. diff --git a/docs/cint-r1/09_INDEPENDENT_REVIEW.md b/docs/cint-r1/09_INDEPENDENT_REVIEW.md new file mode 100644 index 0000000..7a7d9e0 --- /dev/null +++ b/docs/cint-r1/09_INDEPENDENT_REVIEW.md @@ -0,0 +1,93 @@ +# CINT R1 G9 — independent TypeScript review + +Baseline: `acc857b5d6fadd363816ad9dbf2fdc710a35ba15` +Candidate code head: `b549414c4c88682c5e1b56e3a29a8723bfbdb416` +Reviewed at: `2026-08-28T12:40:43Z` + +Exactly two fresh, clean-context reviewers ran with `fork_turns=none`, +`gpt-5.6-terra`, medium reasoning, no child agents, read-only source authority, +an eight-cycle ceiling, and a 1,200-word output ceiling. Neither reviewer made a +tracked change or performed a remote write. + +## Reviewer C — authority and runtime boundary + +Verdict: `ACCEPT-CINT-R1-TYPESCRIPT-CANDIDATE` +Findings: zero. + +Reviewer C verified: + +- untrusted values remain `unknown` until exact-key, schema, canonical-record, + digest, protocol, or signature validation (`src/cint/canonical.ts:102-132`, + `src/cint/canonical.ts:282-321`, `src/cint/schema.ts:108-122`); +- decisions, issued receipts, consumed and rejected store records, + revalidation states, verified/restored outcomes, and evidence seals are + distinct (`src/cint/types/records.ts:180-245`, + `src/cint/types/records.ts:262-399`); +- Adapter 01 declares zero authority operations and its legacy `ADMITTED` + value remains evidence (`src/cint/adapters/codex/legacy-adapter-boundary.ts:16-23`, + `src/cint/adapters/codex/index.ts:141-184`); +- the store uses exclusive one-shot locking and terminal records + (`src/cint/store.ts:167-255`); +- a fresh trusted time and final revalidation immediately precede adapter + invocation with no intervening await (`src/cint/execution.ts:458-481`); +- the TypeScript escape-hatch and typed legacy-boundary scans pass with zero + findings; +- the frozen surfaces report 13/13 identical schemas, 24/24 protocol strings, + 103/103 error codes, and 18/18 behavioral-equivalence scenarios. + +Reviewer C invoked the existing schema verifier, which regenerated only ignored +build output. Root confirmed zero tracked mutations and independently rebuilt +and re-attested the emitted JavaScript and declarations before acceptance. + +## Reviewer D — Node, build, package, and legacy boundary + +Verdict: `ACCEPT-CINT-R1-TYPESCRIPT-CANDIDATE` +Findings: zero. + +Reviewer D verified: + +- the engine expression is exactly `^22.0.0 || ^24.0.0 || ^26.0.0`, Node 24 is + normative, Node 22 temporary, Node 26 forward, and Node 20 historical only; +- CI is exactly nine OS/runtime lanes and both aggregate checks depend on the + same complete matrix; +- verification order and process invocation are cross-platform and fail + closed; +- all four package exports resolve compiled JavaScript and declarations, both + CLIs resolve correctly, and the package remains private at + `0.1.0-cint-r0`; +- `dist/`, `.test-dist/`, and `*.tsbuildinfo` are ignored and untracked, and + source maps contain no private absolute paths; +- core and identity imports do not load Adapter 01; only the explicit legacy + command crosses its typed lazy boundary; +- the preserved legacy JavaScript remains functional; +- no Three.js, Rust, WASM, React, web-framework, UI, server, browser-bundle, or + runtime TypeScript dependency entered the candidate. + +## Root re-attestation + +Root independently reproduced every decision-bearing finding: + +- Node 24.20.0 full ordered verification: 56/56 compiled CINT tests and 16/16 + legacy tests, 72/72 total; +- 14 compile-time negative contracts, 30-file escape-hatch scan, and 29-file + import-boundary scan: pass with zero findings; +- R0 parity harness: 18/18 scenarios and artifact verification pass; +- schemas: 13/13 bytes identical; protocols: 24/24 identical; error codes: + 103/103 identical; +- independently emitted JavaScript and declarations match `dist/` exactly; + packaged source maps use relative sources and disclose zero private paths; +- `src/cint/`: 29 TypeScript files and zero JavaScript files; +- preserved Agent Floor JavaScript and historical R0/archive documents are + unchanged from the accepted base; +- dependency audit: zero vulnerabilities; publication audit: zero findings; + AF-G0 evidence: 53/53; package exports and declarations: 4/4 each; +- package dependencies are only AJV/AJV Formats at runtime and exact-pinned + TypeScript 7.0.2 plus `@types/node` 26.4.0 for development. + +No finding weakens an earlier gate. No schema, protocol, error, canonical byte, +HMAC input, authority order, one-shot state, trusted-time boundary, legacy +behavior, or framework boundary changed. + +## Verdict + +`ACCEPT-CINT-R1-TYPESCRIPT-CANDIDATE` diff --git a/docs/cint-r1/10_CORRECTION_C2.md b/docs/cint-r1/10_CORRECTION_C2.md new file mode 100644 index 0000000..eec2f6a --- /dev/null +++ b/docs/cint-r1/10_CORRECTION_C2.md @@ -0,0 +1,158 @@ +# CINT R1 C2 — state, logic, and architecture + +This record supersedes no historical gate document. It appends the final +package-launch correction state to the R1 candidate lineage. + +## C2 acceptance snapshot — 2026-08-28 + +| Field | Value | +|---|---| +| Repository | `sntllgnc/CINT` | +| Base | `main` at `acc857b5d6fadd363816ad9dbf2fdc710a35ba15` | +| Branch | `cint-r1-typescript` | +| Pre-C1 candidate | `4338955d41749146da36f714a0e0182b9f4bb32e` | +| C1 | `42465b0192d167f419e782674fd4675c4049e4d9` | +| C2 | `97dac5e80609ba6522f15bb5ecc0a4c0aa5ef022` | +| Pull request at C2 acceptance | [#2](https://github.com/sntllgnc/CINT/pull/2); it was open and draft at C2 acceptance | +| Nonqualifying C1 run | `33173883181`, preserved and not rerun | +| Qualifying C2 run | [33175315187](https://github.com/sntllgnc/CINT/actions/runs/33175315187) | +| Terminal | `READY-FOR-CINT-R1-TYPESCRIPT-REVIEW` | + +At C2 acceptance, the terminal meant the branch satisfied the bounded +TypeScript review gate. It did not mean merged, released, published, deployed, +or production-ready. Live head, workflow, and review-readiness state are +maintained on [PR #2](https://github.com/sntllgnc/CINT/pull/2) and its GitHub +checks. + +## Corrected contradiction + +C1 completed the line-ending-independent workflow checks and migrated all six +CINT test suites to strict TypeScript. Its remote run then exposed a distinct +package-verification failure on Windows: + +```text +build and package checks reached +-> spawnSync("npm.cmd", fixed pack arguments) +-> process status = null +-> package verification cannot establish a normal npm result +``` + +The defect belonged to process launch, not package contents, TypeScript output, +runtime authority, schemas, protocols, or historical behavior. + +## Design invariant + +Package verification admits output only after a process has started without a +spawn error and returned an ordinary zero exit status. + +```text +spawn error -> FAIL +status null -> FAIL +status non-zero -> FAIL +status zero -> parse JSON +invalid JSON -> FAIL +wrong report form -> FAIL +valid report -> verify package inventory and exports +``` + +`status: null` is not collapsed into an ordinary non-zero result. It represents +an indeterminate launch or signal state and fails before JSON parsing. + +## Launch-selection logic + +The immutable package request remains: + +```text +npm pack --dry-run --json --ignore-scripts +``` + +Selection is deterministic: + +| Condition | Command boundary | Shell | +|---|---|---| +| `npm_execpath` is present | `process.execPath`, then npm CLI path and fixed arguments | `false` | +| npm metadata absent on Windows | `ComSpec` or `cmd.exe`, then `/d /s /c` and one fixed command | `false` | +| npm metadata absent on POSIX | `npm` with the fixed arguments | `false` | + +The preferred path avoids platform-specific executable-wrapper behavior. The +Windows fallback uses only fixed verifier-owned text. No request, package +field, path from an untrusted record, or adapter result enters its command. + +## Package-assurance sequence + +```text +deterministic launch regression +-> strict build +-> select npm invocation +-> require a determinate successful process result +-> parse exactly one JSON pack report +-> require its file inventory +-> match every declared bin, types, and export target +-> resolve four compiled runtime exports +-> typecheck the declaration consumer +-> reject private absolute paths in source maps +-> require package private = true +-> PASS +``` + +This sequence is a build-assurance gate. It issues no CINT decision, receipt, +consumption record, verified outcome, or seal. + +## Runtime architecture preserved + +C2 changes none of the CINT authority sequence: + +```text +unknown input +-> runtime schema and canonical admission +-> principal, authority, policy, capability, and machine-state challenge +-> ADMIT / DENY / REVIEW with execution_authority = NONE +-> authenticated one-shot receipt +-> locked revalidation and atomic consumption +-> side-effect-free preparation +-> fresh trusted time and final revalidation +-> exact adapter invocation +-> untrusted outcome verification +-> verified seal or verified rollback +``` + +The TypeScript compiler makes authority states and identities mechanically +distinct during development. Emitted JavaScript plus the 13 runtime schemas, +canonical digests, HMAC checks, receipt store, trusted-time revalidation, and +adapter verifiers remain the runtime enforcement surface. + +## Verification receipt + +Local final proof: + +- package-launch regressions: 6/6; +- existing runtime tests: 72/72; +- CINT test suites: 6 TypeScript / 0 JavaScript; +- production CINT: 29 TypeScript / 0 JavaScript; +- schemas: 13/13 with unchanged hashes; +- behavioral equivalence: 18/18; +- AF-G0 evidence: 53/53; +- dependency vulnerabilities and publication findings: 0/0; +- runtime and declaration exports: 4/4 each; +- generated output: ignored and untracked; +- independent correction review: `ACCEPT-CINT-R1-CORRECTION`. + +Remote final proof: + +- Ubuntu 24.04 on Node.js 22, 24, and 26: pass; +- macOS 26 on Node.js 22, 24, and 26: pass; +- Windows 2025 on Node.js 22, 24, and 26: pass; +- `CINT-R0 remote verification`: pass; +- `CINT-R1 TypeScript verification`: pass. + +## Remaining gate + +The candidate is ready for R1 TypeScript review. The following remain outside +this state: + +- PR ready conversion and merge; +- direct `main` mutation; +- tag, release, or npm publication; +- production or machine-wide deployment; +- Rust, WASM, Three.js, browser, UI, or visualization work; +- any rewrite of Agent Floor or the immutable R0 evidence lineage. diff --git a/package-lock.json b/package-lock.json index 96446a3..c5495a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,365 @@ "ajv-formats": "^3.0.1" }, "bin": { - "agent-floor": "bin/agent-floor.js", - "cint": "bin/cint.js" + "agent-floor": "dist/bin/agent-floor.js", + "cint": "dist/bin/cint.js" }, + "devDependencies": { + "@types/node": "26.4.0", + "typescript": "7.0.2" + }, + "engines": { + "node": "^22.0.0 || ^24.0.0 || ^26.0.0" + } + }, + "node_modules/@types/node": { + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=20" + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" } }, "node_modules/ajv": { @@ -89,6 +443,48 @@ "engines": { "node": ">=0.10.0" } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 97152f1..8b008da 100644 --- a/package.json +++ b/package.json @@ -13,32 +13,64 @@ "url": "https://github.com/sntllgnc/CINT/issues" }, "bin": { - "cint": "./bin/cint.js", - "agent-floor": "./bin/agent-floor.js" + "cint": "./dist/bin/cint.js", + "agent-floor": "./dist/bin/agent-floor.js" }, + "types": "./dist/src/cint/index.d.ts", "exports": { - ".": "./src/cint/index.js", - "./cli": "./src/cint/cli.js", - "./adapters/synthetic-file-patch": "./src/cint/adapters/synthetic-file-patch.js", - "./adapters/codex-delegation": "./src/cint/adapters/codex/index.js" + ".": { + "types": "./dist/src/cint/index.d.ts", + "import": "./dist/src/cint/index.js" + }, + "./cli": { + "types": "./dist/src/cint/cli.d.ts", + "import": "./dist/src/cint/cli.js" + }, + "./adapters/synthetic-file-patch": { + "types": "./dist/src/cint/adapters/synthetic-file-patch.d.ts", + "import": "./dist/src/cint/adapters/synthetic-file-patch.js" + }, + "./adapters/codex-delegation": { + "types": "./dist/src/cint/adapters/codex/index.d.ts", + "import": "./dist/src/cint/adapters/codex/index.js" + } }, + "files": [ + "dist/", + "LICENSE", + "README.md" + ], "scripts": { - "cint:identity": "node ./bin/cint.js identity", - "cint:test": "node --test ./tests/cint-*.test.js", + "build": "node ./scripts/build.mjs", + "cint:identity": "node ./scripts/run-built-cint.mjs identity", + "cint:test": "npm run test:cint", + "clean": "node ./scripts/clean.mjs", "demo": "node ./bin/agent-floor.js demo", "doctor": "node ./bin/agent-floor.js doctor", - "test": "node --test", + "pack:verify": "node --test ./tests/npm-pack-launch.test.mjs && node ./scripts/verify-package.mjs", + "scan:escapes": "node ./scripts/scan-typescript-escape-hatches.mjs", + "scan:boundaries": "node ./scripts/verify-cint-import-boundaries.mjs", + "test": "node ./scripts/test.mjs all", + "test:cint": "node ./scripts/test.mjs cint", + "test:legacy": "node ./scripts/test.mjs legacy", + "test:types": "npm run typecheck", + "typecheck": "tsc --project tsconfig.test.json", + "runtime:verify": "node ./scripts/verify-runtime-support.mjs", "schema:verify": "node ./scripts/verify-cint-schema-package.mjs", "public:audit": "node ./scripts/publication-audit.mjs", "evidence:verify": "node ./scripts/verify-evidence-manifest.mjs", - "verify": "npm test && npm run demo && npm audit && npm run public:audit && npm run evidence:verify" + "verify": "node ./scripts/verify.mjs" }, "engines": { - "node": ">=20" + "node": "^22.0.0 || ^24.0.0 || ^26.0.0" }, "license": "Apache-2.0", "dependencies": { "ajv": "^8.20.0", "ajv-formats": "^3.0.1" + }, + "devDependencies": { + "@types/node": "26.4.0", + "typescript": "7.0.2" } } diff --git a/scripts/build-test.mjs b/scripts/build-test.mjs new file mode 100644 index 0000000..a8040ee --- /dev/null +++ b/scripts/build-test.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { cp, mkdir } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("../", import.meta.url)); + +const build = spawnSync(process.execPath, [fileURLToPath(new URL("./build.mjs", import.meta.url))], { + cwd: root, + stdio: "inherit" +}); +assert.equal(build.status, 0, "runtime build failed before test overlay"); + +const compileTests = spawnSync(process.execPath, [ + fileURLToPath(new URL("../node_modules/typescript/bin/tsc", import.meta.url)), + "--project", + "tsconfig.cint-test-build.json" +], { + cwd: root, + stdio: "inherit" +}); +assert.equal(compileTests.status, 0, "strict CINT test compilation failed before test overlay"); + +const testRoot = path.join(root, ".test-dist"); +await mkdir(testRoot, { recursive: true }); +for (const directory of ["src", "bin", "schemas", "fixtures", "examples", "tests"]) { + await cp(path.join(root, directory === "src" || directory === "bin" || directory === "schemas" ? "dist" : "", directory), path.join(testRoot, directory), { + recursive: true, + force: true + }); +} + +console.log(JSON.stringify({ + gate: "CINT-R1-TEST-BUILD", + verdict: "PASS", + output: ".test-dist", + strict_cint_test_compilation: true +}, null, 2)); diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..b8e48a5 --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { copyFile, mkdir, readdir } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("../", import.meta.url)); + +function run(args, label) { + const result = spawnSync(process.execPath, args, { + cwd: root, + stdio: "inherit" + }); + assert.equal(result.status, 0, `${label} failed`); +} + +async function copyMatching(source, destination, include) { + await mkdir(destination, { recursive: true }); + for (const entry of await readdir(source, { withFileTypes: true })) { + const sourcePath = path.join(source, entry.name); + const destinationPath = path.join(destination, entry.name); + if (entry.isDirectory()) { + await copyMatching(sourcePath, destinationPath, include); + } else if (entry.isFile() && include(entry.name)) { + await mkdir(path.dirname(destinationPath), { recursive: true }); + await copyFile(sourcePath, destinationPath); + } + } +} + +run([fileURLToPath(new URL("./clean.mjs", import.meta.url))], "clean"); +run([ + fileURLToPath(new URL("../node_modules/typescript/bin/tsc", import.meta.url)), + "--project", + "tsconfig.build.json" +], "TypeScript build"); + +await copyMatching(path.join(root, "src"), path.join(root, "dist", "src"), (name) => name.endsWith(".js")); +await copyMatching(path.join(root, "bin"), path.join(root, "dist", "bin"), (name) => name.endsWith(".js")); +await copyMatching(path.join(root, "schemas"), path.join(root, "dist", "schemas"), (name) => name.endsWith(".json")); +await copyMatching(path.join(root, "fixtures"), path.join(root, "dist", "fixtures"), () => true); +await copyMatching(path.join(root, "examples"), path.join(root, "dist", "examples"), () => true); + +console.log(JSON.stringify({ + gate: "CINT-R1-BUILD", + verdict: "PASS", + output: "dist", + staged_javascript_copy: true, + schema_copy: true, + legacy_demo_assets_copy: true +}, null, 2)); diff --git a/scripts/clean.mjs b/scripts/clean.mjs new file mode 100644 index 0000000..7e5c137 --- /dev/null +++ b/scripts/clean.mjs @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import { readdir, readFile, rm } from "node:fs/promises"; + +const root = new URL("../", import.meta.url); +const packageJson = JSON.parse(await readFile(new URL("package.json", root), "utf8")); +assert.equal(packageJson.name, "@sntllgnc/cint", "refusing to clean an unexpected package root"); + +for (const relativePath of ["dist/", ".test-dist/"]) { + await rm(new URL(relativePath, root), { recursive: true, force: true }); +} + +for (const entry of await readdir(root, { withFileTypes: true })) { + if (entry.isFile() && entry.name.endsWith(".tsbuildinfo")) { + await rm(new URL(entry.name, root), { force: true }); + } +} + +console.log(JSON.stringify({ + gate: "CINT-R1-CLEAN", + verdict: "PASS", + removed_roots: ["dist", ".test-dist"], + removed_root_tsbuildinfo: true +}, null, 2)); diff --git a/scripts/npm-pack-launch.mjs b/scripts/npm-pack-launch.mjs new file mode 100644 index 0000000..96038c3 --- /dev/null +++ b/scripts/npm-pack-launch.mjs @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; + +export const NPM_PACK_ARGUMENTS = Object.freeze([ + "pack", + "--dry-run", + "--json", + "--ignore-scripts" +]); + +const WINDOWS_NPM_PACK_COMMAND = `npm.cmd ${NPM_PACK_ARGUMENTS.join(" ")}`; + +function nonEmpty(value) { + return typeof value === "string" && value.length > 0; +} + +export function npmPackInvocation(options = {}) { + const platform = Object.hasOwn(options, "platform") ? options.platform : process.platform; + const execPath = Object.hasOwn(options, "execPath") ? options.execPath : process.execPath; + const npmExecPath = Object.hasOwn(options, "npmExecPath") + ? options.npmExecPath + : process.env.npm_execpath; + const commandProcessor = Object.hasOwn(options, "commandProcessor") + ? options.commandProcessor + : process.env.ComSpec; + if (nonEmpty(npmExecPath)) { + return { + command: execPath, + args: [npmExecPath, ...NPM_PACK_ARGUMENTS], + mode: "NPM_CLI_PATH", + shell: false + }; + } + + if (platform === "win32") { + return { + command: nonEmpty(commandProcessor) ? commandProcessor : "cmd.exe", + args: ["/d", "/s", "/c", WINDOWS_NPM_PACK_COMMAND], + mode: "WINDOWS_COMMAND_PROCESSOR", + shell: false + }; + } + + return { + command: "npm", + args: [...NPM_PACK_ARGUMENTS], + mode: "POSIX_PATH", + shell: false + }; +} + +export function spawnNpmPack({ cwd, spawn = spawnSync, ...options }) { + const invocation = npmPackInvocation(options); + const result = spawn(invocation.command, invocation.args, { + cwd, + encoding: "utf8", + shell: invocation.shell, + windowsHide: true + }); + return { invocation, result }; +} + +export function readNpmPackReport(result) { + if (result.error !== undefined) { + const code = nonEmpty(result.error.code) ? ` (${result.error.code})` : ""; + throw new Error(`npm pack could not start${code}: ${result.error.message}`, { + cause: result.error + }); + } + + if (result.status === null) { + const signal = nonEmpty(result.signal) ? ` after signal ${result.signal}` : ""; + throw new Error(`npm pack returned no exit status${signal}`); + } + + assert.equal(result.status, 0, result.stderr || "npm pack dry run failed"); + const report = JSON.parse(result.stdout); + assert.ok(Array.isArray(report) && report.length === 1, "unexpected npm pack report"); + assert.ok(Array.isArray(report[0].files), "npm pack report is missing its file inventory"); + return report; +} diff --git a/scripts/publication-audit.mjs b/scripts/publication-audit.mjs index 9782f22..66ccc67 100644 --- a/scripts/publication-audit.mjs +++ b/scripts/publication-audit.mjs @@ -4,9 +4,9 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const ignoredDirectories = new Set([".git", "node_modules"]); +const ignoredDirectories = new Set([".git", ".test-dist", "dist", "node_modules"]); const ignoredPrefixes = ["artifacts/generated/"]; -const allowedDotfiles = new Set([".gitignore"]); +const allowedDotfiles = new Set([".gitignore", ".node-version", ".npmrc", ".nvmrc"]); const allowedHiddenDirectories = new Set([".github"]); const privateNames = ["S" + "I1", "Y" + "I1", "S" + "I6", "F" + "YRE"]; const authorizedPublicNames = [new RegExp("\\b" + "SI1(?:[ -]CINT)" + "\\b", "gi")]; @@ -42,7 +42,7 @@ async function walk(directory) { findings.push({ code: "NON_REGULAR_FILE", path: relative }); continue; } - if (entry.name.startsWith(".") && !allowedDotfiles.has(entry.name)) { + if (entry.name.startsWith(".") && !allowedDotfiles.has(relative)) { findings.push({ code: "HIDDEN_FILE", path: relative }); } if (entry.name.endsWith(".log")) findings.push({ code: "RAW_LOG_FILE", path: relative }); diff --git a/scripts/run-built-cint.mjs b/scripts/run-built-cint.mjs new file mode 100644 index 0000000..e919417 --- /dev/null +++ b/scripts/run-built-cint.mjs @@ -0,0 +1,15 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("../", import.meta.url)); +for (const [entry, args, label] of [ + [new URL("./build.mjs", import.meta.url), [], "CINT build"], + [new URL("../dist/bin/cint.js", import.meta.url), process.argv.slice(2), "CINT CLI"] +]) { + const result = spawnSync(process.execPath, [fileURLToPath(entry), ...args], { + cwd: root, + stdio: "inherit" + }); + assert.equal(result.status, 0, `${label} failed`); +} diff --git a/scripts/run-test-suite.mjs b/scripts/run-test-suite.mjs new file mode 100644 index 0000000..ab6b537 --- /dev/null +++ b/scripts/run-test-suite.mjs @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { readdir } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const suite = process.argv[2]; +assert.ok(["all", "cint", "legacy"].includes(suite), "suite must be all, cint, or legacy"); + +const sourceRoot = fileURLToPath(new URL("../", import.meta.url)); +const root = process.argv[3] === undefined ? sourceRoot : path.resolve(sourceRoot, process.argv[3]); +const tests = (await readdir(path.join(root, "tests"))) + .filter((name) => name.endsWith(".test.js")) + .filter((name) => { + if (suite === "all") return true; + return suite === "cint" ? name.startsWith("cint-") : !name.startsWith("cint-"); + }) + .sort() + .map((name) => path.join("tests", name)); + +assert.ok(tests.length > 0, `no ${suite} tests found`); +const result = spawnSync(process.execPath, ["--test", ...tests], { + cwd: root, + stdio: "inherit" +}); +assert.equal(result.status, 0, `${suite} test suite failed`); diff --git a/scripts/scan-typescript-escape-hatches.mjs b/scripts/scan-typescript-escape-hatches.mjs new file mode 100644 index 0000000..0e550a6 --- /dev/null +++ b/scripts/scan-typescript-escape-hatches.mjs @@ -0,0 +1,52 @@ +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = new URL("../", import.meta.url); +const rootPath = fileURLToPath(root); +const findings = []; +const prohibited = Object.freeze([ + ["EXPLICIT_ANY", /\bany\b/gu], + ["TYPE_IGNORE", /@ts-ignore\b/gu], + ["TYPE_NOCHECK", /@ts-nocheck\b/gu], + ["TYPE_EXPECT_ERROR_IN_PRODUCTION", /@ts-expect-error\b/gu], + ["DOUBLE_ASSERTION", /\bas\s+(?:unknown|never)\s+as\b/gu], + ["NON_NULL_ASSERTION", /(?:[A-Za-z0-9_$)\]])!(?=[.\[();,?:])/gu], + ["DEFINITE_ASSIGNMENT_ASSERTION", /\b[A-Za-z_$][A-Za-z0-9_$]*!\s*:/gu] +]); + +async function collect(directory) { + const files = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + const url = new URL(entry.name + (entry.isDirectory() ? "/" : ""), directory); + if (entry.isDirectory()) files.push(...await collect(url)); + if (entry.isFile() && entry.name.endsWith(".ts")) files.push(url); + } + return files; +} + +const sourceFiles = await collect(new URL("src/cint/", root)); +const binSource = new URL("bin/cint.ts", root); +if (await readFile(binSource, "utf8").then(() => true, () => false)) sourceFiles.push(binSource); + +for (const url of sourceFiles) { + const text = await readFile(url, "utf8"); + const relative = path.relative(rootPath, fileURLToPath(url)).split(path.sep).join("/"); + for (const [code, expression] of prohibited) { + for (const match of text.matchAll(expression)) { + const prefix = text.slice(0, match.index); + const line = prefix.split("\n").length; + const column = match.index - prefix.lastIndexOf("\n"); + findings.push({ file: relative, code, line, column }); + } + } +} + +const result = { + gate: "CINT-R1-TYPESCRIPT-ESCAPE-HATCH-SCAN", + verdict: findings.length === 0 ? "PASS" : "FAIL", + files_scanned: sourceFiles.length, + findings +}; +console.log(JSON.stringify(result, null, 2)); +if (findings.length > 0) process.exitCode = 1; diff --git a/scripts/test.mjs b/scripts/test.mjs new file mode 100644 index 0000000..f704eba --- /dev/null +++ b/scripts/test.mjs @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const suite = process.argv[2] ?? "all"; +assert.ok(["all", "cint", "legacy"].includes(suite), "suite must be all, cint, or legacy"); + +const root = fileURLToPath(new URL("../", import.meta.url)); +for (const [script, args, label] of [ + ["./build-test.mjs", [], "compiled test build"], + ["./run-test-suite.mjs", [suite, ".test-dist"], `${suite} compiled tests`] +]) { + const result = spawnSync(process.execPath, [fileURLToPath(new URL(script, import.meta.url)), ...args], { + cwd: root, + stdio: "inherit" + }); + assert.equal(result.status, 0, `${label} failed`); +} diff --git a/scripts/verify-behavioral-equivalence.mjs b/scripts/verify-behavioral-equivalence.mjs new file mode 100644 index 0000000..4f67cd1 --- /dev/null +++ b/scripts/verify-behavioral-equivalence.mjs @@ -0,0 +1,707 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { chmod, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const [baselineRoot, candidateRoot, artifactPath] = process.argv.slice(2); +assert.ok(baselineRoot && candidateRoot, "usage: verify-behavioral-equivalence.mjs "); + +const T0 = "2026-08-27T00:00:00.000Z"; +const T1 = "2026-08-27T00:01:00.000Z"; +const T2 = "2026-08-27T00:10:00.000Z"; +const EXPIRY = "2026-08-27T01:00:00.000Z"; +const TARGET = Object.freeze({ path: "target.txt" }); + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value !== null && typeof value === "object") { + return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stable(item)])); + } + return value; +} + +function digest(value) { + return createHash("sha256").update(JSON.stringify(stable(value))).digest("hex"); +} + +function hashBytes(value) { + return createHash("sha256").update(value).digest("hex"); +} + +async function sourceSurface(root, expression) { + const values = new Set(); + async function walk(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const absolute = path.join(directory, entry.name); + if (entry.isDirectory()) await walk(absolute); + if (!entry.isFile() || !entry.name.endsWith(".js")) continue; + const content = await readFile(absolute, "utf8"); + for (const match of content.matchAll(expression)) values.add(match[1]); + } + } + await walk(path.join(path.resolve(root), "src", "cint")); + return [...values].sort(); +} + +async function schemaInventory(root) { + const directory = path.join(path.resolve(root), "schemas", "cint"); + const files = (await readdir(directory)).filter((name) => name.endsWith(".schema.json")).sort(); + return Object.fromEntries(await Promise.all(files.map(async (name) => [name, hashBytes(await readFile(path.join(directory, name)))]))); +} + +function property(value, name) { + return value !== null && (typeof value === "object" || typeof value === "function") + ? Reflect.get(value, name) + : undefined; +} + +function errorCode(error) { + return typeof property(error, "code") === "string" ? property(error, "code") : "UNKNOWN"; +} + +async function loadImplementation(root) { + const absolute = path.resolve(root); + const module = (relative) => import(pathToFileURL(path.join(absolute, relative)).href); + return { + api: await module("src/cint/index.js"), + synthetic: await module("src/cint/adapters/synthetic-file-patch.js"), + codex: await module("src/cint/adapters/codex/index.js"), + demo: await module("src/demo.js"), + policy: await module("src/policy.js") + }; +} + +function baseRecords(implementation, intentOverrides = {}) { + const { api } = implementation; + const before = "0".repeat(64); + const intent = api.createIntent({ + id: "intent.equivalence.1", + principal_id: "principal.operator", + request: Object.hasOwn(intentOverrides, "request") ? intentOverrides.request : "Apply the deterministic synthetic patch.", + action: { + adapter: "cint.adapter.synthetic-file-patch", + type: "SYNTHETIC_FILE_PATCH", + target: TARGET, + parameters: { content: "after\n", expected_before_sha256: before }, + consequence: "CONSEQUENTIAL" + }, + declared_effects: ["Replace the declared disposable target bytes"], + context: { workspace: "equivalence" }, + uncertainties: intentOverrides.uncertainties ?? [], + created_at: T0 + }); + const principal = api.resolvePrincipal({ + id: "principal.operator", + type: "HUMAN", + authenticated: true, + authority_chain: ["authority.equivalence.1"], + attributes: {}, + resolved_at: T0 + }); + const authority = api.createAuthorityGrant({ + id: "authority.equivalence.1", + principal_id: "principal.operator", + issuer_id: "principal.authority", + epoch: 1, + grants: [{ adapter: "cint.adapter.synthetic-file-patch", type: "SYNTHETIC_FILE_PATCH", target: TARGET }], + policy_ids: ["policy.equivalence"], + require_rollback: true, + issued_at: T0, + not_before: T0, + expires_at: EXPIRY + }); + const policy = api.createPolicySnapshot({ + id: "policy.equivalence", + version: "r0.1", + epoch: 1, + allowed_adapters: ["cint.adapter.synthetic-file-patch"], + allowed_action_types: ["SYNTHETIC_FILE_PATCH"], + denied_action_types: [], + require_explicit_request: true, + require_declared_effects: true, + require_rollback_for_consequential: true, + review_on_uncertainty: true, + issued_at: T0 + }); + const adapter_capability = api.createAdapterCapability({ + id: "cint.adapter.synthetic-file-patch", + action_types: ["SYNTHETIC_FILE_PATCH"], + consequence_classes: ["CONSEQUENTIAL"], + prepare_side_effect_free: true, + rollback: true, + interrupt: true, + outcome_verification: true + }); + const machine_state = api.createMachineStateSnapshot({ + id: "machine.equivalence", + epoch: 1, + available: true, + state: { mode: "READY" }, + observed_at: T0 + }); + return { intent, principal, authority, policy, adapter_capability, machine_state }; +} + +function decisionFor(implementation, disposition) { + const overrides = disposition === "DENY" + ? { request: null } + : disposition === "REVIEW" + ? { uncertainties: ["operator confirmation pending"] } + : {}; + const records = baseRecords(implementation, overrides); + return implementation.api.decide({ + id: `decision.equivalence.${disposition.toLowerCase()}`, + ...records, + now: T1, + expires_at: T2 + }); +} + +function normalizedDecision(implementation, disposition) { + const decision = decisionFor(implementation, disposition); + return { + status: decision.status, + receipt_eligible: decision.receipt_eligible, + error_codes: decision.reason_codes, + digest: decision.digest, + binding_digest: decision.binding_digest + }; +} + +function receiptVector(implementation) { + const decision = decisionFor(implementation, "ADMIT"); + const authority = new implementation.api.DecisionReceiptAuthority({ + issuer_id: "cint.receipt-authority.equivalence", + key: Buffer.alloc(32, 7) + }); + const receipt = authority.issue({ + id: "receipt.equivalence.issued", + nonce: "receipt-equivalence-nonce-0001", + decision, + issued_at: T1 + }); + authority.verify(receipt, { now: T1 }); + return { + status: receipt.status, + digest: receipt.digest, + signature: receipt.signature, + decision_digest: receipt.decision_digest, + binding_digest: receipt.binding_digest + }; +} + +async function receiptStoreScenario(implementation, race) { + const root = await mkdtemp(path.join(os.tmpdir(), "cint-equivalence-store-")); + try { + const decision = decisionFor(implementation, "ADMIT"); + const authority = new implementation.api.DecisionReceiptAuthority({ + issuer_id: "cint.receipt-authority.equivalence", + key: Buffer.alloc(32, 7) + }); + const receipt = authority.issue({ + id: "receipt.equivalence.store", + nonce: "receipt-equivalence-store-0001", + decision, + issued_at: T1 + }); + const store = new implementation.api.FileReceiptStore(path.join(root, "store")); + await store.register(receipt, { registered_at: T1 }); + const revalidate = async () => ({ status: "VALID", reason_codes: [], digest: "a".repeat(64) }); + if (race) { + const attempts = await Promise.allSettled([ + store.consume(receipt, { consumed_at: T1, revalidate }), + store.consume(receipt, { consumed_at: T1, revalidate }) + ]); + return { + fulfilled: attempts.filter((item) => item.status === "fulfilled").length, + rejected: attempts.filter((item) => item.status === "rejected").map((item) => errorCode(item.reason)).sort(), + store_state: (await store.inspect(receipt.id)).state + }; + } + await store.consume(receipt, { consumed_at: T1, revalidate }); + let replayCode = null; + try { + await store.consume(receipt, { consumed_at: T1, revalidate }); + } catch (error) { + replayCode = errorCode(error); + } + return { replay_code: replayCode, store_state: (await store.inspect(receipt.id)).state }; + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +function clock(initial = T1) { + let current = initial; + return Object.freeze({ now: () => current, set: (next) => { current = next; } }); +} + +async function withSyntheticRuntime(implementation, run) { + const { api } = implementation; + const root = await mkdtemp(path.join(os.tmpdir(), "cint-equivalence-runtime-")); + const target = path.join(root, "target.txt"); + const initial = Buffer.from("before\n", "utf8"); + await writeFile(target, initial); + const beforeSha256 = hashBytes(initial); + const adapter = new implementation.synthetic.SyntheticFilePatchAdapter(root); + const intent = api.createIntent({ + id: "intent.execution.1", + principal_id: "principal.operator", + request: "Apply the bounded synthetic file patch.", + action: { + adapter: "cint.adapter.synthetic-file-patch", + type: "SYNTHETIC_FILE_PATCH", + target: TARGET, + parameters: { content: "after\n", expected_before_sha256: beforeSha256 }, + consequence: "CONSEQUENTIAL" + }, + declared_effects: ["Replace the declared disposable target bytes"], + context: { workspace: "synthetic-proof" }, + uncertainties: [], + created_at: T0 + }); + const principal = api.resolvePrincipal({ + id: "principal.operator", + type: "HUMAN", + authenticated: true, + authority_chain: ["authority.execution.1"], + attributes: {}, + resolved_at: T0 + }); + const authority = api.createAuthorityGrant({ + id: "authority.execution.1", + principal_id: "principal.operator", + issuer_id: "principal.authority", + epoch: 1, + grants: [{ adapter: "cint.adapter.synthetic-file-patch", type: "SYNTHETIC_FILE_PATCH", target: TARGET }], + policy_ids: ["policy.execution"], + require_rollback: true, + issued_at: T0, + not_before: T0, + expires_at: EXPIRY + }); + const makePolicy = (overrides = {}) => api.createPolicySnapshot({ + id: "policy.execution", + version: overrides.version ?? "r0.1", + epoch: overrides.epoch ?? 1, + allowed_adapters: ["cint.adapter.synthetic-file-patch"], + allowed_action_types: ["SYNTHETIC_FILE_PATCH"], + denied_action_types: [], + require_explicit_request: true, + require_declared_effects: true, + require_rollback_for_consequential: true, + review_on_uncertainty: true, + issued_at: overrides.issued_at ?? T0 + }); + const policy = makePolicy(); + const machine_state = api.createMachineStateSnapshot({ + id: "machine.synthetic", + epoch: 1, + available: true, + state: { mode: "READY", target_sha256: beforeSha256 }, + observed_at: T0 + }); + const snapshot = { intent, principal, authority, policy, adapter_capability: adapter.capability, machine_state }; + const decision = api.decide({ id: "decision.execution.1", ...snapshot, now: T1, expires_at: T2 }); + const receipt_authority = new api.DecisionReceiptAuthority({ issuer_id: "cint.receipt-authority.r0", key: Buffer.alloc(32, 11) }); + const receipt = receipt_authority.issue({ + id: "receipt.execution.1", + nonce: "execution-receipt-nonce-0001", + decision, + issued_at: T1 + }); + const store = new api.FileReceiptStore(path.join(root, "receipt-store")); + await store.register(receipt, { registered_at: T1 }); + const seal_authority = new api.OutcomeSealAuthority({ issuer_id: "cint.seal-authority.r0", key: Buffer.alloc(32, 19) }); + const ledger = new api.ExecutionLedger(path.join(root, "evidence", "execution.jsonl")); + const trustedClock = clock(); + const runtime = { + api, + root, + target, + beforeSha256, + adapter, + intent, + principal, + authority, + policy, + makePolicy, + machine_state, + snapshot, + receipt, + receipt_authority, + store, + seal_authority, + ledger, + clock: trustedClock, + execute: (overrides = {}) => api.executeWithReceipt({ + receipt, + receipt_authority, + store, + snapshot_provider: async () => snapshot, + clock: trustedClock, + adapter, + seal_authority, + ledger, + at: T1, + ...overrides + }) + }; + try { + return await run(runtime); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +async function normalizedExecution(runtime, result, extras = {}) { + return { + status: result.status, + error_code: result.error_code, + action_started: result.action_started, + outcome_status: result.outcome?.status ?? null, + effect_status: result.outcome?.effect_status ?? null, + outcome_digest: result.outcome?.digest ?? null, + final_state_digest: result.outcome?.final_state_digest ?? null, + target_sha256: hashBytes(await readFile(runtime.target)), + store_state: (await runtime.store.inspect(runtime.receipt.id)).state, + ...extras + }; +} + +function adapterProxy(runtime, onPrepared) { + return { + id: runtime.adapter.id, + capability: runtime.adapter.capability, + prepare: async (...args) => { + const prepared = await runtime.adapter.prepare(...args); + await onPrepared(); + return prepared; + }, + execute: runtime.adapter.execute.bind(runtime.adapter), + verify: runtime.adapter.verify.bind(runtime.adapter), + rollback: runtime.adapter.rollback.bind(runtime.adapter) + }; +} + +async function driftScenario(implementation, kind) { + return withSyntheticRuntime(implementation, async (runtime) => { + let active = runtime.snapshot; + let executeCalls = 0; + const adapter = adapterProxy(runtime, async () => { + if (kind === "expiry") runtime.clock.set(T2); + if (kind === "policy") active = { ...active, policy: runtime.makePolicy({ version: "r0.2", epoch: 2, issued_at: T1 }) }; + if (kind === "authority") { + active = { ...active, authority: runtime.api.revokeAuthority(runtime.authority, { revoked_at: T1, reason: "Authority withdrawn" }) }; + } + if (kind === "machine") { + active = { + ...active, + machine_state: runtime.api.createMachineStateSnapshot({ + id: "machine.synthetic", + epoch: 2, + available: true, + state: { mode: "DRIFTED" }, + observed_at: T1 + }) + }; + } + if (kind === "adapter") { + active = { + ...active, + adapter_capability: runtime.api.createAdapterCapability({ + id: "cint.adapter.synthetic-file-patch", + action_types: ["SYNTHETIC_FILE_PATCH"], + consequence_classes: ["CONSEQUENTIAL"], + prepare_side_effect_free: true, + rollback: true, + interrupt: false, + outcome_verification: true + }) + }; + } + }); + adapter.execute = async (...args) => { + executeCalls += 1; + return runtime.adapter.execute(...args); + }; + const result = await runtime.execute({ adapter, snapshot_provider: async () => active }); + return normalizedExecution(runtime, result, { execute_calls: executeCalls }); + }); +} + +function rehash(api, record, changes) { + const { digest: ignored, ...unsigned } = record; + const changed = { ...unsigned, ...changes }; + return { ...changed, digest: api.canonicalDigest(changed) }; +} + +async function alienProtocolScenario(implementation) { + return withSyntheticRuntime(implementation, async (runtime) => { + const alien = rehash(runtime.api, runtime.adapter.capability, { + protocol: "not-cint/adapter-capability/999", + forbidden_field: true + }); + const result = runtime.api.revalidateReceipt({ + receipt: runtime.receipt, + receipt_authority: runtime.receipt_authority, + ...runtime.snapshot, + adapter_capability: alien, + now: T1 + }); + return { status: result.status, reason_codes: [...result.reason_codes].sort() }; + }); +} + +function forbiddenFieldScenario(implementation) { + const records = baseRecords(implementation); + const intent = rehash(implementation.api, records.intent, { forbidden_field: true }); + try { + implementation.api.decide({ id: "decision.equivalence.forbidden", ...records, intent, now: T1, expires_at: T2 }); + return { admitted: true, error_code: null }; + } catch (error) { + return { admitted: false, error_code: errorCode(error) }; + } +} + +async function successfulOutcomeScenario(implementation) { + return withSyntheticRuntime(implementation, async (runtime) => normalizedExecution(runtime, await runtime.execute())); +} + +async function rollbackScenario(implementation) { + return withSyntheticRuntime(implementation, async (runtime) => { + const adapter = { + id: runtime.adapter.id, + capability: runtime.adapter.capability, + prepare: runtime.adapter.prepare.bind(runtime.adapter), + execute: runtime.adapter.execute.bind(runtime.adapter), + verify: async (...args) => { + const verified = await runtime.adapter.verify(...args); + return rehash(runtime.api, verified, { status: "DIVERGED", actual_sha256: "f".repeat(64) }); + }, + rollback: runtime.adapter.rollback.bind(runtime.adapter) + }; + return normalizedExecution(runtime, await runtime.execute({ adapter })); + }); +} + +async function failClosedScenario(implementation) { + return withSyntheticRuntime(implementation, async (runtime) => { + const result = await runtime.execute({ clock: null }); + return normalizedExecution(runtime, result); + }); +} + +async function adapter01Scenario(implementation) { + const { api } = implementation; + const temporary = await mkdtemp(path.join(os.tmpdir(), "cint-equivalence-adapter01-")); + const output = path.join(temporary, "legacy-output"); + try { + const mock = path.join(implementation.demo.PROJECT_ROOT, "fixtures", "sanitized-af-g0", "mock-codex.mjs"); + await chmod(mock, 0o755); + const { spec: loadedSpec } = await implementation.policy.loadTaskSpec(implementation.demo.DEMO_SPEC); + const spec = { ...loadedSpec, delegation: { ...loadedSpec.delegation, max_concurrency: 2 } }; + const action = await implementation.codex.createCodexDelegationAction(spec); + const intent = api.createIntent({ + id: "intent.codex-equivalence.1", + principal_id: "principal.operator", + request: "Run the bounded read-only delegated review.", + action, + declared_effects: ["Execute one read-only delegated review and write local run evidence"], + context: { adapter: "legacy-af-g0" }, + uncertainties: [], + created_at: T0 + }); + const principal = api.resolvePrincipal({ + id: "principal.operator", + type: "HUMAN", + authenticated: true, + authority_chain: ["authority.codex-equivalence.1"], + attributes: {}, + resolved_at: T0 + }); + const authority = api.createAuthorityGrant({ + id: "authority.codex-equivalence.1", + principal_id: "principal.operator", + issuer_id: "principal.authority", + epoch: 1, + grants: [{ adapter: action.adapter, type: action.type, target: action.target }], + policy_ids: ["policy.codex-equivalence"], + require_rollback: false, + issued_at: T0, + not_before: T0, + expires_at: EXPIRY + }); + const policy = api.createPolicySnapshot({ + id: "policy.codex-equivalence", + version: "r0.1", + epoch: 1, + allowed_adapters: [action.adapter], + allowed_action_types: [action.type], + denied_action_types: [], + require_explicit_request: true, + require_declared_effects: true, + require_rollback_for_consequential: true, + review_on_uncertainty: true, + issued_at: T0 + }); + const adapter = new implementation.codex.CodexDelegationCintAdapter({ + spec, + output_dir: output, + codex_binary: process.execPath, + codex_args: [mock] + }); + const machine_state = api.createMachineStateSnapshot({ + id: "machine.codex-equivalence", + epoch: 1, + available: true, + state: { packet_sha256: action.target.packet_sha256, mode: "READY" }, + observed_at: T0 + }); + const snapshot = { intent, principal, authority, policy, adapter_capability: adapter.capability, machine_state }; + const decision = api.decide({ id: "decision.codex-equivalence.1", ...snapshot, now: T1, expires_at: T2 }); + const receipt_authority = new api.DecisionReceiptAuthority({ issuer_id: "cint.receipt-authority.r0", key: Buffer.alloc(32, 23) }); + const receipt = receipt_authority.issue({ + id: "receipt.codex-equivalence.1", + nonce: "codex-equivalence-receipt-0001", + decision, + issued_at: T1 + }); + const store = new api.FileReceiptStore(path.join(temporary, "receipt-store")); + await store.register(receipt, { registered_at: T1 }); + const seal_authority = new api.OutcomeSealAuthority({ issuer_id: "cint.seal-authority.r0", key: Buffer.alloc(32, 29) }); + const ledger = new api.ExecutionLedger(path.join(temporary, "evidence", "execution.jsonl")); + const result = await api.executeWithReceipt({ + receipt, + receipt_authority, + store, + snapshot_provider: async () => snapshot, + clock: { now: () => T1 }, + adapter, + seal_authority, + ledger, + at: T1 + }); + const legacy = JSON.parse(await readFile(path.join(output, "run.json"), "utf8")); + return { + execution_status: result.status, + outcome_status: result.outcome?.status ?? null, + effect_status: result.outcome?.effect_status ?? null, + store_state: (await store.inspect(receipt.id)).state, + admission: legacy.admission.result, + context_mode: legacy.admission.context_mode, + inherited_turns: legacy.context_enforcement.inherited_turns, + evidence_count: legacy.admission.evidence.length, + authority_operations: Object.values(adapter.authority_boundary), + capability: { + consequence: adapter.capability.consequence_classes, + prepare_side_effect_free: adapter.capability.prepare_side_effect_free, + outcome_verification: adapter.capability.outcome_verification + } + }; + } finally { + await rm(temporary, { recursive: true, force: true }); + } +} + +async function afTelemetryScenario(implementation) { + const output = await mkdtemp(path.join(os.tmpdir(), "cint-equivalence-af-g0-")); + try { + const result = await implementation.demo.runDemo({ outputDir: output }); + return { + verdict: result.verdict, + context_mode: result.context_mode, + fork_turns: result.fork_turns, + model_calls: result.model_calls, + incremental_tokens: result.incremental_tokens, + cached_input_tokens: result.cached_input_tokens, + fresh_input_tokens: result.fresh_input_tokens, + output_tokens: result.output_tokens, + result: result.result, + raw_cumulative_tokens: result.historical_regression.raw_cumulative_tokens, + request_local_incremental_tokens: result.historical_regression.request_local_incremental_tokens, + duplicate_usage_events_removed: result.historical_regression.duplicate_usage_events_removed, + cumulative_delta_matches_incremental: result.historical_regression.cumulative_delta_matches_incremental, + checks: result.checks + }; + } finally { + await rm(output, { recursive: true, force: true }); + } +} + +const baseline = await loadImplementation(baselineRoot); +const candidate = await loadImplementation(candidateRoot); +const baselineErrors = await sourceSurface(baselineRoot, /(CINT_[A-Z0-9_]+)/gu); +const candidateErrors = await sourceSurface(candidateRoot, /(CINT_[A-Z0-9_]+)/gu); +const baselineProtocols = await sourceSurface(baselineRoot, /["'](cint\/[a-z0-9-]+\/1)["']/gu); +const candidateProtocols = await sourceSurface(candidateRoot, /["'](cint\/[a-z0-9-]+\/1)["']/gu); +const baselineSchemas = await schemaInventory(baselineRoot); +const candidateSchemas = await schemaInventory(candidateRoot); +assert.deepEqual(candidateErrors, baselineErrors, "compiled CINT error-code surface differs from R0"); +assert.deepEqual(candidateProtocols, baselineProtocols, "compiled CINT protocol surface differs from R0"); +assert.deepEqual(candidateSchemas, baselineSchemas, "compiled CINT schema bytes differ from R0"); +const scenarios = [ + ["ADMIT", (implementation) => normalizedDecision(implementation, "ADMIT")], + ["DENY", (implementation) => normalizedDecision(implementation, "DENY")], + ["REVIEW", (implementation) => normalizedDecision(implementation, "REVIEW")], + ["RECEIPT_ISSUE_AND_VERIFICATION", receiptVector], + ["ONE_SHOT_CONSUMPTION_RACE", (implementation) => receiptStoreScenario(implementation, true)], + ["REPLAY_REJECTION", (implementation) => receiptStoreScenario(implementation, false)], + ["EXPIRY_DURING_PREPARATION", (implementation) => driftScenario(implementation, "expiry")], + ["POLICY_DRIFT", (implementation) => driftScenario(implementation, "policy")], + ["AUTHORITY_DRIFT", (implementation) => driftScenario(implementation, "authority")], + ["MACHINE_STATE_DRIFT", (implementation) => driftScenario(implementation, "machine")], + ["ADAPTER_CAPABILITY_DRIFT", (implementation) => driftScenario(implementation, "adapter")], + ["ALIEN_PROTOCOL_REJECTION", alienProtocolScenario], + ["FORBIDDEN_FIELD_REJECTION", forbiddenFieldScenario], + ["OUTCOME_VERIFICATION", successfulOutcomeScenario], + ["ROLLBACK_AND_RESTORED_HASH", rollbackScenario], + ["FAIL_CLOSED_DEPENDENCY_CHECKS", failClosedScenario], + ["ADAPTER_01_EVIDENCE_ADMISSION", adapter01Scenario], + ["AF_G0_TELEMETRY_REGRESSION", afTelemetryScenario] +]; + +const results = []; +for (const [id, run] of scenarios) { + const baselineOutput = await run(baseline); + const candidateOutput = await run(candidate); + assert.deepEqual(candidateOutput, baselineOutput, `${id} differs from v0.1.0-cint-r0`); + results.push({ id, verdict: "PASS", normalized_sha256: digest(candidateOutput), output: candidateOutput }); +} + +if (artifactPath !== undefined) { + const artifact = JSON.parse(await readFile(path.resolve(artifactPath), "utf8")); + assert.equal(artifact.verdict, "PASS", "behavioral-equivalence artifact is not PASS"); + assert.equal(artifact.scenario_count, results.length, "behavioral-equivalence artifact count differs"); + assert.deepEqual( + artifact.scenarios.map(({ id, normalized_sha256 }) => ({ id, normalized_sha256 })), + results.map(({ id, normalized_sha256 }) => ({ id, normalized_sha256 })), + "behavioral-equivalence artifact hashes differ from the current run" + ); + assert.equal(artifact.frozen_surfaces.runtime_schema_files_identical, `${Object.keys(candidateSchemas).length}/${Object.keys(baselineSchemas).length}`); + assert.equal(artifact.frozen_surfaces.protocol_identifier_set, `${candidateProtocols.length}/${baselineProtocols.length} identical`); + assert.equal(artifact.frozen_surfaces.error_code_set, `${candidateErrors.length}/${baselineErrors.length} identical`); +} + +console.log(JSON.stringify({ + protocol: "cint-r1/behavioral-equivalence/1", + baseline: "v0.1.0-cint-r0", + candidate: "cint-r1-typescript", + normalization: { + excluded: ["generated UUIDs", "temporary absolute paths", "runtime-generated timestamps"], + retained: ["statuses", "error codes", "canonical digests", "HMAC signature", "store states", "target hashes", "AF-G0 counters"] + }, + scenario_count: results.length, + passed: results.length, + failed: 0, + artifact_verification: artifactPath === undefined ? "NOT_REQUESTED" : "PASS", + scenarios: results, + frozen_surfaces: { + runtime_schema_files_identical: `${Object.keys(candidateSchemas).length}/${Object.keys(baselineSchemas).length}`, + protocol_identifier_set: `${candidateProtocols.length}/${baselineProtocols.length} identical`, + error_code_set: `${candidateErrors.length}/${baselineErrors.length} identical` + }, + verdict: "PASS" +}, null, 2)); diff --git a/scripts/verify-cint-import-boundaries.mjs b/scripts/verify-cint-import-boundaries.mjs new file mode 100644 index 0000000..43f8ef3 --- /dev/null +++ b/scripts/verify-cint-import-boundaries.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("../", import.meta.url)); +const source = path.join(root, "src", "cint"); +const allowed = new Map([ + ["src/cint/adapters/codex/legacy-adapter-boundary.ts", new Set(["../../../adapters/codex-delegation/index.js"])], + ["src/cint/cli.ts", new Set(["../cli.js"])] +]); +const findings = []; +let filesScanned = 0; + +async function walk(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const absolute = path.join(directory, entry.name); + if (entry.isDirectory()) await walk(absolute); + if (!entry.isFile() || !entry.name.endsWith(".ts")) continue; + filesScanned += 1; + const relative = path.relative(root, absolute).split(path.sep).join("/"); + const text = await readFile(absolute, "utf8"); + for (const match of text.matchAll(/(?:from\s+|import\s*\()["']([^"']+)["']/gu)) { + const specifier = match[1]; + if (!specifier) continue; + const crossesLegacy = specifier.includes("/adapters/") || specifier === "../cli.js" || specifier.endsWith("/util.js"); + if (!crossesLegacy) continue; + if (!allowed.get(relative)?.has(specifier)) findings.push({ file: relative, specifier }); + } + } +} + +await walk(source); +assert.equal(findings.length, 0, JSON.stringify(findings)); +console.log(JSON.stringify({ + gate: "CINT-R1-TYPED-LEGACY-IMPORT-BOUNDARY", + verdict: "PASS", + files_scanned: filesScanned, + typed_adapter_boundary: "src/cint/adapters/codex/legacy-adapter-boundary.ts", + lazy_legacy_cli_boundary: "src/cint/cli.ts", + findings +}, null, 2)); diff --git a/scripts/verify-cint-schema-package.mjs b/scripts/verify-cint-schema-package.mjs index 4d64fcf..24bcc3b 100644 --- a/scripts/verify-cint-schema-package.mjs +++ b/scripts/verify-cint-schema-package.mjs @@ -2,14 +2,23 @@ import { spawnSync } from "node:child_process"; import { readdir, readFile } from "node:fs/promises"; import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import { CINT_SCHEMA_PROTOCOLS } from "../src/cint/schema.js"; +import { fileURLToPath, pathToFileURL } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const schemaDirectory = path.join(root, "schemas", "cint"); const failures = []; +const build = spawnSync(process.execPath, [path.join(root, "scripts", "build.mjs")], { + cwd: root, + stdio: "inherit" +}); +if (build.status !== 0) failures.push({ code: "TYPESCRIPT_BUILD_FAILED", status: build.status }); + +let CINT_SCHEMA_PROTOCOLS = []; +if (build.status === 0) { + ({ CINT_SCHEMA_PROTOCOLS } = await import(pathToFileURL(path.join(root, "dist", "src", "cint", "schema.js")).href)); +} + const directorySchemas = (await readdir(schemaDirectory)) .filter((name) => name.endsWith(".schema.json")) .sort(); @@ -63,7 +72,7 @@ if (pack.status !== 0) { const packageResult = JSON.parse(pack.stdout); packagedSchemas = (packageResult[0]?.files ?? []) .map(({ path: packagedPath }) => packagedPath) - .filter((packagedPath) => packagedPath.startsWith("schemas/cint/") && packagedPath.endsWith(".schema.json")) + .filter((packagedPath) => packagedPath.startsWith("dist/schemas/cint/") && packagedPath.endsWith(".schema.json")) .map((packagedPath) => path.posix.basename(packagedPath)) .sort(); } catch { diff --git a/scripts/verify-decision-parity.mjs b/scripts/verify-decision-parity.mjs new file mode 100644 index 0000000..0c93bb3 --- /dev/null +++ b/scripts/verify-decision-parity.mjs @@ -0,0 +1,130 @@ +import assert from "node:assert/strict"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const [baselineRoot, candidateRoot] = process.argv.slice(2); +assert.ok(baselineRoot && candidateRoot, "usage: verify-decision-parity.mjs "); + +const T0 = "2026-08-27T00:00:00.000Z"; +const T1 = "2026-08-27T00:01:00.000Z"; +const T2 = "2026-08-27T00:10:00.000Z"; +const EXPIRY = "2026-08-27T01:00:00.000Z"; +const TARGET = Object.freeze({ path: "sandbox/target.txt" }); + +async function loadApi(root) { + const entry = pathToFileURL(path.join(path.resolve(root), "src", "cint", "index.js")).href; + return import(entry); +} + +function records(api, intentOverrides = {}) { + const intent = api.createIntent({ + id: "intent.demo.1", + principal_id: "principal.operator", + request: "Replace the disposable synthetic target with the declared content.", + action: { + adapter: "cint.adapter.synthetic-file-patch", + type: "SYNTHETIC_FILE_PATCH", + target: TARGET, + parameters: { content: "after", expected_before_sha256: "0".repeat(64) }, + consequence: "CONSEQUENTIAL" + }, + declared_effects: ["Replace bytes in the declared disposable target"], + context: { workspace: "synthetic-proof" }, + uncertainties: [], + created_at: T0, + ...intentOverrides + }); + const principal = api.resolvePrincipal({ + id: "principal.operator", + type: "HUMAN", + authenticated: true, + authority_chain: ["authority.demo.1"], + attributes: { role: "operator" }, + resolved_at: T0 + }); + const authority = api.createAuthorityGrant({ + id: "authority.demo.1", + principal_id: "principal.operator", + issuer_id: "principal.authority", + epoch: 1, + grants: [{ adapter: "cint.adapter.synthetic-file-patch", type: "SYNTHETIC_FILE_PATCH", target: TARGET }], + policy_ids: ["policy.demo"], + require_rollback: true, + issued_at: T0, + not_before: T0, + expires_at: EXPIRY + }); + const policy = api.createPolicySnapshot({ + id: "policy.demo", + version: "r0.1", + epoch: 1, + allowed_adapters: ["cint.adapter.synthetic-file-patch"], + allowed_action_types: ["SYNTHETIC_FILE_PATCH"], + denied_action_types: [], + require_explicit_request: true, + require_declared_effects: true, + require_rollback_for_consequential: true, + review_on_uncertainty: true, + issued_at: T0 + }); + const adapter_capability = api.createAdapterCapability({ + id: "cint.adapter.synthetic-file-patch", + action_types: ["SYNTHETIC_FILE_PATCH"], + consequence_classes: ["CONSEQUENTIAL"], + prepare_side_effect_free: true, + rollback: true, + interrupt: true, + outcome_verification: true + }); + const machine_state = api.createMachineStateSnapshot({ + id: "machine.synthetic", + epoch: 1, + available: true, + state: { mode: "READY", target_locked: false }, + observed_at: T0 + }); + return { intent, principal, authority, policy, adapter_capability, machine_state }; +} + +function decision(api, values, id) { + return api.decide({ id, ...values, now: T1, expires_at: T2 }); +} + +async function vector(root) { + const api = await loadApi(root); + const admit = decision(api, records(api), "decision.parity.admit"); + const receiptAuthority = new api.DecisionReceiptAuthority({ + issuer_id: "cint.receipt-authority.parity", + key: Buffer.alloc(32, 7) + }); + const receipt = receiptAuthority.issue({ + id: "receipt.parity.issued", + nonce: "receipt-parity-nonce-0001", + decision: admit, + issued_at: T1 + }); + receiptAuthority.verify(receipt, { now: T1 }); + return { + admit, + deny: decision(api, records(api, { request: null }), "decision.parity.deny"), + review: decision(api, records(api, { uncertainties: ["operator confirmation pending"] }), "decision.parity.review"), + receipt + }; +} + +const baseline = await vector(baselineRoot); +const candidate = await vector(candidateRoot); +assert.deepEqual(candidate, baseline, "compiled decision vectors differ from v0.1.0-cint-r0"); + +console.log(JSON.stringify({ + gate: "CINT-R1-GOLDEN-AUTHORITY-PARITY", + verdict: "PASS", + scenarios: ["ADMIT", "DENY", "REVIEW", "RECEIPT_ISSUE_AND_VERIFY"], + digests: { + admit: candidate.admit.digest, + deny: candidate.deny.digest, + review: candidate.review.digest, + receipt: candidate.receipt.digest + }, + receipt_signature: candidate.receipt.signature +}, null, 2)); diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs new file mode 100644 index 0000000..209b402 --- /dev/null +++ b/scripts/verify-package.mjs @@ -0,0 +1,92 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { readFile, readdir } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { readNpmPackReport, spawnNpmPack } from "./npm-pack-launch.mjs"; + +const root = fileURLToPath(new URL("../", import.meta.url)); +const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")); +assert.equal(packageJson.private, true, "package must remain private"); + +const build = spawnSync(process.execPath, [fileURLToPath(new URL("./build.mjs", import.meta.url))], { + cwd: root, + stdio: "inherit" +}); +assert.equal(build.status, 0, "package verification build failed"); + +const { invocation: npmPackInvocation, result: packed } = spawnNpmPack({ cwd: root }); +const report = readNpmPackReport(packed); +const files = new Set(report[0].files.map((entry) => entry.path)); + +function targets(value) { + if (typeof value === "string") return [value]; + if (value && typeof value === "object") return Object.values(value).flatMap(targets); + return []; +} + +const declaredTargets = [...new Set([ + ...targets(packageJson.exports), + ...targets(packageJson.bin), + ...targets(packageJson.types) +].map((target) => target.replace(/^\.\//u, "")))]; + +for (const target of declaredTargets) { + assert.ok(files.has(target), `declared package target is absent from dry run: ${target}`); +} + +for (const subpath of [ + "@sntllgnc/cint", + "@sntllgnc/cint/cli", + "@sntllgnc/cint/adapters/synthetic-file-patch", + "@sntllgnc/cint/adapters/codex-delegation" +]) { + await import(subpath); +} + +const declarationCheck = spawnSync(process.execPath, [ + fileURLToPath(new URL("../node_modules/typescript/bin/tsc", import.meta.url)), + "--noEmit", + "--target", "ES2022", + "--module", "NodeNext", + "--moduleResolution", "NodeNext", + "--types", "node", + "--strict", + "--skipLibCheck", "false", + "tests/package-exports.consumer.ts" +], { + cwd: root, + encoding: "utf8" +}); +assert.equal(declarationCheck.status, 0, declarationCheck.stdout + declarationCheck.stderr); + +async function sourceMaps(directory) { + const maps = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + const absolute = path.join(directory, entry.name); + if (entry.isDirectory()) maps.push(...await sourceMaps(absolute)); + if (entry.isFile() && entry.name.endsWith(".map")) maps.push(absolute); + } + return maps; +} + +const maps = await sourceMaps(path.join(root, "dist")); +for (const map of maps) { + const content = await readFile(map, "utf8"); + assert.doesNotMatch(content, /\/(?:Users|home|workspaces)\//u, `source map contains a private absolute path: ${map}`); + assert.doesNotMatch(content, /[A-Za-z]:\\(?:Users|home|workspaces)\\/u, `source map contains a private absolute path: ${map}`); +} + +console.log(JSON.stringify({ + gate: "CINT-R1-PACKAGE-EXPORTS", + verdict: "PASS", + private: true, + npm_launch_mode: npmPackInvocation.mode, + file_count: files.size, + declared_targets: declaredTargets.sort(), + runtime_exports_resolved: 4, + declaration_exports_resolved: 4, + source_maps_checked: maps.length, + private_absolute_paths: 0 +}, null, 2)); diff --git a/scripts/verify-runtime-support.mjs b/scripts/verify-runtime-support.mjs new file mode 100644 index 0000000..c231b68 --- /dev/null +++ b/scripts/verify-runtime-support.mjs @@ -0,0 +1,173 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; + +const root = new URL("../", import.meta.url); +const expectedEngine = "^22.0.0 || ^24.0.0 || ^26.0.0"; +const supportedMajors = Object.freeze([22, 24, 26]); +const aggregateChecks = Object.freeze([ + "CINT-R0 remote verification", + "CINT-R1 TypeScript verification" +]); + +async function text(relativePath) { + return readFile(new URL(relativePath, root), "utf8"); +} + +function acceptsMajor(major) { + return supportedMajors.includes(major); +} + +function workflowJobs(workflow) { + const jobs = []; + let inJobs = false; + let current = null; + for (const line of workflow.split("\n")) { + if (line === "jobs:") { + inJobs = true; + continue; + } + if (!inJobs) continue; + const job = /^ ([a-z0-9-]+):$/u.exec(line); + if (job) { + current = { id: job[1], lines: [] }; + jobs.push(current); + continue; + } + if (/^\S/u.test(line)) break; + if (current !== null) current.lines.push(line); + } + return jobs; +} + +function jobName(job) { + const names = job.lines + .map((line) => /^ name: (.+)$/u.exec(line)?.[1]) + .filter((value) => value !== undefined); + assert.equal(names.length, 1, `workflow job ${job.id} must have exactly one name`); + return names[0]; +} + +function jobNeeds(job) { + const needs = []; + const start = job.lines.indexOf(" needs:"); + if (start === -1) return needs; + for (const line of job.lines.slice(start + 1)) { + const dependency = /^ - ([a-z0-9-]+)$/u.exec(line)?.[1]; + if (dependency === undefined) break; + needs.push(dependency); + } + return needs; +} + +function verifyWorkflowStructure(workflowBytes, lineEnding) { + const workflow = workflowBytes.replace(/\r\n?/gu, "\n"); + const matrixBlock = workflow.match(/\n\s{8}node:\n((?:\s{10}- "\d+"\n)+)/u)?.[1]; + assert.ok(matrixBlock, `${lineEnding} Node verification matrix is unavailable`); + const matrixMajors = [...matrixBlock.matchAll(/- "(\d+)"/gu)].map((match) => Number(match[1])); + assert.deepEqual(matrixMajors, supportedMajors, `${lineEnding} CI must test exactly Node 22, 24, and 26`); + + const jobs = workflowJobs(workflow).map((job) => ({ + id: job.id, + name: jobName(job), + needs: jobNeeds(job) + })); + for (const aggregate of aggregateChecks) { + const matches = jobs.filter((job) => job.name === aggregate); + assert.equal(matches.length, 1, `${lineEnding} aggregate name must occur exactly once: ${aggregate}`); + assert.deepEqual( + matches[0].needs, + ["verification-matrix"], + `${lineEnding} aggregate must depend solely on verification-matrix: ${aggregate}` + ); + } + assert.deepEqual( + jobs + .filter((job) => job.needs.includes("verification-matrix")) + .map((job) => job.name) + .sort(), + [...aggregateChecks].sort(), + `${lineEnding} only the two exact aggregate checks may depend on verification-matrix` + ); + return matrixMajors; +} + +const packageJson = JSON.parse(await text("package.json")); +const packageLock = JSON.parse(await text("package-lock.json")); +const nvmrc = (await text(".nvmrc")).trim(); +const nodeVersion = (await text(".node-version")).trim(); +const npmrc = (await text(".npmrc")).trim(); +const workflowBytes = await text(".github/workflows/cint-r0-verification.yml"); + +assert.equal(packageJson.engines?.node, expectedEngine, "package engine policy drifted"); +assert.equal(packageLock.packages?.[""]?.engines?.node, expectedEngine, "lockfile engine policy drifted"); +assert.equal(nvmrc, "24", ".nvmrc must select Node 24"); +assert.equal(nodeVersion, "24", ".node-version must select Node 24"); +assert.equal(npmrc, "engine-strict=true", ".npmrc must enforce engine compatibility"); + +const lfWorkflow = workflowBytes.replace(/\r\n?/gu, "\n"); +const crlfWorkflow = lfWorkflow.replace(/\n/gu, "\r\n"); +const matrixMajors = verifyWorkflowStructure(lfWorkflow, "LF"); +assert.deepEqual( + verifyWorkflowStructure(crlfWorkflow, "CRLF"), + matrixMajors, + "LF and CRLF workflow bytes must produce identical verification results" +); +assert.throws( + () => verifyWorkflowStructure( + lfWorkflow.replace( + " name: CINT-R0 remote verification\n", + " name: CINT-R0 remote verification weakened\n" + ), + "RENAMED" + ), + /aggregate name must occur exactly once/u, + "aggregate name suffixes must not satisfy exact-name enforcement" +); +assert.throws( + () => verifyWorkflowStructure( + lfWorkflow.replace( + " name: CINT-R1 TypeScript verification\n if: ${{ always() }}\n needs:\n - verification-matrix\n", + " name: CINT-R1 TypeScript verification\n if: ${{ always() }}\n needs:\n - unrelated-check\n" + ), + "UNBOUND" + ), + /aggregate must depend solely on verification-matrix/u, + "aggregate names must remain bound to their own matrix dependency" +); + +const samples = Object.freeze({ + "20.0.0": acceptsMajor(20), + "22.0.0": acceptsMajor(22), + "24.0.0": acceptsMajor(24), + "25.0.0": acceptsMajor(25), + "26.0.0": acceptsMajor(26) +}); +assert.deepEqual(samples, { + "20.0.0": false, + "22.0.0": true, + "24.0.0": true, + "25.0.0": false, + "26.0.0": true +}); + +const currentMajor = Number(process.versions.node.split(".")[0]); +assert.ok(acceptsMajor(currentMajor), `current Node ${process.version} is outside the supported/tested majors`); + +console.log(JSON.stringify({ + gate: "CINT-R1-RUNTIME-SUPPORT", + verdict: "PASS", + normative_major: 24, + temporary_compatibility_major: 22, + forward_compatibility_major: 26, + retired_major: 20, + engine: expectedEngine, + current_runtime: process.version, + matrix: matrixMajors, + aggregate_checks: aggregateChecks, + line_ending_regressions: { + LF: "PASS", + CRLF: "PASS" + }, + aggregate_binding_negative_regressions: "PASS", + samples +}, null, 2)); diff --git a/scripts/verify.mjs b/scripts/verify.mjs new file mode 100644 index 0000000..3f1e3fe --- /dev/null +++ b/scripts/verify.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; + +const npmCli = process.env.npm_execpath; + +function npm(args, label) { + const command = npmCli === undefined + ? (process.platform === "win32" ? "npm.cmd" : "npm") + : process.execPath; + const commandArgs = npmCli === undefined ? args : [npmCli, ...args]; + const result = spawnSync(command, commandArgs, { + stdio: "inherit", + shell: false + }); + assert.equal(result.status, 0, `${label} failed`); +} + +const gates = [ + ["runtime:verify", "runtime support policy"], + ["scan:escapes", "TypeScript escape-hatch scan"], + ["scan:boundaries", "typed legacy import-boundary scan"], + ["typecheck", "strict TypeScript typecheck"], + ["build", "compiled build"], + ["schema:verify", "runtime schema package verification"], + ["test:cint", "compiled CINT tests"], + ["test:legacy", "legacy tests"], + ["demo", "AF-G0 deterministic demo"] +]; + +for (const [script, label] of gates) npm(["run", script], label); +npm(["audit"], "dependency audit"); +for (const [script, label] of [ + ["public:audit", "publication audit"], + ["evidence:verify", "evidence verification"], + ["pack:verify", "package and export dry run"] +]) npm(["run", script], label); diff --git a/src/adapters/codex-delegation/index.d.ts b/src/adapters/codex-delegation/index.d.ts new file mode 100644 index 0000000..f7dce5e --- /dev/null +++ b/src/adapters/codex-delegation/index.d.ts @@ -0,0 +1,10 @@ +export function createChildPacket(spec: unknown): Promise; + +export interface LegacyRunInput { + readonly spec: unknown; + readonly outputDir: string; + readonly codexBinary?: string | undefined; + readonly codexArgs?: readonly string[] | undefined; +} + +export function runGovernedChild(input: LegacyRunInput): Promise; diff --git a/src/cint/adapters/codex/index.js b/src/cint/adapters/codex/index.js deleted file mode 100644 index ac68e08..0000000 --- a/src/cint/adapters/codex/index.js +++ /dev/null @@ -1,138 +0,0 @@ -import path from "node:path"; - -import { - CODEX_DELEGATION_ADAPTER, - createChildPacket, - runGovernedChild -} from "../../../adapters/codex-delegation/index.js"; -import { canonicalJson, sha256 } from "../../../util.js"; -import { - CintError, - assertCint, - assertExactKeys, - canonicalDigest, - immutableRecord, - isoInstant, - sealRecord, - verifyProtocolRecord, - verifySealedRecord -} from "../../canonical.js"; -import { createAdapterCapability } from "../../challenge.js"; - -export function codexDelegationTaskDigest(spec) { - const safeBinding = { - protocol: "cint/codex-delegation-task-binding/1", - version: spec.version, - root_task_id: spec.root_task_id, - worker: spec.worker, - admission: spec.admission, - delegation: spec.delegation, - repository_boundary_digest: sha256(spec.repository_boundary) - }; - return canonicalDigest(safeBinding); -} - -export async function createCodexDelegationAction(spec) { - const packetRecord = await createChildPacket(spec); - return immutableRecord({ - adapter: "cint.adapter.codex-delegation", - type: "CODEX_DELEGATED_REVIEW", - target: { - packet_sha256: packetRecord.packet_sha256 - }, - parameters: { - legacy_task_digest: codexDelegationTaskDigest(spec) - }, - consequence: "READ_ONLY" - }); -} - -export class CodexDelegationCintAdapter { - constructor(input) { - assertExactKeys(input, ["spec", "output_dir"], ["codex_args", "codex_binary"], "Codex adapter configuration"); - assertCint( - input.codex_args === undefined || - (Array.isArray(input.codex_args) && input.codex_args.every((value) => typeof value === "string" && value.length > 0)), - "CINT_CODEX_ARGS_INVALID", - "Codex adapter argument prefix must contain non-empty strings" - ); - this.id = "cint.adapter.codex-delegation"; - this.spec = input.spec; - this.output_dir = path.resolve(input.output_dir); - this.codex_binary = input.codex_binary; - this.codex_args = Object.freeze([...(input.codex_args ?? [])]); - this.capability = createAdapterCapability({ - id: this.id, - action_types: ["CODEX_DELEGATED_REVIEW"], - consequence_classes: ["READ_ONLY"], - prepare_side_effect_free: true, - rollback: false, - interrupt: false, - outcome_verification: true - }); - this.authority_boundary = CODEX_DELEGATION_ADAPTER.authority_boundary; - } - - async prepare(intent, options) { - verifyProtocolRecord(intent, "cint/intent/1", "intent"); - assertCint(intent.action.adapter === this.id, "CINT_ADAPTER_MISMATCH", "Intent does not target the Codex delegation adapter"); - assertCint(intent.action.type === "CODEX_DELEGATED_REVIEW", "CINT_ADAPTER_ACTION_UNSUPPORTED", "Codex adapter supports delegated review only"); - assertCint(intent.action.consequence === "READ_ONLY", "CINT_ADAPTER_CONSEQUENCE_UNSUPPORTED", "Legacy Codex adapter is read-only"); - assertExactKeys(intent.action.target, ["packet_sha256"], [], "Codex adapter target"); - assertExactKeys(intent.action.parameters, ["legacy_task_digest"], [], "Codex adapter parameters"); - const packetRecord = await createChildPacket(this.spec); - const taskDigest = codexDelegationTaskDigest(this.spec); - assertCint(packetRecord.packet_sha256 === intent.action.target.packet_sha256, "CINT_CODEX_PACKET_CHANGED", "Legacy child packet changed after decision"); - assertCint(taskDigest === intent.action.parameters.legacy_task_digest, "CINT_CODEX_TASK_CHANGED", "Legacy task changed after decision"); - return Object.freeze({ - packet_record: packetRecord, - task_digest: taskDigest, - prepared_at: isoInstant(options.at, "Codex adapter preparation time") - }); - } - - async execute(prepared, options) { - if (options.signal?.aborted) throw new CintError("CINT_EXECUTION_INTERRUPTED", "Codex delegation was interrupted before execution"); - const run = await runGovernedChild({ - spec: this.spec, - outputDir: this.output_dir, - codexBinary: this.codex_binary, - codexArgs: this.codex_args - }); - return sealRecord({ - protocol: "cint/codex-delegation-execution/1", - adapter_id: this.id, - action_type: "CODEX_DELEGATED_REVIEW", - legacy_packet_sha256: prepared.packet_record.packet_sha256, - legacy_task_digest: prepared.task_digest, - legacy_run_digest: sha256(canonicalJson(run)), - legacy_result: run.admission.result, - context_mode: run.admission.context_mode, - model_calls: run.admission.model_calls, - incremental_tokens: run.admission.incremental_tokens, - executed_at: isoInstant(options.at, "Codex adapter execution time") - }); - } - - async verify(prepared, execution, options) { - verifySealedRecord(execution, "Codex adapter execution"); - const matched = - execution.adapter_id === this.id && - execution.legacy_packet_sha256 === prepared.packet_record.packet_sha256 && - execution.legacy_task_digest === prepared.task_digest && - ["ADMITTED", "REJECTED"].includes(execution.legacy_result) && - execution.context_mode === "clean"; - return sealRecord({ - protocol: "cint/outcome-verification/1", - status: matched ? "VERIFIED" : "DIVERGED", - target: "codex-delegation-packet", - expected_sha256: prepared.packet_record.packet_sha256, - actual_sha256: matched ? execution.legacy_packet_sha256 : "0".repeat(64), - checked_at: isoInstant(options.at, "Codex adapter verification time") - }); - } - - async rollback() { - throw new CintError("CINT_ROLLBACK_UNSUPPORTED", "Read-only Codex delegation has no rollback operation"); - } -} diff --git a/src/cint/adapters/codex/index.ts b/src/cint/adapters/codex/index.ts new file mode 100644 index 0000000..30b9931 --- /dev/null +++ b/src/cint/adapters/codex/index.ts @@ -0,0 +1,190 @@ +import path from "node:path"; + +import { + CintError, + assertCint, + assertExactKeys, + boundedString, + canonicalDigest, + canonicalJson, + identifier, + immutableRecord, + isPlainRecord, + isoInstant, + sealRecord, + sha256, + verifyProtocolRecord, + verifySealedRecord +} from "../../canonical.js"; +import { createAdapterCapability } from "../../challenge.js"; +import { verifyOutcomeVerification } from "../../outcome.js"; +import { + executeLegacyCandidate, + LEGACY_ADAPTER_01_AUTHORITY_BOUNDARY, + prepareLegacyPacket +} from "./legacy-adapter-boundary.js"; +import type { + AdapterId, + CanonicalInstant, + Identifier, + Sha256Digest, + TargetDigest +} from "../../types/brands.js"; +import type { AdapterOperationOptions } from "../../types/adapters.js"; +import type { + AdapterCapability, + AdapterExecution, + CintAction, + IntentRecord, + OutcomeVerification +} from "../../types/records.js"; + +export interface CodexDelegationPreparedAction { + readonly packet_sha256: TargetDigest; + readonly task_digest: Sha256Digest; + readonly prepared_at: CanonicalInstant; +} + +function specField(spec: unknown, name: string): unknown { + assertCint(isPlainRecord(spec), "CINT_CODEX_TASK_CHANGED", "Codex delegation specification must be an object"); + return spec[name]; +} + +export function codexDelegationTaskDigest(spec: unknown): Sha256Digest { + const repositoryBoundary = boundedString( + specField(spec, "repository_boundary"), + "Codex delegation repository boundary", + { minimum: 1, maximum: 4096 } + ); + const safeBinding = { + protocol: "cint/codex-delegation-task-binding/1", + version: specField(spec, "version"), + root_task_id: specField(spec, "root_task_id"), + worker: specField(spec, "worker"), + admission: specField(spec, "admission"), + delegation: specField(spec, "delegation"), + repository_boundary_digest: sha256(repositoryBoundary) + }; + return canonicalDigest(safeBinding); +} + +export async function createCodexDelegationAction(spec: unknown): Promise { + const packet = await prepareLegacyPacket(spec); + return immutableRecord({ + adapter: identifier("cint.adapter.codex-delegation", "Codex adapter id"), + type: identifier("CODEX_DELEGATED_REVIEW", "Codex action type"), + target: { + packet_sha256: packet.packet_sha256 + }, + parameters: { + legacy_task_digest: codexDelegationTaskDigest(spec) + }, + consequence: "READ_ONLY" as const + }); +} + +export class CodexDelegationCintAdapter { + readonly id: AdapterId; + readonly spec: unknown; + readonly output_dir: string; + readonly codex_binary: string | undefined; + readonly codex_args: readonly string[]; + readonly capability: AdapterCapability; + readonly authority_boundary = LEGACY_ADAPTER_01_AUTHORITY_BOUNDARY; + + constructor(value: unknown) { + const input = assertExactKeys(value, ["spec", "output_dir"], ["codex_args", "codex_binary"], "Codex adapter configuration"); + const rawArgs = input["codex_args"]; + assertCint( + rawArgs === undefined || + (Array.isArray(rawArgs) && rawArgs.every((item) => typeof item === "string" && item.length > 0)), + "CINT_CODEX_ARGS_INVALID", + "Codex adapter argument prefix must contain non-empty strings" + ); + const rawBinary = input["codex_binary"]; + assertCint(rawBinary === undefined || typeof rawBinary === "string", "CINT_CODEX_ARGS_INVALID", "Codex binary must be a string"); + this.id = identifier("cint.adapter.codex-delegation", "Codex adapter id"); + this.spec = input["spec"]; + this.output_dir = path.resolve(boundedString(input["output_dir"], "Codex adapter output directory", { maximum: 4096 })); + this.codex_binary = rawBinary; + this.codex_args = Object.freeze(rawArgs === undefined ? [] : [...rawArgs]); + this.capability = createAdapterCapability({ + id: this.id, + action_types: ["CODEX_DELEGATED_REVIEW"], + consequence_classes: ["READ_ONLY"], + prepare_side_effect_free: true, + rollback: false, + interrupt: false, + outcome_verification: true + }); + } + + prepare(intent: IntentRecord, options: AdapterOperationOptions): Promise; + async prepare(value: unknown, options: AdapterOperationOptions): Promise { + const intent = verifyProtocolRecord(value, "cint/intent/1", "intent"); + assertCint(intent.action.adapter === this.id, "CINT_ADAPTER_MISMATCH", "Intent does not target the Codex delegation adapter"); + assertCint(intent.action.type === "CODEX_DELEGATED_REVIEW", "CINT_ADAPTER_ACTION_UNSUPPORTED", "Codex adapter supports delegated review only"); + assertCint(intent.action.consequence === "READ_ONLY", "CINT_ADAPTER_CONSEQUENCE_UNSUPPORTED", "Legacy Codex adapter is read-only"); + const target = assertExactKeys(intent.action.target, ["packet_sha256"], [], "Codex adapter target"); + const parameters = assertExactKeys(intent.action.parameters, ["legacy_task_digest"], [], "Codex adapter parameters"); + const packet = await prepareLegacyPacket(this.spec); + const taskDigest = codexDelegationTaskDigest(this.spec); + assertCint(packet.packet_sha256 === target["packet_sha256"], "CINT_CODEX_PACKET_CHANGED", "Legacy child packet changed after decision"); + assertCint(taskDigest === parameters["legacy_task_digest"], "CINT_CODEX_TASK_CHANGED", "Legacy task changed after decision"); + return Object.freeze({ + packet_sha256: packet.packet_sha256, + task_digest: taskDigest, + prepared_at: isoInstant(options.at, "Codex adapter preparation time") + }); + } + + async execute(prepared: CodexDelegationPreparedAction, options: AdapterOperationOptions): Promise { + if (options.signal?.aborted) throw new CintError("CINT_EXECUTION_INTERRUPTED", "Codex delegation was interrupted before execution"); + const run = await executeLegacyCandidate({ + spec: this.spec, + output_dir: this.output_dir, + codex_binary: this.codex_binary, + codex_args: this.codex_args + }); + return sealRecord({ + protocol: "cint/codex-delegation-execution/1" as const, + adapter_id: this.id, + action_type: identifier("CODEX_DELEGATED_REVIEW", "Codex action type"), + legacy_packet_sha256: prepared.packet_sha256, + legacy_task_digest: prepared.task_digest, + legacy_run_digest: sha256(canonicalJson(run.record)), + legacy_result: run.result, + context_mode: run.context_mode, + model_calls: run.model_calls, + incremental_tokens: run.incremental_tokens, + executed_at: isoInstant(options.at, "Codex adapter execution time") + }); + } + + async verify( + prepared: CodexDelegationPreparedAction, + execution: unknown, + options: AdapterOperationOptions + ): Promise { + const record = verifySealedRecord(execution, "Codex adapter execution"); + const matched = + record["protocol"] === "cint/codex-delegation-execution/1" && + record["adapter_id"] === this.id && + record["legacy_packet_sha256"] === prepared.packet_sha256 && + record["legacy_task_digest"] === prepared.task_digest && + (record["legacy_result"] === "ADMITTED" || record["legacy_result"] === "REJECTED") && + record["context_mode"] === "clean"; + return verifyOutcomeVerification(sealRecord({ + protocol: "cint/outcome-verification/1" as const, + status: matched ? "VERIFIED" as const : "DIVERGED" as const, + target: "codex-delegation-packet", + expected_sha256: prepared.packet_sha256, + actual_sha256: matched ? prepared.packet_sha256 : "0".repeat(64), + checked_at: isoInstant(options.at, "Codex adapter verification time") + })); + } + + async rollback(): Promise { + throw new CintError("CINT_ROLLBACK_UNSUPPORTED", "Read-only Codex delegation has no rollback operation"); + } +} diff --git a/src/cint/adapters/codex/legacy-adapter-boundary.ts b/src/cint/adapters/codex/legacy-adapter-boundary.ts new file mode 100644 index 0000000..0a55845 --- /dev/null +++ b/src/cint/adapters/codex/legacy-adapter-boundary.ts @@ -0,0 +1,84 @@ +import { + createChildPacket, + runGovernedChild +} from "../../../adapters/codex-delegation/index.js"; + +import { + assertCint, + assertJsonValue, + integer, + isPlainRecord, + sha256Digest +} from "../../canonical.js"; +import type { TargetDigest } from "../../types/brands.js"; +import type { JsonValue } from "../../types/protocols.js"; + +export const LEGACY_ADAPTER_01_AUTHORITY_BOUNDARY = Object.freeze({ + may_mint_decision: false, + may_issue_receipt: false, + may_consume_receipt: false, + may_self_admit: false, + may_bypass_revalidation: false, + may_seal_outcome: false +} as const); + +export interface LegacyPacketCandidate { + readonly packet_sha256: TargetDigest; +} + +export interface LegacyRunCandidate { + readonly record: JsonValue; + readonly result: "ADMITTED" | "REJECTED"; + readonly context_mode: string; + readonly model_calls: number; + readonly incremental_tokens: number | null; +} + +export interface LegacyExecutionInput { + readonly spec: unknown; + readonly output_dir: string; + readonly codex_binary?: string | undefined; + readonly codex_args?: readonly string[] | undefined; +} + +function requiredRecord(value: unknown, label: string): Readonly> { + assertCint(isPlainRecord(value), "CINT_FAIL_CLOSED", `${label} must be an object`); + return value; +} + +export async function prepareLegacyPacket(spec: unknown): Promise { + const packet = requiredRecord(await createChildPacket(spec), "Legacy child packet"); + return Object.freeze({ + packet_sha256: sha256Digest(packet["packet_sha256"], "legacy packet sha256") + }); +} + +export async function executeLegacyCandidate(input: LegacyExecutionInput): Promise { + const raw = await runGovernedChild({ + spec: input.spec, + outputDir: input.output_dir, + codexBinary: input.codex_binary, + codexArgs: input.codex_args + }); + const record = requiredRecord(raw, "Legacy Adapter 01 run"); + const admission = requiredRecord(record["admission"], "Legacy Adapter 01 admission"); + const result = admission["result"]; + assertCint( + result === "ADMITTED" || result === "REJECTED", + "CINT_FAIL_CLOSED", + "Legacy Adapter 01 returned an invalid admission result" + ); + const contextMode = admission["context_mode"]; + assertCint(typeof contextMode === "string", "CINT_FAIL_CLOSED", "Legacy Adapter 01 context mode is invalid"); + const rawIncrementalTokens = admission["incremental_tokens"]; + const incrementalTokens = rawIncrementalTokens === null + ? null + : integer(rawIncrementalTokens, "Legacy Adapter 01 incremental tokens"); + return Object.freeze({ + record: assertJsonValue(raw, "Legacy Adapter 01 run"), + result, + context_mode: contextMode, + model_calls: integer(admission["model_calls"], "Legacy Adapter 01 model calls"), + incremental_tokens: incrementalTokens + }); +} diff --git a/src/cint/adapters/synthetic-file-patch.js b/src/cint/adapters/synthetic-file-patch.js deleted file mode 100644 index fb418c9..0000000 --- a/src/cint/adapters/synthetic-file-patch.js +++ /dev/null @@ -1,127 +0,0 @@ -import { open, readFile, realpath, rename, stat, unlink } from "node:fs/promises"; -import path from "node:path"; -import { randomUUID } from "node:crypto"; - -import { resolveInside, sha256 } from "../../util.js"; -import { - CintError, - assertCint, - assertExactKeys, - boundedString, - isoInstant, - sealRecord, - sha256Digest, - verifyProtocolRecord, - verifySealedRecord -} from "../canonical.js"; -import { createAdapterCapability } from "../challenge.js"; - -async function atomicWrite(filePath, bytes, mode) { - const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.cint.tmp`); - let handle; - try { - handle = await open(temporary, "wx", mode); - await handle.writeFile(bytes); - await handle.sync(); - await handle.close(); - handle = null; - await rename(temporary, filePath); - } finally { - if (handle) await handle.close().catch(() => {}); - await unlink(temporary).catch(() => {}); - } -} - -export class SyntheticFilePatchAdapter { - constructor(root) { - this.id = "cint.adapter.synthetic-file-patch"; - this.root = path.resolve(root); - this.capability = createAdapterCapability({ - id: this.id, - action_types: ["SYNTHETIC_FILE_PATCH"], - consequence_classes: ["CONSEQUENTIAL"], - prepare_side_effect_free: true, - rollback: true, - interrupt: true, - outcome_verification: true - }); - } - - async prepare(intent, options) { - verifyProtocolRecord(intent, "cint/intent/1", "intent"); - assertCint(intent.action.adapter === this.id, "CINT_ADAPTER_MISMATCH", "Intent does not target the synthetic adapter"); - assertCint(intent.action.type === "SYNTHETIC_FILE_PATCH", "CINT_ADAPTER_ACTION_UNSUPPORTED", "Synthetic adapter supports only file patch actions"); - assertExactKeys(intent.action.target, ["path"], [], "synthetic target"); - assertExactKeys(intent.action.parameters, ["content", "expected_before_sha256"], [], "synthetic parameters"); - const relative = boundedString(intent.action.target.path, "synthetic target path", { minimum: 1, maximum: 1024 }); - const content = boundedString(intent.action.parameters.content, "synthetic content", { minimum: 0, maximum: 1024 * 1024 }); - const expectedBefore = sha256Digest(intent.action.parameters.expected_before_sha256, "synthetic expected_before_sha256"); - const boundary = await realpath(this.root).catch((error) => { - throw new CintError("CINT_TARGET_INVALID", "Synthetic adapter root is unavailable", { cause: error.code }); - }); - const resolved = await resolveInside(boundary, relative).catch((error) => { - throw new CintError("CINT_TARGET_INVALID", "Synthetic target is outside the available disposable boundary", { - cause: error.code - }); - }); - const metadata = await stat(resolved.absolute); - const originalBytes = await readFile(resolved.absolute); - const beforeSha256 = sha256(originalBytes); - assertCint(beforeSha256 === expectedBefore, "CINT_TARGET_STATE_CHANGED", "Synthetic target changed before execution"); - return Object.freeze({ - absolute: resolved.absolute, - relative: resolved.relative, - mode: metadata.mode & 0o777, - original_bytes: Buffer.from(originalBytes), - desired_bytes: Buffer.from(content, "utf8"), - before_sha256: beforeSha256, - after_sha256: sha256(Buffer.from(content, "utf8")), - prepared_at: isoInstant(options.at, "synthetic preparation time") - }); - } - - async execute(prepared, options) { - assertCint(options.signal?.aborted !== true, "CINT_EXECUTION_INTERRUPTED", "Synthetic action was interrupted before execution"); - const currentBytes = await readFile(prepared.absolute); - assertCint(sha256(currentBytes) === prepared.before_sha256, "CINT_TARGET_STATE_CHANGED", "Synthetic target changed after preparation"); - await atomicWrite(prepared.absolute, prepared.desired_bytes, prepared.mode); - assertCint(options.signal?.aborted !== true, "CINT_EXECUTION_INTERRUPTED", "Synthetic action was interrupted during execution"); - return sealRecord({ - protocol: "cint/synthetic-execution/1", - adapter_id: this.id, - action_type: "SYNTHETIC_FILE_PATCH", - target: prepared.relative, - before_sha256: prepared.before_sha256, - intended_after_sha256: prepared.after_sha256, - bytes_written: prepared.desired_bytes.byteLength, - executed_at: isoInstant(options.at, "synthetic execution time") - }); - } - - async verify(prepared, execution, options) { - verifySealedRecord(execution, "synthetic execution"); - const currentBytes = await readFile(prepared.absolute); - const actual = sha256(currentBytes); - return sealRecord({ - protocol: "cint/outcome-verification/1", - status: actual === prepared.after_sha256 ? "VERIFIED" : "DIVERGED", - target: prepared.relative, - expected_sha256: prepared.after_sha256, - actual_sha256: actual, - checked_at: isoInstant(options.at, "synthetic verification time") - }); - } - - async rollback(prepared, options) { - await atomicWrite(prepared.absolute, prepared.original_bytes, prepared.mode); - const restored = sha256(await readFile(prepared.absolute)); - return sealRecord({ - protocol: "cint/rollback/1", - status: restored === prepared.before_sha256 ? "RESTORED" : "FAILED", - target: prepared.relative, - expected_sha256: prepared.before_sha256, - actual_sha256: restored, - rolled_back_at: isoInstant(options.at, "synthetic rollback time") - }); - } -} diff --git a/src/cint/adapters/synthetic-file-patch.ts b/src/cint/adapters/synthetic-file-patch.ts new file mode 100644 index 0000000..86588a5 --- /dev/null +++ b/src/cint/adapters/synthetic-file-patch.ts @@ -0,0 +1,221 @@ +import { randomUUID } from "node:crypto"; +import { open, readFile, realpath, rename, stat, unlink } from "node:fs/promises"; +import path from "node:path"; +import type { FileHandle } from "node:fs/promises"; + +import { + CintError, + assertCint, + assertExactKeys, + boundedString, + identifier, + isoInstant, + sealRecord, + sha256, + sha256Digest, + verifyProtocolRecord, + verifySealedRecord +} from "../canonical.js"; +import { createAdapterCapability } from "../challenge.js"; +import { verifyOutcomeVerification, verifyRestoredRollback } from "../outcome.js"; +import type { + AdapterId, + CanonicalInstant, + Identifier, + TargetDigest +} from "../types/brands.js"; +import type { AdapterOperationOptions } from "../types/adapters.js"; +import type { + AdapterCapability, + AdapterExecution, + FailedRollback, + IntentRecord, + OutcomeVerification, + RestoredRollback +} from "../types/records.js"; + +interface ResolvedTarget { + readonly absolute: string; + readonly relative: string; +} + +export interface SyntheticPreparedAction { + readonly absolute: string; + readonly relative: string; + readonly mode: number; + readonly original_bytes: Buffer; + readonly desired_bytes: Buffer; + readonly before_sha256: TargetDigest; + readonly after_sha256: TargetDigest; + readonly prepared_at: CanonicalInstant; +} + +function property(value: unknown, name: string): unknown { + return value !== null && (typeof value === "object" || typeof value === "function") + ? Reflect.get(value, name) + : undefined; +} + +function errorCode(error: unknown): unknown { + return property(error, "code"); +} + +function signalAborted(signal: AbortSignal | undefined): boolean { + return signal?.aborted === true; +} + +function isWithin(boundary: string, candidate: string): boolean { + const relative = path.relative(boundary, candidate); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} + +function safeRelativePath(value: unknown): string { + const relative = boundedString(value, "synthetic target path", { minimum: 1, maximum: 1024 }); + assertCint( + !path.isAbsolute(relative) && !relative.includes("\0"), + "CINT_TARGET_INVALID", + "Synthetic target path must be relative and NUL-free" + ); + const normalized = path.normalize(relative); + assertCint( + normalized !== ".." && !normalized.startsWith(`..${path.sep}`), + "CINT_TARGET_INVALID", + "Synthetic target path leaves the disposable boundary" + ); + return normalized.split(path.sep).join("/"); +} + +async function resolveInside(boundary: string, value: unknown): Promise { + const relative = safeRelativePath(value); + const joined = path.resolve(boundary, relative); + assertCint(isWithin(boundary, joined), "CINT_TARGET_INVALID", "Synthetic target path leaves the disposable boundary"); + const absolute = await realpath(joined); + assertCint(isWithin(boundary, absolute), "CINT_TARGET_INVALID", "Synthetic target resolves outside the disposable boundary"); + const metadata = await stat(absolute); + assertCint(metadata.isFile(), "CINT_TARGET_INVALID", "Synthetic target must be a regular file"); + return { absolute, relative }; +} + +async function atomicWrite(filePath: string, bytes: Uint8Array, mode: number): Promise { + const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.cint.tmp`); + let handle: FileHandle | undefined; + try { + handle = await open(temporary, "wx", mode); + await handle.writeFile(bytes); + await handle.sync(); + await handle.close(); + handle = undefined; + await rename(temporary, filePath); + } finally { + if (handle) await handle.close().catch(() => {}); + await unlink(temporary).catch(() => {}); + } +} + +export class SyntheticFilePatchAdapter { + readonly id: AdapterId; + readonly root: string; + readonly capability: AdapterCapability; + + constructor(root: string) { + this.id = identifier("cint.adapter.synthetic-file-patch", "synthetic adapter id"); + this.root = path.resolve(root); + this.capability = createAdapterCapability({ + id: this.id, + action_types: ["SYNTHETIC_FILE_PATCH"], + consequence_classes: ["CONSEQUENTIAL"], + prepare_side_effect_free: true, + rollback: true, + interrupt: true, + outcome_verification: true + }); + } + + prepare(intent: IntentRecord, options: AdapterOperationOptions): Promise; + async prepare(value: unknown, options: AdapterOperationOptions): Promise { + const intent = verifyProtocolRecord(value, "cint/intent/1", "intent"); + assertCint(intent.action.adapter === this.id, "CINT_ADAPTER_MISMATCH", "Intent does not target the synthetic adapter"); + assertCint(intent.action.type === "SYNTHETIC_FILE_PATCH", "CINT_ADAPTER_ACTION_UNSUPPORTED", "Synthetic adapter supports only file patch actions"); + const target = assertExactKeys(intent.action.target, ["path"], [], "synthetic target"); + const parameters = assertExactKeys(intent.action.parameters, ["content", "expected_before_sha256"], [], "synthetic parameters"); + const content = boundedString(parameters["content"], "synthetic content", { minimum: 0, maximum: 1024 * 1024 }); + const expectedBefore = sha256Digest(parameters["expected_before_sha256"], "synthetic expected_before_sha256"); + const boundary = await realpath(this.root).catch((error: unknown) => { + throw new CintError("CINT_TARGET_INVALID", "Synthetic adapter root is unavailable", { cause: errorCode(error) }); + }); + const resolved = await resolveInside(boundary, target["path"]).catch((error: unknown) => { + throw new CintError("CINT_TARGET_INVALID", "Synthetic target is outside the available disposable boundary", { + cause: errorCode(error) + }); + }); + const metadata = await stat(resolved.absolute); + const originalBytes = await readFile(resolved.absolute); + const beforeSha256 = sha256(originalBytes); + assertCint(beforeSha256 === expectedBefore, "CINT_TARGET_STATE_CHANGED", "Synthetic target changed before execution"); + return Object.freeze({ + absolute: resolved.absolute, + relative: resolved.relative, + mode: metadata.mode & 0o777, + original_bytes: Buffer.from(originalBytes), + desired_bytes: Buffer.from(content, "utf8"), + before_sha256: beforeSha256, + after_sha256: sha256(Buffer.from(content, "utf8")), + prepared_at: isoInstant(options.at, "synthetic preparation time") + }); + } + + async execute(prepared: SyntheticPreparedAction, options: AdapterOperationOptions): Promise { + assertCint(!signalAborted(options.signal), "CINT_EXECUTION_INTERRUPTED", "Synthetic action was interrupted before execution"); + const currentBytes = await readFile(prepared.absolute); + assertCint(sha256(currentBytes) === prepared.before_sha256, "CINT_TARGET_STATE_CHANGED", "Synthetic target changed after preparation"); + await atomicWrite(prepared.absolute, prepared.desired_bytes, prepared.mode); + assertCint(!signalAborted(options.signal), "CINT_EXECUTION_INTERRUPTED", "Synthetic action was interrupted during execution"); + return sealRecord({ + protocol: "cint/synthetic-execution/1" as const, + adapter_id: this.id, + action_type: identifier("SYNTHETIC_FILE_PATCH", "synthetic action type"), + target: prepared.relative, + before_sha256: prepared.before_sha256, + intended_after_sha256: prepared.after_sha256, + bytes_written: prepared.desired_bytes.byteLength, + executed_at: isoInstant(options.at, "synthetic execution time") + }); + } + + async verify( + prepared: SyntheticPreparedAction, + execution: unknown, + options: AdapterOperationOptions + ): Promise { + verifySealedRecord(execution, "synthetic execution"); + const currentBytes = await readFile(prepared.absolute); + const actual = sha256(currentBytes); + return verifyOutcomeVerification(sealRecord({ + protocol: "cint/outcome-verification/1" as const, + status: actual === prepared.after_sha256 ? "VERIFIED" as const : "DIVERGED" as const, + target: prepared.relative, + expected_sha256: prepared.after_sha256, + actual_sha256: actual, + checked_at: isoInstant(options.at, "synthetic verification time") + })); + } + + async rollback( + prepared: SyntheticPreparedAction, + options: AdapterOperationOptions + ): Promise { + await atomicWrite(prepared.absolute, prepared.original_bytes, prepared.mode); + const restored = sha256(await readFile(prepared.absolute)); + const common = { + protocol: "cint/rollback/1" as const, + target: prepared.relative, + expected_sha256: prepared.before_sha256, + actual_sha256: restored, + rolled_back_at: isoInstant(options.at, "synthetic rollback time") + }; + if (restored === prepared.before_sha256) { + return verifyRestoredRollback(sealRecord({ ...common, status: "RESTORED" as const })); + } + return sealRecord({ ...common, status: "FAILED" as const }); + } +} diff --git a/src/cint/authority.js b/src/cint/authority.js deleted file mode 100644 index 8668058..0000000 --- a/src/cint/authority.js +++ /dev/null @@ -1,110 +0,0 @@ -import { - assertCint, - assertExactKeys, - assertJsonValue, - boundedString, - canonicalDigest, - identifier, - integer, - isoInstant, - sealRecord, - stringArray, - verifyProtocolRecord -} from "./canonical.js"; - -function grantRecord(value, index) { - assertExactKeys(value, ["adapter", "type", "target"], [], `authority.grants[${index}]`); - return { - adapter: identifier(value.adapter, `authority.grants[${index}].adapter`), - type: identifier(value.type, `authority.grants[${index}].type`), - target_digest: canonicalDigest(assertJsonValue(value.target, `authority.grants[${index}].target`)) - }; -} - -export function createAuthorityGrant(input) { - assertExactKeys( - input, - [ - "id", - "principal_id", - "issuer_id", - "epoch", - "grants", - "policy_ids", - "require_rollback", - "issued_at", - "not_before", - "expires_at" - ], - [], - "authority" - ); - assertCint(Array.isArray(input.grants) && input.grants.length > 0 && input.grants.length <= 32, "CINT_AUTHORITY_GRANTS", "authority.grants must contain 1-32 exact grants"); - assertCint(typeof input.require_rollback === "boolean", "CINT_AUTHORITY_ROLLBACK", "authority.require_rollback must be boolean"); - const issuedAt = isoInstant(input.issued_at, "authority.issued_at"); - const notBefore = isoInstant(input.not_before, "authority.not_before"); - const expiresAt = isoInstant(input.expires_at, "authority.expires_at"); - assertCint(Date.parse(issuedAt) <= Date.parse(notBefore), "CINT_AUTHORITY_TIME", "authority.not_before precedes issuance"); - assertCint(Date.parse(notBefore) < Date.parse(expiresAt), "CINT_AUTHORITY_TIME", "authority expiry must follow activation"); - const grants = input.grants.map(grantRecord); - assertCint(new Set(grants.map(canonicalDigest)).size === grants.length, "CINT_AUTHORITY_GRANTS", "authority.grants contains duplicates"); - return sealRecord({ - protocol: "cint/authority/1", - id: identifier(input.id, "authority.id"), - principal_id: identifier(input.principal_id, "authority.principal_id"), - issuer_id: identifier(input.issuer_id, "authority.issuer_id"), - status: "ACTIVE", - epoch: integer(input.epoch, "authority.epoch", { minimum: 1 }), - grants, - policy_ids: stringArray(input.policy_ids, "authority.policy_ids", { - minimum: 0, - maximum: 16, - bytes: 128 - }).map((value) => identifier(value, "authority.policy_ids item")), - require_rollback: input.require_rollback, - issued_at: issuedAt, - not_before: notBefore, - expires_at: expiresAt, - revoked_at: null, - revocation_reason: null - }); -} - -export function revokeAuthority(authority, input) { - verifyProtocolRecord(authority, "cint/authority/1", "authority"); - assertExactKeys(input, ["revoked_at", "reason"], [], "authority revocation"); - const revokedAt = isoInstant(input.revoked_at, "authority.revoked_at"); - assertCint(authority.status === "ACTIVE", "CINT_AUTHORITY_INACTIVE", "Only active authority can be revoked"); - assertCint(Date.parse(revokedAt) >= Date.parse(authority.issued_at), "CINT_AUTHORITY_TIME", "authority revocation precedes issuance"); - const unsigned = Object.fromEntries(Object.entries(authority).filter(([key]) => key !== "digest")); - return sealRecord({ - ...unsigned, - status: "REVOKED", - epoch: authority.epoch + 1, - revoked_at: revokedAt, - revocation_reason: boundedString(input.reason, "authority revocation reason", { minimum: 1, maximum: 1024 }) - }); -} - -export function evaluateAuthority({ authority, intent, policy, now }) { - verifyProtocolRecord(authority, "cint/authority/1", "authority"); - verifyProtocolRecord(intent, "cint/intent/1", "intent"); - if (policy) verifyProtocolRecord(policy, "cint/policy/1", "policy"); - const at = isoInstant(now, "authority evaluation time"); - const reasons = []; - if (authority.status !== "ACTIVE") reasons.push("CINT_AUTHORITY_INACTIVE"); - if (authority.principal_id !== intent.principal_id) reasons.push("CINT_AUTHORITY_PRINCIPAL_MISMATCH"); - if (Date.parse(at) < Date.parse(authority.not_before)) reasons.push("CINT_AUTHORITY_NOT_YET_VALID"); - if (Date.parse(at) >= Date.parse(authority.expires_at)) reasons.push("CINT_AUTHORITY_EXPIRED"); - const exactGrant = authority.grants.some( - (grant) => - grant.adapter === intent.action.adapter && - grant.type === intent.action.type && - grant.target_digest === intent.target_digest - ); - if (!exactGrant) reasons.push("CINT_AUTHORITY_ACTION_DENIED"); - if (policy && authority.policy_ids.length > 0 && !authority.policy_ids.includes(policy.id)) { - reasons.push("CINT_AUTHORITY_POLICY_DENIED"); - } - return { allowed: reasons.length === 0, reasons }; -} diff --git a/src/cint/authority.ts b/src/cint/authority.ts new file mode 100644 index 0000000..7191802 --- /dev/null +++ b/src/cint/authority.ts @@ -0,0 +1,151 @@ +import { + assertCint, + assertExactKeys, + assertJsonValue, + boundedString, + canonicalDigest, + identifier, + integer, + isoInstant, + sealRecord, + stringArray, + verifyProtocolRecord +} from "./canonical.js"; +import type { + AdapterId, + AuthorityId, + Epoch, + Identifier, + PolicyId, + PrincipalId, + TargetDigest +} from "./types/brands.js"; +import type { + ActiveAuthority, + AuthorityGrant, + AuthorityRecord, + IntentRecord, + PolicySnapshot, + RevokedAuthority +} from "./types/records.js"; + +export interface AuthorityEvaluationInput { + readonly authority: AuthorityRecord; + readonly intent: IntentRecord; + readonly policy?: PolicySnapshot | null; + readonly now: unknown; +} + +export interface AuthorityEvaluationResult { + readonly allowed: boolean; + readonly reasons: readonly string[]; +} + +function grantRecord(value: unknown, index: number): AuthorityGrant { + const input = assertExactKeys(value, ["adapter", "type", "target"], [], `authority.grants[${index}]`); + return { + adapter: identifier(input["adapter"], `authority.grants[${index}].adapter`), + type: identifier(input["type"], `authority.grants[${index}].type`), + target_digest: canonicalDigest(assertJsonValue(input["target"], `authority.grants[${index}].target`)) + }; +} + +export function createAuthorityGrant(value: unknown): ActiveAuthority { + const input = assertExactKeys( + value, + [ + "id", + "principal_id", + "issuer_id", + "epoch", + "grants", + "policy_ids", + "require_rollback", + "issued_at", + "not_before", + "expires_at" + ], + [], + "authority" + ); + assertCint( + Array.isArray(input["grants"]) && input["grants"].length > 0 && input["grants"].length <= 32, + "CINT_AUTHORITY_GRANTS", + "authority.grants must contain 1-32 exact grants" + ); + assertCint(typeof input["require_rollback"] === "boolean", "CINT_AUTHORITY_ROLLBACK", "authority.require_rollback must be boolean"); + const issuedAt = isoInstant(input["issued_at"], "authority.issued_at"); + const notBefore = isoInstant(input["not_before"], "authority.not_before"); + const expiresAt = isoInstant(input["expires_at"], "authority.expires_at"); + assertCint(Date.parse(issuedAt) <= Date.parse(notBefore), "CINT_AUTHORITY_TIME", "authority.not_before precedes issuance"); + assertCint(Date.parse(notBefore) < Date.parse(expiresAt), "CINT_AUTHORITY_TIME", "authority expiry must follow activation"); + const grants = input["grants"].map((grant, index) => grantRecord(grant, index)); + assertCint(new Set(grants.map(canonicalDigest)).size === grants.length, "CINT_AUTHORITY_GRANTS", "authority.grants contains duplicates"); + return sealRecord({ + protocol: "cint/authority/1" as const, + id: identifier(input["id"], "authority.id"), + principal_id: identifier(input["principal_id"], "authority.principal_id"), + issuer_id: identifier(input["issuer_id"], "authority.issuer_id"), + status: "ACTIVE" as const, + epoch: integer(input["epoch"], "authority.epoch", { minimum: 1 }), + grants, + policy_ids: stringArray(input["policy_ids"], "authority.policy_ids", { + minimum: 0, + maximum: 16, + bytes: 128 + }).map((item) => identifier(item, "authority.policy_ids item")), + require_rollback: input["require_rollback"], + issued_at: issuedAt, + not_before: notBefore, + expires_at: expiresAt, + revoked_at: null, + revocation_reason: null + }); +} + +export function revokeAuthority(authority: unknown, value: unknown): RevokedAuthority { + const current = verifyProtocolRecord(authority, "cint/authority/1", "authority"); + const input = assertExactKeys(value, ["revoked_at", "reason"], [], "authority revocation"); + const revokedAt = isoInstant(input["revoked_at"], "authority.revoked_at"); + assertCint(current.status === "ACTIVE", "CINT_AUTHORITY_INACTIVE", "Only active authority can be revoked"); + assertCint(Date.parse(revokedAt) >= Date.parse(current.issued_at), "CINT_AUTHORITY_TIME", "authority revocation precedes issuance"); + return sealRecord({ + protocol: current.protocol, + id: current.id, + principal_id: current.principal_id, + issuer_id: current.issuer_id, + status: "REVOKED" as const, + epoch: integer(current.epoch + 1, "authority.epoch", { minimum: 1 }), + grants: current.grants, + policy_ids: current.policy_ids, + require_rollback: current.require_rollback, + issued_at: current.issued_at, + not_before: current.not_before, + expires_at: current.expires_at, + revoked_at: revokedAt, + revocation_reason: boundedString(input["reason"], "authority revocation reason", { minimum: 1, maximum: 1024 }) + }); +} + +export function evaluateAuthority(input: AuthorityEvaluationInput): AuthorityEvaluationResult { + const authority = verifyProtocolRecord(input.authority, "cint/authority/1", "authority"); + const intent = verifyProtocolRecord(input.intent, "cint/intent/1", "intent"); + const policy = input.policy ? verifyProtocolRecord(input.policy, "cint/policy/1", "policy") : null; + const at = isoInstant(input.now, "authority evaluation time"); + const reasons: string[] = []; + if (authority.status !== "ACTIVE") reasons.push("CINT_AUTHORITY_INACTIVE"); + if (authority.principal_id !== intent.principal_id) reasons.push("CINT_AUTHORITY_PRINCIPAL_MISMATCH"); + if (Date.parse(at) < Date.parse(authority.not_before)) reasons.push("CINT_AUTHORITY_NOT_YET_VALID"); + if (Date.parse(at) >= Date.parse(authority.expires_at)) reasons.push("CINT_AUTHORITY_EXPIRED"); + const exactGrant = authority.grants.some( + (grant) => + grant.adapter === intent.action.adapter && + grant.type === intent.action.type && + grant.target_digest === intent.target_digest + ); + if (!exactGrant) reasons.push("CINT_AUTHORITY_ACTION_DENIED"); + if (policy && authority.policy_ids.length > 0 && !authority.policy_ids.includes(policy.id)) { + reasons.push("CINT_AUTHORITY_POLICY_DENIED"); + } + return { allowed: reasons.length === 0, reasons }; +} diff --git a/src/cint/canonical.js b/src/cint/canonical.js deleted file mode 100644 index e986f05..0000000 --- a/src/cint/canonical.js +++ /dev/null @@ -1,176 +0,0 @@ -import { canonicalJson, canonicalize, sha256 } from "../util.js"; -import { validateCintSchema } from "./schema.js"; - -const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; -const SHA256 = /^[a-f0-9]{64}$/; - -export class CintError extends Error { - constructor(code, message, details = undefined) { - super(message); - this.name = "CintError"; - this.code = code; - if (details !== undefined) this.details = details; - } -} - -export function assertCint(condition, code, message, details = undefined) { - if (!condition) throw new CintError(code, message, details); -} - -export function isPlainRecord(value) { - if (value === null || typeof value !== "object" || Array.isArray(value)) return false; - const prototype = Object.getPrototypeOf(value); - return prototype === Object.prototype || prototype === null; -} - -export function assertExactKeys(value, required, optional = [], label = "object") { - assertCint(isPlainRecord(value), "CINT_OBJECT_INVALID", `${label} must be a plain JSON object`); - const allowed = new Set([...required, ...optional]); - const unknown = Object.keys(value).filter((key) => !allowed.has(key)); - const missing = required.filter((key) => !Object.hasOwn(value, key)); - assertCint(unknown.length === 0, "CINT_UNKNOWN_FIELD", `${label} contains unknown fields`, { unknown }); - assertCint(missing.length === 0, "CINT_REQUIRED_FIELD", `${label} is missing required fields`, { missing }); - return value; -} - -export function assertJsonValue(value, label = "value") { - if (value === null || typeof value === "string" || typeof value === "boolean") return value; - if (typeof value === "number") { - assertCint(Number.isFinite(value), "CINT_JSON_INVALID", `${label} must contain only finite numbers`); - return value; - } - if (Array.isArray(value)) { - value.forEach((item, index) => assertJsonValue(item, `${label}[${index}]`)); - return value; - } - assertCint(isPlainRecord(value), "CINT_JSON_INVALID", `${label} must contain only JSON values`); - for (const [key, item] of Object.entries(value)) { - assertCint(typeof key === "string", "CINT_JSON_INVALID", `${label} has an invalid key`); - assertJsonValue(item, `${label}.${key}`); - } - return value; -} - -export function identifier(value, label = "identifier") { - assertCint(typeof value === "string" && IDENTIFIER.test(value), "CINT_IDENTIFIER_INVALID", `${label} is invalid`); - return value; -} - -export function boundedString(value, label, options = {}) { - const minimum = options.minimum ?? 1; - const maximum = options.maximum ?? 4096; - assertCint(typeof value === "string", "CINT_STRING_INVALID", `${label} must be a string`); - const bytes = Buffer.byteLength(value, "utf8"); - assertCint(bytes >= minimum && bytes <= maximum, "CINT_STRING_INVALID", `${label} must contain ${minimum}-${maximum} bytes`, { bytes }); - return value; -} - -export function stringArray(value, label, options = {}) { - const minimum = options.minimum ?? 0; - const maximum = options.maximum ?? 32; - const itemMinimum = options.itemMinimum ?? 1; - const itemMaximum = options.itemMaximum ?? options.bytes ?? 512; - assertCint(Array.isArray(value), "CINT_ARRAY_INVALID", `${label} must be an array`); - assertCint(value.length >= minimum && value.length <= maximum, "CINT_ARRAY_INVALID", `${label} must contain ${minimum}-${maximum} items`); - const checked = value.map((item, index) => - boundedString(item, `${label}[${index}]`, { minimum: itemMinimum, maximum: itemMaximum }) - ); - assertCint(new Set(checked).size === checked.length, "CINT_ARRAY_DUPLICATE", `${label} contains duplicates`); - return checked; -} - -export function integer(value, label, options = {}) { - const minimum = options.minimum ?? 0; - const maximum = options.maximum ?? Number.MAX_SAFE_INTEGER; - assertCint(Number.isSafeInteger(value) && value >= minimum && value <= maximum, "CINT_INTEGER_INVALID", `${label} must be an integer from ${minimum} through ${maximum}`); - return value; -} - -export function isoInstant(value, label = "timestamp") { - boundedString(value, label, { minimum: 24, maximum: 32 }); - const parsed = new Date(value); - assertCint(!Number.isNaN(parsed.valueOf()) && parsed.toISOString() === value, "CINT_TIME_INVALID", `${label} must be a canonical UTC instant`); - return value; -} - -export function sha256Digest(value, label = "sha256") { - assertCint(typeof value === "string" && SHA256.test(value), "CINT_DIGEST_INVALID", `${label} must be a lowercase SHA-256 digest`); - return value; -} - -export function canonicalDigest(value) { - assertJsonValue(value); - return sha256(canonicalJson(value)); -} - -export function parseCanonicalJson(text, label = "JSON") { - let value; - try { - value = JSON.parse(text); - } catch (error) { - throw new CintError("CINT_JSON_INVALID", `${label} is not valid JSON`, { cause: error.message }); - } - assertJsonValue(value, label); - assertCint(text === canonicalJson(value), "CINT_JSON_NOT_CANONICAL", `${label} must use canonical JSON encoding`); - return value; -} - -export function deepFreeze(value) { - if (value && typeof value === "object" && !Object.isFrozen(value)) { - for (const item of Object.values(value)) deepFreeze(item); - Object.freeze(value); - } - return value; -} - -export function immutableRecord(value) { - assertJsonValue(value); - return deepFreeze(canonicalize(value)); -} - -export function sealRecord(value) { - assertCint(!Object.hasOwn(value, "digest"), "CINT_DIGEST_PRESET", "Record digest must be computed by CINT"); - const canonical = canonicalize(value); - const sealed = { ...canonical, digest: canonicalDigest(canonical) }; - const validation = validateCintSchema(sealed); - assertCint( - !validation.known || validation.valid, - "CINT_SCHEMA_INVALID", - `Record does not satisfy the ${sealed.protocol} runtime schema`, - { errors: validation.errors } - ); - return immutableRecord(sealed); -} - -export function verifySealedRecord(value, label = "record") { - assertCint(isPlainRecord(value), "CINT_RECORD_INVALID", `${label} must be an object`); - sha256Digest(value.digest, `${label}.digest`); - const unsigned = Object.fromEntries(Object.entries(value).filter(([key]) => key !== "digest")); - assertCint( - canonicalDigest(unsigned) === value.digest, - "CINT_RECORD_TAMPERED", - `${label} digest does not match its content` - ); - const validation = validateCintSchema(value); - assertCint( - !validation.known || validation.valid, - "CINT_SCHEMA_INVALID", - `${label} does not satisfy the ${value.protocol} runtime schema`, - { errors: validation.errors } - ); - return value; -} - -export function verifyProtocolRecord(value, protocol, label = "record") { - verifySealedRecord(value, label); - assertCint(value.protocol === protocol, "CINT_PROTOCOL_INVALID", `${label} uses an unsupported protocol`, { - expected: protocol, - actual: value.protocol ?? null - }); - const validation = validateCintSchema(value, protocol); - assertCint(validation.known, "CINT_SCHEMA_UNAVAILABLE", `Runtime schema is unavailable for ${protocol}`); - assertCint(validation.valid, "CINT_SCHEMA_INVALID", `${label} does not satisfy the ${protocol} runtime schema`, { - errors: validation.errors - }); - return value; -} diff --git a/src/cint/canonical.ts b/src/cint/canonical.ts new file mode 100644 index 0000000..55f2e3b --- /dev/null +++ b/src/cint/canonical.ts @@ -0,0 +1,322 @@ +import { createHash } from "node:crypto"; + +import { validateCintSchema } from "./schema.js"; +import type { + CanonicalInstant, + CanonicalJson, + ConsumptionDigest, + DecisionDigest, + EvidenceSealDigest, + ExecutionDigest, + Identifier, + IntentDigest, + OutcomeDigest, + ReceiptDigest, + RecordDigest, + RevalidationDigest, + Sha256Digest, + VerificationDigest +} from "./types/brands.js"; +import type { + AdapterCapability, + AuthorityRecord, + ChallengeRecord, + Decision, + EvidenceSeal, + ExecutionResult, + IntentRecord, + IssuedDecisionReceipt, + MachineStateSnapshot, + Outcome, + PolicySnapshot, + PrincipalRecord, + Revalidation +} from "./types/records.js"; +import type { JsonObject, JsonValue } from "./types/protocols.js"; + +const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +const SHA256 = /^[a-f0-9]{64}$/; + +export type JsonRecord = Record; +export type VerifiedSealedRecord = JsonRecord & { readonly digest: Sha256Digest }; + +type ProtocolRecordMap = { + readonly "cint/adapter-capability/1": AdapterCapability; + readonly "cint/authority/1": AuthorityRecord; + readonly "cint/challenge/1": ChallengeRecord; + readonly "cint/decision/1": Decision; + readonly "cint/execution-result/1": ExecutionResult; + readonly "cint/intent/1": IntentRecord; + readonly "cint/machine-state/1": MachineStateSnapshot; + readonly "cint/outcome/1": Outcome; + readonly "cint/policy/1": PolicySnapshot; + readonly "cint/principal/1": PrincipalRecord; + readonly "cint/decision-receipt/1": IssuedDecisionReceipt; + readonly "cint/revalidation/1": Revalidation; + readonly "cint/evidence-seal/1": EvidenceSeal; +}; + +type DigestFor = + Value extends { readonly protocol: "cint/intent/1" } ? IntentDigest : + Value extends { readonly protocol: "cint/decision/1" } ? DecisionDigest : + Value extends { readonly protocol: "cint/decision-receipt/1" } ? ReceiptDigest : + Value extends { readonly protocol: "cint/revalidation/1" } ? RevalidationDigest : + Value extends { readonly protocol: "cint/outcome/1" } ? OutcomeDigest : + Value extends { readonly protocol: "cint/evidence-seal/1" } ? EvidenceSealDigest : + Value extends { readonly protocol: "cint/outcome-verification/1" } ? VerificationDigest : + Value extends { readonly protocol: "cint/synthetic-execution/1" | "cint/codex-delegation-execution/1" | "cint/execution-interruption/1" } ? ExecutionDigest : + Value extends { readonly protocol: "cint/receipt-store-entry/1"; readonly state: "CONSUMED" } ? ConsumptionDigest : + RecordDigest; + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export class CintError extends Error { + override name = "CintError"; + readonly code: string; + readonly details?: unknown; + + constructor(code: string, message: string, details: unknown = undefined) { + super(message); + this.code = code; + if (details !== undefined) this.details = details; + } +} + +export function assertCint( + condition: unknown, + code: string, + message: string, + details: unknown = undefined +): asserts condition { + if (!condition) throw new CintError(code, message, details); +} + +export function isPlainRecord(value: unknown): value is JsonRecord { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +export function assertExactKeys( + value: unknown, + required: readonly string[], + optional: readonly string[] = [], + label = "object" +): JsonRecord { + assertCint(isPlainRecord(value), "CINT_OBJECT_INVALID", `${label} must be a plain JSON object`); + const allowed = new Set([...required, ...optional]); + const unknown = Object.keys(value).filter((key) => !allowed.has(key)); + const missing = required.filter((key) => !Object.hasOwn(value, key)); + assertCint(unknown.length === 0, "CINT_UNKNOWN_FIELD", `${label} contains unknown fields`, { unknown }); + assertCint(missing.length === 0, "CINT_REQUIRED_FIELD", `${label} is missing required fields`, { missing }); + return value; +} + +export function assertJsonValue(value: unknown, label = "value"): JsonValue { + if (value === null || typeof value === "string" || typeof value === "boolean") return value; + if (typeof value === "number") { + assertCint(Number.isFinite(value), "CINT_JSON_INVALID", `${label} must contain only finite numbers`); + return value; + } + if (Array.isArray(value)) { + value.forEach((item, index) => assertJsonValue(item, `${label}[${index}]`)); + return value as JsonValue; + } + assertCint(isPlainRecord(value), "CINT_JSON_INVALID", `${label} must contain only JSON values`); + for (const [key, item] of Object.entries(value)) { + assertCint(typeof key === "string", "CINT_JSON_INVALID", `${label} has an invalid key`); + assertJsonValue(item, `${label}.${key}`); + } + return value as JsonObject; +} + +export function canonicalize(value: JsonValue): JsonValue { + if (Array.isArray(value)) return value.map(canonicalize); + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0) + .map(([key, item]) => [key, canonicalize(item)]) + ); + } + return value; +} + +export function canonicalJson(value: JsonValue): CanonicalJson { + return JSON.stringify(canonicalize(value)) as CanonicalJson; +} + +export function sha256(value: string | Uint8Array): Digest { + return createHash("sha256").update(value).digest("hex") as Digest; +} + +export function identifier( + value: unknown, + label = "identifier" +): IdentifierType { + assertCint(typeof value === "string" && IDENTIFIER.test(value), "CINT_IDENTIFIER_INVALID", `${label} is invalid`); + return value as IdentifierType; +} + +export interface BoundedStringOptions { + readonly minimum?: number; + readonly maximum?: number; +} + +export function boundedString(value: unknown, label: string, options: BoundedStringOptions = {}): string { + const minimum = options.minimum ?? 1; + const maximum = options.maximum ?? 4096; + assertCint(typeof value === "string", "CINT_STRING_INVALID", `${label} must be a string`); + const bytes = Buffer.byteLength(value, "utf8"); + assertCint(bytes >= minimum && bytes <= maximum, "CINT_STRING_INVALID", `${label} must contain ${minimum}-${maximum} bytes`, { bytes }); + return value; +} + +export interface StringArrayOptions { + readonly minimum?: number; + readonly maximum?: number; + readonly itemMinimum?: number; + readonly itemMaximum?: number; + readonly bytes?: number; +} + +export function stringArray(value: unknown, label: string, options: StringArrayOptions = {}): string[] { + const minimum = options.minimum ?? 0; + const maximum = options.maximum ?? 32; + const itemMinimum = options.itemMinimum ?? 1; + const itemMaximum = options.itemMaximum ?? options.bytes ?? 512; + assertCint(Array.isArray(value), "CINT_ARRAY_INVALID", `${label} must be an array`); + assertCint(value.length >= minimum && value.length <= maximum, "CINT_ARRAY_INVALID", `${label} must contain ${minimum}-${maximum} items`); + const checked = value.map((item, index) => + boundedString(item, `${label}[${index}]`, { minimum: itemMinimum, maximum: itemMaximum }) + ); + assertCint(new Set(checked).size === checked.length, "CINT_ARRAY_DUPLICATE", `${label} contains duplicates`); + return checked; +} + +export interface IntegerOptions { + readonly minimum?: number; + readonly maximum?: number; +} + +export function integer( + value: unknown, + label: string, + options: IntegerOptions = {} +): NumberType { + const minimum = options.minimum ?? 0; + const maximum = options.maximum ?? Number.MAX_SAFE_INTEGER; + assertCint( + typeof value === "number" && Number.isSafeInteger(value) && value >= minimum && value <= maximum, + "CINT_INTEGER_INVALID", + `${label} must be an integer from ${minimum} through ${maximum}` + ); + return value as NumberType; +} + +export function isoInstant(value: unknown, label = "timestamp"): CanonicalInstant { + const checked = boundedString(value, label, { minimum: 24, maximum: 32 }); + const parsed = new Date(checked); + assertCint(!Number.isNaN(parsed.valueOf()) && parsed.toISOString() === checked, "CINT_TIME_INVALID", `${label} must be a canonical UTC instant`); + return checked as CanonicalInstant; +} + +export function sha256Digest( + value: unknown, + label = "sha256" +): Digest { + assertCint(typeof value === "string" && SHA256.test(value), "CINT_DIGEST_INVALID", `${label} must be a lowercase SHA-256 digest`); + return value as Digest; +} + +export function canonicalDigest(value: unknown): Digest { + return sha256(canonicalJson(assertJsonValue(value))); +} + +export function parseCanonicalJson(text: string, label = "JSON"): JsonValue { + let value: unknown; + try { + value = JSON.parse(text) as unknown; + } catch (error) { + throw new CintError("CINT_JSON_INVALID", `${label} is not valid JSON`, { cause: errorMessage(error) }); + } + const checked = assertJsonValue(value, label); + assertCint(text === canonicalJson(checked), "CINT_JSON_NOT_CANONICAL", `${label} must use canonical JSON encoding`); + return checked; +} + +export function deepFreeze(value: Value): Value { + if (value !== null && typeof value === "object" && !Object.isFrozen(value)) { + for (const item of Object.values(value)) deepFreeze(item); + Object.freeze(value); + } + return value; +} + +export function immutableRecord(value: Value): Value; +export function immutableRecord(value: unknown): unknown { + const checked = assertJsonValue(value); + return deepFreeze(canonicalize(checked)); +} + +export function sealRecord(value: Value): Readonly }>; +export function sealRecord(value: object): object { + assertCint(!Object.hasOwn(value, "digest"), "CINT_DIGEST_PRESET", "Record digest must be computed by CINT"); + const canonical = canonicalize(assertJsonValue(value)); + assertCint(isPlainRecord(canonical), "CINT_OBJECT_INVALID", "Record must remain a plain JSON object"); + const sealed: JsonRecord & { digest: Sha256Digest } = { ...canonical, digest: canonicalDigest(canonical) }; + const validation = validateCintSchema(sealed); + assertCint( + !validation.known || validation.valid, + "CINT_SCHEMA_INVALID", + `Record does not satisfy the ${String(sealed["protocol"])} runtime schema`, + { errors: validation.errors } + ); + const immutable = immutableRecord(assertJsonValue(sealed)); + assertCint(isPlainRecord(immutable), "CINT_OBJECT_INVALID", "Sealed record must be an object"); + return immutable; +} + +export function verifySealedRecord(value: unknown, label = "record"): VerifiedSealedRecord { + assertCint(isPlainRecord(value), "CINT_RECORD_INVALID", `${label} must be an object`); + const digest = sha256Digest(value["digest"], `${label}.digest`); + const unsigned = Object.fromEntries(Object.entries(value).filter(([key]) => key !== "digest")); + assertCint( + canonicalDigest(unsigned) === digest, + "CINT_RECORD_TAMPERED", + `${label} digest does not match its content` + ); + const validation = validateCintSchema(value); + assertCint( + !validation.known || validation.valid, + "CINT_SCHEMA_INVALID", + `${label} does not satisfy the ${String(value["protocol"])} runtime schema`, + { errors: validation.errors } + ); + return value as VerifiedSealedRecord; +} + +export function verifyProtocolRecord( + value: unknown, + protocol: Protocol, + label?: string +): ProtocolRecordMap[Protocol]; +export function verifyProtocolRecord( + value: unknown, + protocol: keyof ProtocolRecordMap, + label = "record" +): unknown { + const record = verifySealedRecord(value, label); + assertCint(record["protocol"] === protocol, "CINT_PROTOCOL_INVALID", `${label} uses an unsupported protocol`, { + expected: protocol, + actual: record["protocol"] ?? null + }); + const validation = validateCintSchema(record, protocol); + assertCint(validation.known, "CINT_SCHEMA_UNAVAILABLE", `Runtime schema is unavailable for ${protocol}`); + assertCint(validation.valid, "CINT_SCHEMA_INVALID", `${label} does not satisfy the ${protocol} runtime schema`, { + errors: validation.errors + }); + return record; +} diff --git a/src/cint/challenge.js b/src/cint/challenge.js deleted file mode 100644 index 521bc76..0000000 --- a/src/cint/challenge.js +++ /dev/null @@ -1,135 +0,0 @@ -import { - assertCint, - assertExactKeys, - identifier, - isoInstant, - sealRecord, - stringArray, - verifyProtocolRecord -} from "./canonical.js"; -import { evaluateAuthority } from "./authority.js"; -import { evaluatePolicy } from "./policy.js"; - -const CONSEQUENCE_CLASSES = new Set(["READ_ONLY", "CONSEQUENTIAL"]); - -function challengeReason(code, disposition, message) { - return { code, disposition, message }; -} - -export function createAdapterCapability(input) { - assertExactKeys( - input, - [ - "id", - "action_types", - "consequence_classes", - "prepare_side_effect_free", - "rollback", - "interrupt", - "outcome_verification" - ], - [], - "adapter capability" - ); - for (const field of ["prepare_side_effect_free", "rollback", "interrupt", "outcome_verification"]) { - assertCint(typeof input[field] === "boolean", "CINT_ADAPTER_CAPABILITY_INVALID", `adapter capability ${field} must be boolean`); - } - const consequenceClasses = stringArray(input.consequence_classes, "adapter consequence classes", { - minimum: 1, - maximum: 2, - bytes: 32 - }); - assertCint( - consequenceClasses.every((value) => CONSEQUENCE_CLASSES.has(value)), - "CINT_ADAPTER_CAPABILITY_INVALID", - "adapter capability has an unknown consequence class" - ); - return sealRecord({ - protocol: "cint/adapter-capability/1", - id: identifier(input.id, "adapter capability id"), - action_types: stringArray(input.action_types, "adapter action types", { - minimum: 1, - maximum: 64, - bytes: 128 - }).map((value) => identifier(value, "adapter action type")), - consequence_classes: consequenceClasses, - prepare_side_effect_free: input.prepare_side_effect_free, - rollback: input.rollback, - interrupt: input.interrupt, - outcome_verification: input.outcome_verification - }); -} - -export function runCounterIntentChallenge({ - intent, - principal, - authority, - policy, - adapter_capability, - machine_state, - now -}) { - verifyProtocolRecord(intent, "cint/intent/1", "intent"); - verifyProtocolRecord(principal, "cint/principal/1", "principal"); - verifyProtocolRecord(authority, "cint/authority/1", "authority"); - verifyProtocolRecord(policy, "cint/policy/1", "policy"); - verifyProtocolRecord(adapter_capability, "cint/adapter-capability/1", "adapter capability"); - verifyProtocolRecord(machine_state, "cint/machine-state/1", "machine state"); - const checkedAt = isoInstant(now, "challenge time"); - const reasons = []; - if (!machine_state.available) { - reasons.push(challengeReason("CINT_UNAVAILABLE", "DENY", "Current machine state is unavailable")); - } - if (!principal.authenticated) { - reasons.push(challengeReason("CINT_PRINCIPAL_UNAUTHENTICATED", "DENY", "Principal is not authenticated")); - } - if (principal.id !== intent.principal_id) { - reasons.push(challengeReason("CINT_PRINCIPAL_MISMATCH", "DENY", "Resolved principal does not own the intent")); - } - if (principal.authority_chain.length > 0 && !principal.authority_chain.includes(authority.id)) { - reasons.push(challengeReason("CINT_AUTHORITY_CHAIN_MISMATCH", "DENY", "Authority is absent from the principal chain")); - } - if (adapter_capability.id !== intent.action.adapter) { - reasons.push(challengeReason("CINT_ADAPTER_MISMATCH", "DENY", "Intent names a different adapter")); - } - if (adapter_capability.prepare_side_effect_free !== true) { - reasons.push(challengeReason("CINT_ADAPTER_PREPARE_UNSAFE", "DENY", "Adapter preparation is not side-effect-free")); - } - if (policy.require_explicit_request && intent.request === null) { - reasons.push(challengeReason("CINT_SILENT_REQUEST", "DENY", "No explicit principal request authorizes the action")); - } - if (policy.require_declared_effects && intent.declared_effects.length === 0) { - reasons.push(challengeReason("CINT_EFFECT_UNDECLARED", "DENY", "Consequential effects were not declared")); - } - const authorityResult = evaluateAuthority({ authority, intent, policy, now: checkedAt }); - for (const code of authorityResult.reasons) { - reasons.push(challengeReason(code, "DENY", "Current authority does not admit the exact action binding")); - } - const policyResult = evaluatePolicy({ policy, intent, adapter_capability }); - for (const code of policyResult.reasons) { - reasons.push(challengeReason(code, "DENY", "Current policy or adapter capability rejects the action")); - } - if (authority.require_rollback && intent.action.consequence === "CONSEQUENTIAL" && !adapter_capability.rollback) { - reasons.push(challengeReason("CINT_AUTHORITY_ROLLBACK_REQUIRED", "DENY", "Authority requires a rollback-capable adapter")); - } - if (policy.review_on_uncertainty && intent.uncertainties.length > 0) { - reasons.push(challengeReason("CINT_COUNTER_INTENT_UNRESOLVED", "REVIEW", "Intent contains unresolved counter-intent")); - } - const status = reasons.some((reason) => reason.disposition === "DENY") - ? "DENIED" - : reasons.some((reason) => reason.disposition === "REVIEW") - ? "REVIEW" - : "CLEAR"; - return sealRecord({ - protocol: "cint/challenge/1", - status, - checked_at: checkedAt, - intent_digest: intent.digest, - principal_digest: principal.digest, - authority_digest: authority.digest, - policy_digest: policy.digest, - adapter_capability_digest: adapter_capability.digest, - machine_state_digest: machine_state.state_digest, - reasons - }); -} diff --git a/src/cint/challenge.ts b/src/cint/challenge.ts new file mode 100644 index 0000000..7bbcf04 --- /dev/null +++ b/src/cint/challenge.ts @@ -0,0 +1,163 @@ +import { + assertCint, + assertExactKeys, + identifier, + isoInstant, + sealRecord, + stringArray, + verifyProtocolRecord +} from "./canonical.js"; +import { evaluateAuthority } from "./authority.js"; +import { evaluatePolicy } from "./policy.js"; +import type { AdapterId, Identifier } from "./types/brands.js"; +import type { + AdapterCapability, + AuthorityRecord, + ChallengeReason, + ChallengeRecord, + ConsequenceClass, + IntentRecord, + MachineStateSnapshot, + PolicySnapshot, + PrincipalRecord +} from "./types/records.js"; + +const CONSEQUENCE_CLASSES: ReadonlySet = new Set(["READ_ONLY", "CONSEQUENTIAL"]); +const CAPABILITY_FLAGS = ["prepare_side_effect_free", "rollback", "interrupt", "outcome_verification"] as const; + +function areConsequenceClasses(values: readonly string[]): values is readonly ConsequenceClass[] { + return values.every((item) => CONSEQUENCE_CLASSES.has(item)); +} + +function capabilityBoolean(value: unknown, field: string): boolean { + assertCint(typeof value === "boolean", "CINT_ADAPTER_CAPABILITY_INVALID", `adapter capability ${field} must be boolean`); + return value; +} + +export interface CounterIntentChallengeInput { + readonly intent: IntentRecord; + readonly principal: PrincipalRecord; + readonly authority: AuthorityRecord; + readonly policy: PolicySnapshot; + readonly adapter_capability: AdapterCapability; + readonly machine_state: MachineStateSnapshot; + readonly now: unknown; +} + +function challengeReason(code: string, disposition: "DENY" | "REVIEW", message: string): ChallengeReason { + return { code: identifier(code, "challenge reason code"), disposition, message }; +} + +export function createAdapterCapability(value: unknown): AdapterCapability { + const input = assertExactKeys( + value, + [ + "id", + "action_types", + "consequence_classes", + "prepare_side_effect_free", + "rollback", + "interrupt", + "outcome_verification" + ], + [], + "adapter capability" + ); + for (const field of CAPABILITY_FLAGS) { + assertCint(typeof input[field] === "boolean", "CINT_ADAPTER_CAPABILITY_INVALID", `adapter capability ${field} must be boolean`); + } + const consequenceClasses = stringArray(input["consequence_classes"], "adapter consequence classes", { + minimum: 1, + maximum: 2, + bytes: 32 + }); + assertCint( + areConsequenceClasses(consequenceClasses), + "CINT_ADAPTER_CAPABILITY_INVALID", + "adapter capability has an unknown consequence class" + ); + return sealRecord({ + protocol: "cint/adapter-capability/1" as const, + id: identifier(input["id"], "adapter capability id"), + action_types: stringArray(input["action_types"], "adapter action types", { + minimum: 1, + maximum: 64, + bytes: 128 + }).map((item) => identifier(item, "adapter action type")), + consequence_classes: consequenceClasses, + prepare_side_effect_free: capabilityBoolean(input["prepare_side_effect_free"], "prepare_side_effect_free"), + rollback: capabilityBoolean(input["rollback"], "rollback"), + interrupt: capabilityBoolean(input["interrupt"], "interrupt"), + outcome_verification: capabilityBoolean(input["outcome_verification"], "outcome_verification") + }); +} + +export function runCounterIntentChallenge(input: CounterIntentChallengeInput): ChallengeRecord { + const intent = verifyProtocolRecord(input.intent, "cint/intent/1", "intent"); + const principal = verifyProtocolRecord(input.principal, "cint/principal/1", "principal"); + const authority = verifyProtocolRecord(input.authority, "cint/authority/1", "authority"); + const policy = verifyProtocolRecord(input.policy, "cint/policy/1", "policy"); + const adapterCapability = verifyProtocolRecord( + input.adapter_capability, + "cint/adapter-capability/1", + "adapter capability" + ); + const machineState = verifyProtocolRecord(input.machine_state, "cint/machine-state/1", "machine state"); + const checkedAt = isoInstant(input.now, "challenge time"); + const reasons: ChallengeReason[] = []; + if (!machineState.available) { + reasons.push(challengeReason("CINT_UNAVAILABLE", "DENY", "Current machine state is unavailable")); + } + if (!principal.authenticated) { + reasons.push(challengeReason("CINT_PRINCIPAL_UNAUTHENTICATED", "DENY", "Principal is not authenticated")); + } + if (principal.id !== intent.principal_id) { + reasons.push(challengeReason("CINT_PRINCIPAL_MISMATCH", "DENY", "Resolved principal does not own the intent")); + } + if (principal.authority_chain.length > 0 && !principal.authority_chain.includes(authority.id)) { + reasons.push(challengeReason("CINT_AUTHORITY_CHAIN_MISMATCH", "DENY", "Authority is absent from the principal chain")); + } + if (adapterCapability.id !== intent.action.adapter) { + reasons.push(challengeReason("CINT_ADAPTER_MISMATCH", "DENY", "Intent names a different adapter")); + } + if (adapterCapability.prepare_side_effect_free !== true) { + reasons.push(challengeReason("CINT_ADAPTER_PREPARE_UNSAFE", "DENY", "Adapter preparation is not side-effect-free")); + } + if (policy.require_explicit_request && intent.request === null) { + reasons.push(challengeReason("CINT_SILENT_REQUEST", "DENY", "No explicit principal request authorizes the action")); + } + if (policy.require_declared_effects && intent.declared_effects.length === 0) { + reasons.push(challengeReason("CINT_EFFECT_UNDECLARED", "DENY", "Consequential effects were not declared")); + } + const authorityResult = evaluateAuthority({ authority, intent, policy, now: checkedAt }); + for (const code of authorityResult.reasons) { + reasons.push(challengeReason(code, "DENY", "Current authority does not admit the exact action binding")); + } + const policyResult = evaluatePolicy({ policy, intent, adapter_capability: adapterCapability }); + for (const code of policyResult.reasons) { + reasons.push(challengeReason(code, "DENY", "Current policy or adapter capability rejects the action")); + } + if (authority.require_rollback && intent.action.consequence === "CONSEQUENTIAL" && !adapterCapability.rollback) { + reasons.push(challengeReason("CINT_AUTHORITY_ROLLBACK_REQUIRED", "DENY", "Authority requires a rollback-capable adapter")); + } + if (policy.review_on_uncertainty && intent.uncertainties.length > 0) { + reasons.push(challengeReason("CINT_COUNTER_INTENT_UNRESOLVED", "REVIEW", "Intent contains unresolved counter-intent")); + } + const status = reasons.some((reason) => reason.disposition === "DENY") + ? "DENIED" + : reasons.some((reason) => reason.disposition === "REVIEW") + ? "REVIEW" + : "CLEAR"; + return sealRecord({ + protocol: "cint/challenge/1" as const, + status, + checked_at: checkedAt, + intent_digest: intent.digest, + principal_digest: principal.digest, + authority_digest: authority.digest, + policy_digest: policy.digest, + adapter_capability_digest: adapterCapability.digest, + machine_state_digest: machineState.state_digest, + reasons + }); +} diff --git a/src/cint/cli.js b/src/cint/cli.ts similarity index 87% rename from src/cint/cli.js rename to src/cint/cli.ts index cfc0ed5..0ae857d 100644 --- a/src/cint/cli.js +++ b/src/cint/cli.ts @@ -16,9 +16,13 @@ export const CINT_IDENTITY = Object.freeze({ "cint.adapter.synthetic-file-patch", "cint.adapter.codex-delegation" ]) -}); +} as const); -export function renderCintHelp() { +export interface CintCliOptions { + readonly write?: (value: string) => void; +} + +export function renderCintHelp(): string { return [ "SI1 CINT — Machine Counterintelligence Runtime", "", @@ -31,7 +35,10 @@ export function renderCintHelp() { ].join("\n"); } -export async function main(argv = process.argv.slice(2), options = {}) { +export async function main( + argv: readonly string[] = process.argv.slice(2), + options: CintCliOptions = {} +): Promise { const write = options.write ?? ((value) => process.stdout.write(value)); const [command = "help", ...rest] = argv; if (command === "help" || command === "--help" || command === "-h") { diff --git a/src/cint/decision.js b/src/cint/decision.js deleted file mode 100644 index 0681bc3..0000000 --- a/src/cint/decision.js +++ /dev/null @@ -1,67 +0,0 @@ -import { - assertCint, - assertExactKeys, - canonicalDigest, - identifier, - isoInstant, - sealRecord -} from "./canonical.js"; -import { runCounterIntentChallenge } from "./challenge.js"; - -export function decide(input) { - assertExactKeys( - input, - ["id", "intent", "principal", "authority", "policy", "adapter_capability", "machine_state", "now", "expires_at"], - [], - "decision request" - ); - const issuedAt = isoInstant(input.now, "decision issued_at"); - const expiresAt = isoInstant(input.expires_at, "decision expires_at"); - assertCint(Date.parse(expiresAt) > Date.parse(issuedAt), "CINT_DECISION_TIME", "Decision expiry must follow issuance"); - assertCint( - Date.parse(expiresAt) <= Date.parse(input.authority.expires_at), - "CINT_DECISION_TIME", - "Decision cannot outlive authority" - ); - const challenge = runCounterIntentChallenge({ - intent: input.intent, - principal: input.principal, - authority: input.authority, - policy: input.policy, - adapter_capability: input.adapter_capability, - machine_state: input.machine_state, - now: issuedAt - }); - const status = challenge.status === "CLEAR" ? "ADMIT" : challenge.status === "REVIEW" ? "REVIEW" : "DENY"; - const binding = { - intent_digest: input.intent.digest, - action_digest: input.intent.action_digest, - target_digest: input.intent.target_digest, - context_digest: input.intent.context_digest, - principal_digest: input.principal.digest, - authority_id: input.authority.id, - authority_digest: input.authority.digest, - authority_epoch: input.authority.epoch, - policy_id: input.policy.id, - policy_digest: input.policy.digest, - policy_epoch: input.policy.epoch, - adapter_id: input.adapter_capability.id, - adapter_capability_digest: input.adapter_capability.digest, - machine_state_id: input.machine_state.id, - machine_state_epoch: input.machine_state.epoch, - machine_state_digest: input.machine_state.state_digest - }; - return sealRecord({ - protocol: "cint/decision/1", - id: identifier(input.id, "decision.id"), - status, - issued_at: issuedAt, - expires_at: expiresAt, - binding, - binding_digest: canonicalDigest(binding), - challenge_digest: challenge.digest, - reason_codes: challenge.reasons.map((reason) => reason.code), - receipt_eligible: status === "ADMIT", - execution_authority: "NONE" - }); -} diff --git a/src/cint/decision.ts b/src/cint/decision.ts new file mode 100644 index 0000000..ed6a9ac --- /dev/null +++ b/src/cint/decision.ts @@ -0,0 +1,94 @@ +import { + assertCint, + assertExactKeys, + canonicalDigest, + identifier, + isoInstant, + sealRecord, + verifyProtocolRecord +} from "./canonical.js"; +import { runCounterIntentChallenge } from "./challenge.js"; +import type { BindingDigest, DecisionId } from "./types/brands.js"; +import { assertNever, type DecisionStatus } from "./types/states.js"; +import type { ChallengeRecord, Decision } from "./types/records.js"; + +function statusFromChallenge(status: ChallengeRecord["status"]): DecisionStatus { + switch (status) { + case "CLEAR": return "ADMIT"; + case "DENIED": return "DENY"; + case "REVIEW": return "REVIEW"; + default: return assertNever(status); + } +} + +export function decide(value: unknown): Decision { + const input = assertExactKeys( + value, + ["id", "intent", "principal", "authority", "policy", "adapter_capability", "machine_state", "now", "expires_at"], + [], + "decision request" + ); + const intent = verifyProtocolRecord(input["intent"], "cint/intent/1", "intent"); + const principal = verifyProtocolRecord(input["principal"], "cint/principal/1", "principal"); + const authority = verifyProtocolRecord(input["authority"], "cint/authority/1", "authority"); + const policy = verifyProtocolRecord(input["policy"], "cint/policy/1", "policy"); + const adapterCapability = verifyProtocolRecord( + input["adapter_capability"], + "cint/adapter-capability/1", + "adapter capability" + ); + const machineState = verifyProtocolRecord(input["machine_state"], "cint/machine-state/1", "machine state"); + const issuedAt = isoInstant(input["now"], "decision issued_at"); + const expiresAt = isoInstant(input["expires_at"], "decision expires_at"); + assertCint(Date.parse(expiresAt) > Date.parse(issuedAt), "CINT_DECISION_TIME", "Decision expiry must follow issuance"); + assertCint( + Date.parse(expiresAt) <= Date.parse(authority.expires_at), + "CINT_DECISION_TIME", + "Decision cannot outlive authority" + ); + const challenge = runCounterIntentChallenge({ + intent, + principal, + authority, + policy, + adapter_capability: adapterCapability, + machine_state: machineState, + now: issuedAt + }); + const status = statusFromChallenge(challenge.status); + const binding = { + intent_digest: intent.digest, + action_digest: intent.action_digest, + target_digest: intent.target_digest, + context_digest: intent.context_digest, + principal_digest: principal.digest, + authority_id: authority.id, + authority_digest: authority.digest, + authority_epoch: authority.epoch, + policy_id: policy.id, + policy_digest: policy.digest, + policy_epoch: policy.epoch, + adapter_id: adapterCapability.id, + adapter_capability_digest: adapterCapability.digest, + machine_state_id: machineState.id, + machine_state_epoch: machineState.epoch, + machine_state_digest: machineState.state_digest + }; + const record = { + protocol: "cint/decision/1" as const, + id: identifier(input["id"], "decision.id"), + issued_at: issuedAt, + expires_at: expiresAt, + binding, + binding_digest: canonicalDigest(binding), + challenge_digest: challenge.digest, + reason_codes: challenge.reasons.map((reason) => reason.code), + execution_authority: "NONE" as const + }; + switch (status) { + case "ADMIT": return sealRecord({ ...record, status, receipt_eligible: true as const }); + case "DENY": return sealRecord({ ...record, status, receipt_eligible: false as const }); + case "REVIEW": return sealRecord({ ...record, status, receipt_eligible: false as const }); + default: return assertNever(status); + } +} diff --git a/src/cint/evidence.js b/src/cint/evidence.js deleted file mode 100644 index 52f247b..0000000 --- a/src/cint/evidence.js +++ /dev/null @@ -1,85 +0,0 @@ -import { mkdir, open, readFile, unlink } from "node:fs/promises"; -import path from "node:path"; - -import { canonicalJson } from "../util.js"; -import { - CintError, - assertExactKeys, - canonicalDigest, - identifier, - isoInstant, - parseCanonicalJson, - sealRecord, - verifySealedRecord -} from "./canonical.js"; - -export class ExecutionLedger { - constructor(filePath) { - this.file = path.resolve(filePath); - this.lock = `${this.file}.lock`; - } - - async #entries() { - const text = await readFile(this.file, "utf8").catch((error) => { - if (error.code === "ENOENT") return ""; - throw error; - }); - const entries = text - .split(/\r?\n/) - .filter(Boolean) - .map((line, index) => parseCanonicalJson(line, `ledger line ${index + 1}`)); - let previous = null; - for (let index = 0; index < entries.length; index += 1) { - const entry = entries[index]; - verifySealedRecord(entry, `ledger entry ${index + 1}`); - if (entry.sequence !== index + 1 || entry.previous_digest !== previous) { - throw new CintError("CINT_LEDGER_CHAIN_INVALID", "Execution ledger chain is invalid"); - } - previous = entry.digest; - } - return entries; - } - - async record(input) { - assertExactKeys(input, ["event_id", "type", "at", "payload"], [], "ledger event"); - await mkdir(path.dirname(this.file), { recursive: true, mode: 0o700 }); - let lock; - try { - lock = await open(this.lock, "wx", 0o600); - await lock.writeFile(canonicalJson({ event_id: input.event_id, at: input.at }), "utf8"); - await lock.sync(); - } catch (error) { - if (lock) await lock.close().catch(() => {}); - if (error.code === "EEXIST") throw new CintError("CINT_LEDGER_LOCKED", "Execution ledger is already in use"); - throw error; - } - try { - const entries = await this.#entries(); - const entry = sealRecord({ - protocol: "cint/execution-ledger/1", - sequence: entries.length + 1, - event_id: identifier(input.event_id, "ledger event id"), - type: identifier(input.type, "ledger event type"), - at: isoInstant(input.at, "ledger event time"), - previous_digest: entries.at(-1)?.digest ?? null, - payload_digest: canonicalDigest(input.payload) - }); - const handle = await open(this.file, "a", 0o600); - try { - await handle.writeFile(`${canonicalJson(entry)}\n`, "utf8"); - await handle.sync(); - } finally { - await handle.close(); - } - return entry; - } finally { - await lock.close().catch(() => {}); - await unlink(this.lock).catch(() => {}); - } - } - - async head() { - const entries = await this.#entries(); - return entries.at(-1) ?? null; - } -} diff --git a/src/cint/evidence.ts b/src/cint/evidence.ts new file mode 100644 index 0000000..21bf0a2 --- /dev/null +++ b/src/cint/evidence.ts @@ -0,0 +1,144 @@ +import { mkdir, open, readFile, unlink } from "node:fs/promises"; +import path from "node:path"; +import type { FileHandle } from "node:fs/promises"; + +import { + CintError, + assertCint, + assertExactKeys, + assertJsonValue, + canonicalDigest, + canonicalJson, + identifier, + integer, + isoInstant, + parseCanonicalJson, + sealRecord, + sha256Digest, + verifySealedRecord +} from "./canonical.js"; +import type { + CanonicalInstant, + Identifier, + RecordDigest +} from "./types/brands.js"; +import type { SealedRecord } from "./types/records.js"; + +export interface ExecutionLedgerEntry extends SealedRecord<"cint/execution-ledger/1"> { + readonly sequence: number; + readonly event_id: Identifier; + readonly type: Identifier; + readonly at: CanonicalInstant; + readonly previous_digest: RecordDigest | null; + readonly payload_digest: RecordDigest; +} + +function property(value: unknown, name: string): unknown { + return value !== null && (typeof value === "object" || typeof value === "function") + ? Reflect.get(value, name) + : undefined; +} + +function errorCode(error: unknown): unknown { + return property(error, "code"); +} + +export function verifyExecutionLedgerEntry(value: unknown, label = "ledger entry"): ExecutionLedgerEntry { + const record = verifySealedRecord(value, label); + assertCint( + record["protocol"] === "cint/execution-ledger/1", + "CINT_PROTOCOL_INVALID", + `${label} uses an unsupported protocol` + ); + const rawPreviousDigest = record["previous_digest"]; + return { + protocol: "cint/execution-ledger/1", + sequence: integer(record["sequence"], `${label}.sequence`, { minimum: 1 }), + event_id: identifier(record["event_id"], `${label}.event_id`), + type: identifier(record["type"], `${label}.type`), + at: isoInstant(record["at"], `${label}.at`), + previous_digest: rawPreviousDigest === null + ? null + : sha256Digest(rawPreviousDigest, `${label}.previous_digest`), + payload_digest: sha256Digest(record["payload_digest"], `${label}.payload_digest`), + digest: sha256Digest(record["digest"], `${label}.digest`) + }; +} + +export class ExecutionLedger { + readonly file: string; + readonly lock: string; + + constructor(filePath: string) { + this.file = path.resolve(filePath); + this.lock = `${this.file}.lock`; + } + + async #entries(): Promise { + const text = await readFile(this.file, "utf8").catch((error: unknown) => { + if (errorCode(error) === "ENOENT") return ""; + throw error; + }); + const values = text + .split(/\r?\n/) + .filter(Boolean) + .map((line, index) => verifyExecutionLedgerEntry(parseCanonicalJson(line, `ledger line ${index + 1}`), `ledger entry ${index + 1}`)); + let previous: RecordDigest | null = null; + for (let index = 0; index < values.length; index += 1) { + const entry = values[index]; + assertCint(entry !== undefined, "CINT_LEDGER_CHAIN_INVALID", "Execution ledger entry is unavailable"); + if (entry.sequence !== index + 1 || entry.previous_digest !== previous) { + throw new CintError("CINT_LEDGER_CHAIN_INVALID", "Execution ledger chain is invalid"); + } + previous = entry.digest; + } + return values; + } + + async record(value: unknown): Promise { + const input = assertExactKeys(value, ["event_id", "type", "at", "payload"], [], "ledger event"); + await mkdir(path.dirname(this.file), { recursive: true, mode: 0o700 }); + let lock: FileHandle | undefined; + try { + lock = await open(this.lock, "wx", 0o600); + await lock.writeFile(canonicalJson(assertJsonValue({ + event_id: input["event_id"], + at: input["at"] + })), "utf8"); + await lock.sync(); + } catch (error) { + if (lock) await lock.close().catch(() => {}); + if (errorCode(error) === "EEXIST") throw new CintError("CINT_LEDGER_LOCKED", "Execution ledger is already in use"); + throw error; + } + try { + const entries = await this.#entries(); + const entry = sealRecord({ + protocol: "cint/execution-ledger/1" as const, + sequence: entries.length + 1, + event_id: identifier(input["event_id"], "ledger event id"), + type: identifier(input["type"], "ledger event type"), + at: isoInstant(input["at"], "ledger event time"), + previous_digest: entries.at(-1)?.digest ?? null, + payload_digest: canonicalDigest(input["payload"]) + }); + const handle = await open(this.file, "a", 0o600); + try { + await handle.writeFile(`${canonicalJson(assertJsonValue(entry))}\n`, "utf8"); + await handle.sync(); + } finally { + await handle.close(); + } + return entry; + } finally { + assertCint(lock !== undefined, "CINT_LEDGER_LOCKED", "Execution ledger lock was not acquired"); + await lock.close().catch(() => {}); + await unlink(this.lock).catch(() => {}); + } + } + + async head(): Promise { + const entries = await this.#entries(); + return entries.at(-1) ?? null; + } +} diff --git a/src/cint/execution.js b/src/cint/execution.js deleted file mode 100644 index 0d4737b..0000000 --- a/src/cint/execution.js +++ /dev/null @@ -1,362 +0,0 @@ -import { randomUUID } from "node:crypto"; - -import { - assertCint, - assertExactKeys, - isoInstant, - sealRecord, - verifyProtocolRecord, - verifySealedRecord -} from "./canonical.js"; -import { createOutcome } from "./outcome.js"; -import { revalidateReceipt } from "./revalidation.js"; -import { performRollback } from "./rollback.js"; - -function eventId(type) { - return `event.${type.toLowerCase()}.${randomUUID()}`; -} - -function trustedExecutionTime(clock) { - return isoInstant(clock.now(), "trusted execution time"); -} - -function availableExecutionRuntime(input) { - try { - verifyProtocolRecord(input.adapter?.capability, "cint/adapter-capability/1", "adapter capability"); - } catch { - return false; - } - const capability = input.adapter.capability; - return ( - typeof input.snapshot_provider === "function" && - input.clock?.now instanceof Function && - input.receipt_authority?.verify instanceof Function && - input.store?.consume instanceof Function && - typeof input.adapter?.id === "string" && - input.adapter.id === capability.id && - capability.prepare_side_effect_free === true && - capability.outcome_verification === true && - input.adapter.prepare instanceof Function && - input.adapter.execute instanceof Function && - input.adapter.verify instanceof Function && - (capability.rollback !== true || input.adapter.rollback instanceof Function) && - input.seal_authority?.issue instanceof Function && - input.seal_authority?.verify instanceof Function && - input.ledger?.record instanceof Function - ); -} - -function failureResult({ receipt, status, code, at, consumption = null, revalidation = null, actionStarted = false }) { - return sealRecord({ - protocol: "cint/execution-result/1", - status, - receipt_id: receipt?.id ?? null, - receipt_digest: receipt?.digest ?? null, - consumption_digest: consumption?.digest ?? null, - revalidation_digest: revalidation?.digest ?? null, - action_started: actionStarted, - outcome: null, - evidence_seal: null, - error_code: code, - completed_at: at - }); -} - -function statusForError(error, revalidation) { - if (error?.code === "CINT_RECEIPT_REPLAY_REJECTED") return "REPLAY_REJECTED"; - if (revalidation?.status === "REJECTED") return "REJECTED"; - if (revalidation?.status === "REVOKED" || error?.code === "CINT_RECEIPT_REVOKED") return "REVOKED"; - return "FAIL_CLOSED"; -} - -async function record(ledger, type, at, payload) { - return ledger.record({ event_id: eventId(type), type, at, payload }); -} - -async function sealedResult({ - input, - consumption, - revalidation, - outcome, - status, - at, - errorCode = null, - actionStarted = true -}) { - const outcomeEvent = await record(input.ledger, `OUTCOME_${outcome.status}`, at, { - receipt_digest: input.receipt.digest, - outcome_digest: outcome.digest - }); - const seal = input.seal_authority.issue({ - receipt: input.receipt, - consumption, - revalidation, - outcome, - ledger_head: outcomeEvent, - issued_at: at - }); - input.seal_authority.verify(seal); - return sealRecord({ - protocol: "cint/execution-result/1", - status, - receipt_id: input.receipt.id, - receipt_digest: input.receipt.digest, - consumption_digest: consumption.digest, - revalidation_digest: revalidation.digest, - action_started: actionStarted, - outcome, - evidence_seal: seal, - error_code: errorCode, - completed_at: at - }); -} - -export async function executeWithReceipt(input) { - assertExactKeys( - input, - [ - "receipt", - "receipt_authority", - "store", - "snapshot_provider", - "clock", - "adapter", - "seal_authority", - "ledger", - "at" - ], - ["signal"], - "execution request" - ); - const at = isoInstant(input.at, "execution time"); - if (!availableExecutionRuntime(input)) { - return failureResult({ - receipt: input.receipt, - status: "FAIL_CLOSED", - code: "CINT_UNAVAILABLE", - at, - actionStarted: false - }); - } - try { - verifyProtocolRecord(input.receipt, "cint/decision-receipt/1", "receipt"); - } catch (error) { - return failureResult({ - receipt: input.receipt, - status: "REJECTED", - code: error.code ?? "CINT_RECEIPT_INVALID", - at, - actionStarted: false - }); - } - - let lockedSnapshot = null; - let consumption = null; - let revalidation = null; - try { - input.receipt_authority.verify(input.receipt, { now: at }); - await record(input.ledger, "RECEIPT_PRESENTED", at, { receipt_digest: input.receipt.digest }); - consumption = await input.store.consume(input.receipt, { - consumed_at: at, - revalidate: async () => { - lockedSnapshot = await input.snapshot_provider(); - revalidation = revalidateReceipt({ - receipt: input.receipt, - receipt_authority: input.receipt_authority, - ...lockedSnapshot, - now: at - }); - return revalidation; - } - }); - await record(input.ledger, "RECEIPT_CONSUMED", at, { - receipt_digest: input.receipt.digest, - consumption_digest: consumption.digest, - revalidation_digest: revalidation.digest - }); - } catch (error) { - return failureResult({ - receipt: input.receipt, - status: statusForError(error, revalidation), - code: error.code ?? "CINT_FAIL_CLOSED", - at, - consumption, - revalidation, - actionStarted: false - }); - } - - let currentSnapshot; - try { - currentSnapshot = await input.snapshot_provider(); - revalidation = revalidateReceipt({ - receipt: input.receipt, - receipt_authority: input.receipt_authority, - ...currentSnapshot, - now: at - }); - if (revalidation.status !== "VALID") { - return failureResult({ - receipt: input.receipt, - status: statusForError(null, revalidation), - code: revalidation.reason_codes[0] ?? "CINT_REVALIDATION_FAILED", - at, - consumption, - revalidation, - actionStarted: false - }); - } - assertCint( - input.adapter.id === currentSnapshot.adapter_capability.id && - input.adapter.capability.digest === currentSnapshot.adapter_capability.digest, - "CINT_ADAPTER_MISMATCH", - "Runtime adapter does not match the current capability" - ); - await record(input.ledger, "EXECUTION_REVALIDATED", at, { - receipt_digest: input.receipt.digest, - revalidation_digest: revalidation.digest - }); - } catch (error) { - return failureResult({ - receipt: input.receipt, - status: "FAIL_CLOSED", - code: error.code ?? "CINT_FAIL_CLOSED", - at, - consumption, - revalidation, - actionStarted: false - }); - } - - let prepared = null; - let execution = null; - let actionStarted = false; - let executionAt = at; - try { - prepared = await input.adapter.prepare(currentSnapshot.intent, { at, signal: input.signal }); - await record(input.ledger, "EXECUTION_PREPARED", at, { - receipt_digest: input.receipt.digest, - action_digest: currentSnapshot.intent.action_digest, - target_digest: currentSnapshot.intent.target_digest - }); - - currentSnapshot = await input.snapshot_provider(); - executionAt = trustedExecutionTime(input.clock); - revalidation = revalidateReceipt({ - receipt: input.receipt, - receipt_authority: input.receipt_authority, - ...currentSnapshot, - now: executionAt - }); - if (revalidation.status !== "VALID") { - return failureResult({ - receipt: input.receipt, - status: statusForError(null, revalidation), - code: revalidation.reason_codes[0] ?? "CINT_REVALIDATION_FAILED", - at: executionAt, - consumption, - revalidation, - actionStarted: false - }); - } - assertCint( - input.adapter.id === currentSnapshot.adapter_capability.id && - input.adapter.capability.digest === currentSnapshot.adapter_capability.digest, - "CINT_ADAPTER_MISMATCH", - "Runtime adapter does not match the execution-bound capability" - ); - actionStarted = true; - execution = await input.adapter.execute(prepared, { at: executionAt, signal: input.signal }); - verifySealedRecord(execution, "adapter execution"); - await record(input.ledger, "EXECUTION_COMPLETED", executionAt, { - receipt_digest: input.receipt.digest, - execution_digest: execution.digest, - revalidation_digest: revalidation.digest - }); - const verification = await input.adapter.verify(prepared, execution, { at: executionAt, signal: input.signal }); - verifySealedRecord(verification, "outcome verification"); - if (verification.status === "VERIFIED") { - const outcome = createOutcome({ - receipt: input.receipt, - execution, - verification, - rollback: null, - completed_at: executionAt - }); - return await sealedResult({ input, consumption, revalidation, outcome, status: "SEALED", at: executionAt }); - } - const rollback = await performRollback(input.adapter, prepared, { at: executionAt, signal: input.signal }); - assertCint(rollback.status === "RESTORED", "CINT_ROLLBACK_FAILED", "Divergent outcome could not be restored"); - const outcome = createOutcome({ - receipt: input.receipt, - execution, - verification, - rollback, - completed_at: executionAt - }); - return await sealedResult({ input, consumption, revalidation, outcome, status: "ROLLED_BACK", at: executionAt }); - } catch (error) { - if (prepared && actionStarted) { - const interruptedExecution = execution ?? sealRecord({ - protocol: "cint/execution-interruption/1", - status: "INTERRUPTED", - error_code: error.code ?? "CINT_EXECUTION_FAILED", - interrupted_at: executionAt - }); - let verification; - try { - verification = await input.adapter.verify(prepared, interruptedExecution, { at: executionAt, signal: input.signal }); - verifySealedRecord(verification, "outcome verification"); - } catch { - verification = sealRecord({ - protocol: "cint/outcome-verification/1", - status: "DIVERGED", - target: "unknown", - expected_sha256: prepared.after_sha256 ?? "0".repeat(64), - actual_sha256: "0".repeat(64), - checked_at: executionAt - }); - } - const rollback = await performRollback(input.adapter, prepared, { at: executionAt, signal: input.signal }); - if (rollback.status === "RESTORED") { - const outcome = createOutcome({ - receipt: input.receipt, - execution: interruptedExecution, - verification, - rollback, - completed_at: executionAt - }); - try { - return await sealedResult({ - input, - consumption, - revalidation, - outcome, - status: "ROLLED_BACK", - at: executionAt, - errorCode: error.code ?? "CINT_EXECUTION_FAILED" - }); - } catch (sealError) { - return failureResult({ - receipt: input.receipt, - status: "FAIL_CLOSED", - code: sealError.code ?? "CINT_SEAL_FAILED", - at: executionAt, - consumption, - revalidation, - actionStarted - }); - } - } - } - return failureResult({ - receipt: input.receipt, - status: "FAIL_CLOSED", - code: error.code ?? "CINT_FAIL_CLOSED", - at: executionAt, - consumption, - revalidation, - actionStarted - }); - } -} diff --git a/src/cint/execution.ts b/src/cint/execution.ts new file mode 100644 index 0000000..a1e8d55 --- /dev/null +++ b/src/cint/execution.ts @@ -0,0 +1,599 @@ +import { randomUUID } from "node:crypto"; + +import { + assertCint, + assertExactKeys, + identifier, + isoInstant, + sealRecord, + sha256Digest, + verifyProtocolRecord, + verifySealedRecord +} from "./canonical.js"; +import type { ExecutionLedgerEntry } from "./evidence.js"; +import { + createOutcome, + verifyOutcomeVerification +} from "./outcome.js"; +import { revalidateReceipt, type ReceiptVerifier } from "./revalidation.js"; +import { performRollback } from "./rollback.js"; +import type { OutcomeSealInput } from "./seal.js"; +import type { + AdapterId, + CanonicalInstant, + ExecutionDigest, + Identifier +} from "./types/brands.js"; +import type { AdapterOperationOptions } from "./types/adapters.js"; +import type { + AdapterCapability, + AuthorityRecord, + ConsumedReceiptRecord, + EvidenceSeal, + ExecutionResult, + FailedExecutionResult, + IntentRecord, + IssuedDecisionReceipt, + MachineStateSnapshot, + Outcome, + OutcomeVerification, + PolicySnapshot, + PrincipalRecord, + Revalidation, + ValidRevalidation +} from "./types/records.js"; +import type { JsonRecord } from "./canonical.js"; + +export interface TrustedExecutionClock { + now(): unknown; +} + +export interface ExecutionSnapshot { + readonly intent: IntentRecord; + readonly principal: PrincipalRecord; + readonly authority: AuthorityRecord; + readonly policy: PolicySnapshot; + readonly adapter_capability: AdapterCapability; + readonly machine_state: MachineStateSnapshot; +} + +export interface ExecutionAdapter { + readonly id: AdapterId | string; + readonly capability: AdapterCapability; + prepare(intent: IntentRecord, options: AdapterOperationOptions): Promise; + execute(prepared: unknown, options: AdapterOperationOptions): Promise; + verify(prepared: unknown, execution: unknown, options: AdapterOperationOptions): Promise; + rollback?(prepared: unknown, options: AdapterOperationOptions): Promise; +} + +export interface ReceiptConsumptionStore { + consume( + receipt: IssuedDecisionReceipt, + input: { + readonly consumed_at: unknown; + readonly revalidate: (receipt: IssuedDecisionReceipt) => Promise; + } + ): Promise; +} + +export interface ExecutionSealAuthority { + issue(input: OutcomeSealInput): EvidenceSeal; + verify(seal: unknown): EvidenceSeal; +} + +export interface ExecutionLedgerBoundary { + record(input: unknown): Promise; +} + +export interface ExecuteWithReceiptInput { + readonly receipt: IssuedDecisionReceipt; + readonly receipt_authority: ReceiptVerifier; + readonly store: ReceiptConsumptionStore; + readonly snapshot_provider: () => Promise; + readonly clock: TrustedExecutionClock; + readonly adapter: ExecutionAdapter; + readonly seal_authority: ExecutionSealAuthority; + readonly ledger: ExecutionLedgerBoundary; + readonly at: unknown; + readonly signal?: AbortSignal | undefined; +} + +type AvailableExecutionRuntime = JsonRecord & { + readonly receipt: unknown; + readonly receipt_authority: ReceiptVerifier; + readonly store: ReceiptConsumptionStore; + readonly snapshot_provider: () => Promise; + readonly clock: TrustedExecutionClock; + readonly adapter: ExecutionAdapter; + readonly seal_authority: ExecutionSealAuthority; + readonly ledger: ExecutionLedgerBoundary; + readonly at: unknown; + readonly signal?: AbortSignal | undefined; +}; + +interface AdapterExecutionEvidence { + readonly raw: unknown; + readonly digest: ExecutionDigest; +} + +interface FailureInput { + readonly receipt: unknown; + readonly status: FailedExecutionResult["status"]; + readonly code: string; + readonly at: CanonicalInstant; + readonly consumption?: ConsumedReceiptRecord | null; + readonly revalidation?: Revalidation | null; + readonly actionStarted?: boolean; +} + +interface SealedResultInput { + readonly runtime: AvailableExecutionRuntime; + readonly receipt: IssuedDecisionReceipt; + readonly consumption: ConsumedReceiptRecord; + readonly revalidation: ValidRevalidation; + readonly outcome: Outcome; + readonly status: "SEALED" | "ROLLED_BACK"; + readonly at: CanonicalInstant; + readonly errorCode?: string | null; + readonly actionStarted?: boolean; +} + +function property(value: unknown, name: string): unknown { + return value !== null && (typeof value === "object" || typeof value === "function") + ? Reflect.get(value, name) + : undefined; +} + +function errorCode(error: unknown, fallback: string): string { + const code = property(error, "code"); + return typeof code === "string" ? code : fallback; +} + +function eventId(type: string): Identifier { + return identifier(`event.${type.toLowerCase()}.${randomUUID()}`, "ledger event id"); +} + +function trustedExecutionTime(clock: TrustedExecutionClock): CanonicalInstant { + return isoInstant(clock.now(), "trusted execution time"); +} + +function operationOptions(at: CanonicalInstant, signal: AbortSignal | undefined): AdapterOperationOptions { + return signal === undefined ? { at } : { at, signal }; +} + +function availableExecutionRuntime(input: JsonRecord): input is AvailableExecutionRuntime { + const adapter = input["adapter"]; + const capabilityValue = property(adapter, "capability"); + let capability: AdapterCapability; + try { + capability = verifyProtocolRecord(capabilityValue, "cint/adapter-capability/1", "adapter capability"); + } catch { + return false; + } + const clock = input["clock"]; + const receiptAuthority = input["receipt_authority"]; + const store = input["store"]; + const sealAuthority = input["seal_authority"]; + const ledger = input["ledger"]; + return ( + typeof input["snapshot_provider"] === "function" && + typeof property(clock, "now") === "function" && + typeof property(receiptAuthority, "verify") === "function" && + typeof property(store, "consume") === "function" && + typeof property(adapter, "id") === "string" && + property(adapter, "id") === capability.id && + capability.prepare_side_effect_free === true && + capability.outcome_verification === true && + typeof property(adapter, "prepare") === "function" && + typeof property(adapter, "execute") === "function" && + typeof property(adapter, "verify") === "function" && + (capability.rollback !== true || typeof property(adapter, "rollback") === "function") && + typeof property(sealAuthority, "issue") === "function" && + typeof property(sealAuthority, "verify") === "function" && + typeof property(ledger, "record") === "function" + ); +} + +function failureResult(input: FailureInput): FailedExecutionResult { + const sealed = sealRecord({ + protocol: "cint/execution-result/1" as const, + status: input.status, + receipt_id: property(input.receipt, "id") ?? null, + receipt_digest: property(input.receipt, "digest") ?? null, + consumption_digest: input.consumption?.digest ?? null, + revalidation_digest: input.revalidation?.digest ?? null, + action_started: input.actionStarted ?? false, + outcome: null, + evidence_seal: null, + error_code: input.code, + completed_at: input.at + }); + const result = verifyProtocolRecord(sealed, "cint/execution-result/1", "execution failure"); + assertCint( + result.status === "REPLAY_REJECTED" || + result.status === "REJECTED" || + result.status === "REVOKED" || + result.status === "FAIL_CLOSED", + "CINT_FAIL_CLOSED", + "Execution failure has a non-failure status" + ); + return result; +} + +function statusForError(error: unknown, revalidation: Revalidation | null): FailedExecutionResult["status"] { + if (property(error, "code") === "CINT_RECEIPT_REPLAY_REJECTED") return "REPLAY_REJECTED"; + if (revalidation?.status === "REJECTED") return "REJECTED"; + if (revalidation?.status === "REVOKED" || property(error, "code") === "CINT_RECEIPT_REVOKED") return "REVOKED"; + return "FAIL_CLOSED"; +} + +function requiredRevalidation(value: Revalidation | null): Revalidation { + assertCint(value !== null, "CINT_REVALIDATION_INVALID", "Receipt consumption returned without revalidation"); + return value; +} + +async function record( + ledger: ExecutionLedgerBoundary, + type: string, + at: CanonicalInstant, + payload: unknown +): Promise { + return ledger.record({ event_id: eventId(type), type, at, payload }); +} + +function snapshotRevalidation( + receipt: IssuedDecisionReceipt, + receiptAuthority: ReceiptVerifier, + snapshot: unknown, + now: CanonicalInstant +): Revalidation { + return revalidateReceipt({ + receipt, + receipt_authority: receiptAuthority, + intent: property(snapshot, "intent"), + principal: property(snapshot, "principal"), + authority: property(snapshot, "authority"), + policy: property(snapshot, "policy"), + adapter_capability: property(snapshot, "adapter_capability"), + machine_state: property(snapshot, "machine_state"), + now + }); +} + +function executionSnapshot(value: unknown): ExecutionSnapshot { + return { + intent: verifyProtocolRecord(property(value, "intent"), "cint/intent/1", "intent"), + principal: verifyProtocolRecord(property(value, "principal"), "cint/principal/1", "principal"), + authority: verifyProtocolRecord(property(value, "authority"), "cint/authority/1", "authority"), + policy: verifyProtocolRecord(property(value, "policy"), "cint/policy/1", "policy"), + adapter_capability: verifyProtocolRecord( + property(value, "adapter_capability"), + "cint/adapter-capability/1", + "adapter capability" + ), + machine_state: verifyProtocolRecord(property(value, "machine_state"), "cint/machine-state/1", "machine state") + }; +} + +function adapterExecution(value: unknown): AdapterExecutionEvidence { + const record = verifySealedRecord(value, "adapter execution"); + const protocol = record["protocol"]; + assertCint( + protocol === "cint/synthetic-execution/1" || + protocol === "cint/codex-delegation-execution/1" || + protocol === "cint/execution-interruption/1", + "CINT_PROTOCOL_INVALID", + "Adapter execution uses an unsupported protocol" + ); + return { + raw: value, + digest: sha256Digest(record["digest"], "adapter execution digest") + }; +} + +async function sealedResult(input: SealedResultInput): Promise { + assertCint( + (input.status === "SEALED" && input.outcome.status === "VERIFIED") || + (input.status === "ROLLED_BACK" && input.outcome.status === "ROLLED_BACK"), + "CINT_FAIL_CLOSED", + "Execution result status does not match its outcome" + ); + const outcomeEvent = await record(input.runtime.ledger, `OUTCOME_${input.outcome.status}`, input.at, { + receipt_digest: input.receipt.digest, + outcome_digest: input.outcome.digest + }); + const seal = input.runtime.seal_authority.issue({ + receipt: input.receipt, + consumption: input.consumption, + revalidation: input.revalidation, + outcome: input.outcome, + ledger_head: outcomeEvent, + issued_at: input.at + }); + input.runtime.seal_authority.verify(seal); + return verifyProtocolRecord(sealRecord({ + protocol: "cint/execution-result/1" as const, + status: input.status, + receipt_id: input.receipt.id, + receipt_digest: input.receipt.digest, + consumption_digest: input.consumption.digest, + revalidation_digest: input.revalidation.digest, + action_started: input.actionStarted ?? true, + outcome: input.outcome, + evidence_seal: seal, + error_code: input.errorCode ?? null, + completed_at: input.at + }), "cint/execution-result/1", "execution result"); +} + +export function executeWithReceipt(input: ExecuteWithReceiptInput): Promise; +export async function executeWithReceipt(value: unknown): Promise { + const input = assertExactKeys( + value, + [ + "receipt", + "receipt_authority", + "store", + "snapshot_provider", + "clock", + "adapter", + "seal_authority", + "ledger", + "at" + ], + ["signal"], + "execution request" + ); + const at = isoInstant(input["at"], "execution time"); + if (!availableExecutionRuntime(input)) { + return failureResult({ + receipt: input["receipt"], + status: "FAIL_CLOSED", + code: "CINT_UNAVAILABLE", + at, + actionStarted: false + }); + } + const runtime = input; + let receipt: IssuedDecisionReceipt; + try { + receipt = verifyProtocolRecord(runtime.receipt, "cint/decision-receipt/1", "receipt"); + } catch (error) { + return failureResult({ + receipt: runtime.receipt, + status: "REJECTED", + code: errorCode(error, "CINT_RECEIPT_INVALID"), + at, + actionStarted: false + }); + } + + let lockedSnapshot: unknown = null; + let consumption: ConsumedReceiptRecord | null = null; + let revalidation: Revalidation | null = null; + try { + runtime.receipt_authority.verify(receipt, { now: at }); + await record(runtime.ledger, "RECEIPT_PRESENTED", at, { receipt_digest: receipt.digest }); + consumption = await runtime.store.consume(receipt, { + consumed_at: at, + revalidate: async () => { + lockedSnapshot = await runtime.snapshot_provider(); + revalidation = snapshotRevalidation(receipt, runtime.receipt_authority, lockedSnapshot, at); + return revalidation; + } + }); + const consumedRevalidation = requiredRevalidation(revalidation); + revalidation = consumedRevalidation; + await record(runtime.ledger, "RECEIPT_CONSUMED", at, { + receipt_digest: receipt.digest, + consumption_digest: consumption.digest, + revalidation_digest: consumedRevalidation.digest + }); + } catch (error) { + return failureResult({ + receipt, + status: statusForError(error, revalidation), + code: errorCode(error, "CINT_FAIL_CLOSED"), + at, + consumption, + revalidation, + actionStarted: false + }); + } + assertCint(consumption !== null, "CINT_FAIL_CLOSED", "Receipt consumption evidence is unavailable"); + + let currentSnapshot: ExecutionSnapshot; + let admittedRevalidation: ValidRevalidation; + try { + const rawSnapshot = await runtime.snapshot_provider(); + const checked = snapshotRevalidation(receipt, runtime.receipt_authority, rawSnapshot, at); + revalidation = checked; + if (checked.status !== "VALID") { + return failureResult({ + receipt, + status: statusForError(null, checked), + code: checked.reason_codes[0] ?? "CINT_REVALIDATION_FAILED", + at, + consumption, + revalidation: checked, + actionStarted: false + }); + } + admittedRevalidation = checked; + currentSnapshot = executionSnapshot(rawSnapshot); + assertCint( + runtime.adapter.id === currentSnapshot.adapter_capability.id && + runtime.adapter.capability.digest === currentSnapshot.adapter_capability.digest, + "CINT_ADAPTER_MISMATCH", + "Runtime adapter does not match the current capability" + ); + await record(runtime.ledger, "EXECUTION_REVALIDATED", at, { + receipt_digest: receipt.digest, + revalidation_digest: admittedRevalidation.digest + }); + } catch (error) { + return failureResult({ + receipt, + status: "FAIL_CLOSED", + code: errorCode(error, "CINT_FAIL_CLOSED"), + at, + consumption, + revalidation, + actionStarted: false + }); + } + + let prepared: unknown = null; + let execution: AdapterExecutionEvidence | null = null; + let actionStarted = false; + let executionAt = at; + try { + prepared = await runtime.adapter.prepare(currentSnapshot.intent, operationOptions(at, runtime.signal)); + await record(runtime.ledger, "EXECUTION_PREPARED", at, { + receipt_digest: receipt.digest, + action_digest: currentSnapshot.intent.action_digest, + target_digest: currentSnapshot.intent.target_digest + }); + + const finalSnapshotValue = await runtime.snapshot_provider(); + executionAt = trustedExecutionTime(runtime.clock); + const finalRevalidation = snapshotRevalidation(receipt, runtime.receipt_authority, finalSnapshotValue, executionAt); + revalidation = finalRevalidation; + if (finalRevalidation.status !== "VALID") { + return failureResult({ + receipt, + status: statusForError(null, finalRevalidation), + code: finalRevalidation.reason_codes[0] ?? "CINT_REVALIDATION_FAILED", + at: executionAt, + consumption, + revalidation: finalRevalidation, + actionStarted: false + }); + } + const finalSnapshot = executionSnapshot(finalSnapshotValue); + assertCint( + runtime.adapter.id === finalSnapshot.adapter_capability.id && + runtime.adapter.capability.digest === finalSnapshot.adapter_capability.digest, + "CINT_ADAPTER_MISMATCH", + "Runtime adapter does not match the execution-bound capability" + ); + actionStarted = true; + const rawExecution = await runtime.adapter.execute(prepared, operationOptions(executionAt, runtime.signal)); + execution = adapterExecution(rawExecution); + await record(runtime.ledger, "EXECUTION_COMPLETED", executionAt, { + receipt_digest: receipt.digest, + execution_digest: execution.digest, + revalidation_digest: finalRevalidation.digest + }); + const rawVerification = await runtime.adapter.verify( + prepared, + execution.raw, + operationOptions(executionAt, runtime.signal) + ); + const verification = verifyOutcomeVerification(rawVerification); + if (verification.status === "VERIFIED") { + const outcome = createOutcome({ + receipt, + execution: execution.raw, + verification, + rollback: null, + completed_at: executionAt + }); + return await sealedResult({ + runtime, + receipt, + consumption, + revalidation: finalRevalidation, + outcome, + status: "SEALED", + at: executionAt + }); + } + const rollback = await performRollback(runtime.adapter, prepared, operationOptions(executionAt, runtime.signal)); + assertCint(rollback.status === "RESTORED", "CINT_ROLLBACK_FAILED", "Divergent outcome could not be restored"); + const outcome = createOutcome({ + receipt, + execution: execution.raw, + verification, + rollback, + completed_at: executionAt + }); + return await sealedResult({ + runtime, + receipt, + consumption, + revalidation: finalRevalidation, + outcome, + status: "ROLLED_BACK", + at: executionAt + }); + } catch (error) { + if (prepared && actionStarted) { + const interruptedExecution = execution ?? adapterExecution(sealRecord({ + protocol: "cint/execution-interruption/1" as const, + status: "INTERRUPTED" as const, + error_code: errorCode(error, "CINT_EXECUTION_FAILED"), + interrupted_at: executionAt + })); + let verification: OutcomeVerification; + try { + verification = verifyOutcomeVerification(await runtime.adapter.verify( + prepared, + interruptedExecution.raw, + operationOptions(executionAt, runtime.signal) + )); + } catch { + verification = verifyOutcomeVerification(sealRecord({ + protocol: "cint/outcome-verification/1" as const, + status: "DIVERGED" as const, + target: "unknown", + expected_sha256: property(prepared, "after_sha256") ?? "0".repeat(64), + actual_sha256: "0".repeat(64), + checked_at: executionAt + })); + } + const rollback = await performRollback(runtime.adapter, prepared, operationOptions(executionAt, runtime.signal)); + if (rollback.status === "RESTORED") { + const outcome = createOutcome({ + receipt, + execution: interruptedExecution.raw, + verification, + rollback, + completed_at: executionAt + }); + try { + assertCint(revalidation?.status === "VALID", "CINT_REVALIDATION_INVALID", "Valid final revalidation is unavailable"); + return await sealedResult({ + runtime, + receipt, + consumption, + revalidation, + outcome, + status: "ROLLED_BACK", + at: executionAt, + errorCode: errorCode(error, "CINT_EXECUTION_FAILED") + }); + } catch (sealError) { + return failureResult({ + receipt, + status: "FAIL_CLOSED", + code: errorCode(sealError, "CINT_SEAL_FAILED"), + at: executionAt, + consumption, + revalidation, + actionStarted + }); + } + } + } + return failureResult({ + receipt, + status: "FAIL_CLOSED", + code: errorCode(error, "CINT_FAIL_CLOSED"), + at: executionAt, + consumption, + revalidation, + actionStarted + }); + } +} diff --git a/src/cint/index.js b/src/cint/index.ts similarity index 100% rename from src/cint/index.js rename to src/cint/index.ts diff --git a/src/cint/intent.js b/src/cint/intent.js deleted file mode 100644 index a6ad576..0000000 --- a/src/cint/intent.js +++ /dev/null @@ -1,71 +0,0 @@ -import { - assertCint, - assertExactKeys, - assertJsonValue, - boundedString, - canonicalDigest, - identifier, - immutableRecord, - isoInstant, - sealRecord, - stringArray -} from "./canonical.js"; - -const CONSEQUENCE_CLASSES = new Set(["READ_ONLY", "CONSEQUENTIAL"]); - -function actionRecord(value) { - assertExactKeys(value, ["adapter", "type", "target", "parameters", "consequence"], [], "intent.action"); - const action = { - adapter: identifier(value.adapter, "intent.action.adapter"), - type: identifier(value.type, "intent.action.type"), - target: assertJsonValue(value.target, "intent.action.target"), - parameters: assertJsonValue(value.parameters, "intent.action.parameters"), - consequence: boundedString(value.consequence, "intent.action.consequence", { minimum: 8, maximum: 32 }) - }; - assertCint( - CONSEQUENCE_CLASSES.has(action.consequence), - "CINT_CONSEQUENCE_INVALID", - `Unsupported consequence class: ${action.consequence}` - ); - return immutableRecord(action); -} - -export function createIntent(input) { - assertExactKeys( - input, - ["id", "principal_id", "request", "action", "declared_effects", "context", "uncertainties", "created_at"], - [], - "intent" - ); - const action = actionRecord(input.action); - const record = { - protocol: "cint/intent/1", - id: identifier(input.id, "intent.id"), - principal_id: identifier(input.principal_id, "intent.principal_id"), - request: - input.request === null - ? null - : boundedString(input.request, "intent.request", { minimum: 1, maximum: 8192 }), - action, - declared_effects: stringArray(input.declared_effects, "intent.declared_effects", { - minimum: 0, - maximum: 32, - bytes: 512 - }), - context: assertJsonValue(input.context, "intent.context"), - uncertainties: stringArray(input.uncertainties, "intent.uncertainties", { - minimum: 0, - maximum: 16, - bytes: 512 - }), - created_at: isoInstant(input.created_at, "intent.created_at"), - action_digest: canonicalDigest(action), - target_digest: canonicalDigest(action.target), - context_digest: canonicalDigest(input.context) - }; - return sealRecord(record); -} - -export function reconstructIntent(input) { - return createIntent(input); -} diff --git a/src/cint/intent.ts b/src/cint/intent.ts new file mode 100644 index 0000000..5ed0889 --- /dev/null +++ b/src/cint/intent.ts @@ -0,0 +1,86 @@ +import { + assertCint, + assertExactKeys, + assertJsonValue, + boundedString, + canonicalDigest, + identifier, + immutableRecord, + isoInstant, + sealRecord, + stringArray +} from "./canonical.js"; +import type { + ActionDigest, + AdapterId, + ContextDigest, + Identifier, + IntentId, + PrincipalId, + TargetDigest +} from "./types/brands.js"; +import type { CintAction, ConsequenceClass, IntentRecord } from "./types/records.js"; + +const CONSEQUENCE_CLASSES: ReadonlySet = new Set(["READ_ONLY", "CONSEQUENTIAL"]); + +function isConsequenceClass(value: string): value is ConsequenceClass { + return CONSEQUENCE_CLASSES.has(value); +} + +function actionRecord(value: unknown): CintAction { + const input = assertExactKeys(value, ["adapter", "type", "target", "parameters", "consequence"], [], "intent.action"); + const consequence = boundedString(input["consequence"], "intent.action.consequence", { minimum: 8, maximum: 32 }); + assertCint( + isConsequenceClass(consequence), + "CINT_CONSEQUENCE_INVALID", + `Unsupported consequence class: ${consequence}` + ); + return immutableRecord({ + adapter: identifier(input["adapter"], "intent.action.adapter"), + type: identifier(input["type"], "intent.action.type"), + target: assertJsonValue(input["target"], "intent.action.target"), + parameters: assertJsonValue(input["parameters"], "intent.action.parameters"), + consequence + }); +} + +export function createIntent(value: unknown): IntentRecord { + const input = assertExactKeys( + value, + ["id", "principal_id", "request", "action", "declared_effects", "context", "uncertainties", "created_at"], + [], + "intent" + ); + const action = actionRecord(input["action"]); + const context = assertJsonValue(input["context"], "intent.context"); + const record = { + protocol: "cint/intent/1" as const, + id: identifier(input["id"], "intent.id"), + principal_id: identifier(input["principal_id"], "intent.principal_id"), + request: + input["request"] === null + ? null + : boundedString(input["request"], "intent.request", { minimum: 1, maximum: 8192 }), + action, + declared_effects: stringArray(input["declared_effects"], "intent.declared_effects", { + minimum: 0, + maximum: 32, + bytes: 512 + }), + context, + uncertainties: stringArray(input["uncertainties"], "intent.uncertainties", { + minimum: 0, + maximum: 16, + bytes: 512 + }), + created_at: isoInstant(input["created_at"], "intent.created_at"), + action_digest: canonicalDigest(action), + target_digest: canonicalDigest(action.target), + context_digest: canonicalDigest(context) + }; + return sealRecord(record); +} + +export function reconstructIntent(input: unknown): IntentRecord { + return createIntent(input); +} diff --git a/src/cint/machine-state.js b/src/cint/machine-state.js deleted file mode 100644 index d164f4a..0000000 --- a/src/cint/machine-state.js +++ /dev/null @@ -1,24 +0,0 @@ -import { - assertCint, - assertExactKeys, - assertJsonValue, - canonicalDigest, - identifier, - integer, - isoInstant, - sealRecord -} from "./canonical.js"; - -export function createMachineStateSnapshot(input) { - assertExactKeys(input, ["id", "epoch", "available", "state", "observed_at"], [], "machine state"); - assertCint(typeof input.available === "boolean", "CINT_MACHINE_STATE_INVALID", "machine state availability must be boolean"); - const state = assertJsonValue(input.state, "machine state value"); - return sealRecord({ - protocol: "cint/machine-state/1", - id: identifier(input.id, "machine state id"), - epoch: integer(input.epoch, "machine state epoch", { minimum: 1 }), - available: input.available, - observed_at: isoInstant(input.observed_at, "machine state observed_at"), - state_digest: canonicalDigest(state) - }); -} diff --git a/src/cint/machine-state.ts b/src/cint/machine-state.ts new file mode 100644 index 0000000..328e119 --- /dev/null +++ b/src/cint/machine-state.ts @@ -0,0 +1,26 @@ +import { + assertCint, + assertExactKeys, + assertJsonValue, + canonicalDigest, + identifier, + integer, + isoInstant, + sealRecord +} from "./canonical.js"; +import type { Epoch, MachineStateDigest, MachineStateId } from "./types/brands.js"; +import type { MachineStateSnapshot } from "./types/records.js"; + +export function createMachineStateSnapshot(value: unknown): MachineStateSnapshot { + const input = assertExactKeys(value, ["id", "epoch", "available", "state", "observed_at"], [], "machine state"); + assertCint(typeof input["available"] === "boolean", "CINT_MACHINE_STATE_INVALID", "machine state availability must be boolean"); + const state = assertJsonValue(input["state"], "machine state value"); + return sealRecord({ + protocol: "cint/machine-state/1" as const, + id: identifier(input["id"], "machine state id"), + epoch: integer(input["epoch"], "machine state epoch", { minimum: 1 }), + available: input["available"], + observed_at: isoInstant(input["observed_at"], "machine state observed_at"), + state_digest: canonicalDigest(state) + }); +} diff --git a/src/cint/outcome.js b/src/cint/outcome.js deleted file mode 100644 index b8c2066..0000000 --- a/src/cint/outcome.js +++ /dev/null @@ -1,38 +0,0 @@ -import { - assertCint, - assertExactKeys, - isoInstant, - sealRecord, - verifyProtocolRecord, - verifySealedRecord -} from "./canonical.js"; - -export function createOutcome(input) { - assertExactKeys( - input, - ["receipt", "execution", "verification", "rollback", "completed_at"], - [], - "outcome" - ); - verifyProtocolRecord(input.receipt, "cint/decision-receipt/1", "receipt"); - verifySealedRecord(input.execution, "execution"); - verifySealedRecord(input.verification, "outcome verification"); - if (input.rollback !== null) verifySealedRecord(input.rollback, "rollback"); - const applied = input.verification.status === "VERIFIED" && input.rollback === null; - const restored = ["VERIFIED", "DIVERGED"].includes(input.verification.status) && input.rollback?.status === "RESTORED"; - assertCint(applied || restored, "CINT_OUTCOME_UNVERIFIED", "Outcome must be verified or demonstrably restored"); - return sealRecord({ - protocol: "cint/outcome/1", - status: applied ? "VERIFIED" : "ROLLED_BACK", - effect_status: applied ? "APPLIED" : "RESTORED", - receipt_id: input.receipt.id, - receipt_digest: input.receipt.digest, - action_digest: input.receipt.binding.action_digest, - target_digest: input.receipt.binding.target_digest, - execution_digest: input.execution.digest, - verification_digest: input.verification.digest, - rollback_digest: input.rollback?.digest ?? null, - final_state_digest: applied ? input.verification.actual_sha256 : input.rollback.actual_sha256, - completed_at: isoInstant(input.completed_at, "outcome completed_at") - }); -} diff --git a/src/cint/outcome.ts b/src/cint/outcome.ts new file mode 100644 index 0000000..9f8cca6 --- /dev/null +++ b/src/cint/outcome.ts @@ -0,0 +1,147 @@ +import { + assertCint, + assertExactKeys, + boundedString, + isoInstant, + sealRecord, + sha256Digest, + verifyProtocolRecord, + verifySealedRecord +} from "./canonical.js"; +import type { + ExecutionDigest, + RecordDigest, + TargetDigest, + VerificationDigest +} from "./types/brands.js"; +import type { + IssuedDecisionReceipt, + OutcomeVerification, + RestoredOutcome, + RestoredRollback, + VerifiedOutcome +} from "./types/records.js"; + +export interface AppliedOutcomeInput { + readonly receipt: IssuedDecisionReceipt; + readonly execution: unknown; + readonly verification: OutcomeVerification & { readonly status: "VERIFIED" }; + readonly rollback: null; + readonly completed_at: unknown; +} + +export interface RestoredOutcomeInput { + readonly receipt: IssuedDecisionReceipt; + readonly execution: unknown; + readonly verification: OutcomeVerification; + readonly rollback: RestoredRollback; + readonly completed_at: unknown; +} + +interface VerifiedExecutionEvidence { + readonly digest: ExecutionDigest; +} + +export function verifyOutcomeVerification(value: unknown): OutcomeVerification { + const record = verifySealedRecord(value, "outcome verification"); + assertCint( + record["protocol"] === "cint/outcome-verification/1", + "CINT_PROTOCOL_INVALID", + "Outcome verification uses an unsupported protocol" + ); + const status = record["status"]; + assertCint( + status === "VERIFIED" || status === "DIVERGED", + "CINT_OUTCOME_UNVERIFIED", + "Outcome verification status is invalid" + ); + return { + protocol: "cint/outcome-verification/1", + status, + target: boundedString(record["target"], "outcome verification target"), + expected_sha256: sha256Digest(record["expected_sha256"], "outcome verification expected_sha256"), + actual_sha256: sha256Digest(record["actual_sha256"], "outcome verification actual_sha256"), + checked_at: isoInstant(record["checked_at"], "outcome verification checked_at"), + digest: sha256Digest(record["digest"], "outcome verification digest") + }; +} + +export function verifyRestoredRollback(value: unknown): RestoredRollback { + const record = verifySealedRecord(value, "rollback"); + assertCint( + record["protocol"] === "cint/rollback/1" && record["status"] === "RESTORED", + "CINT_ROLLBACK_FAILED", + "Rollback did not produce restored evidence" + ); + return { + protocol: "cint/rollback/1", + status: "RESTORED", + target: boundedString(record["target"], "rollback target"), + expected_sha256: sha256Digest(record["expected_sha256"], "rollback expected_sha256"), + actual_sha256: sha256Digest(record["actual_sha256"], "rollback actual_sha256"), + rolled_back_at: isoInstant(record["rolled_back_at"], "rollback rolled_back_at"), + digest: sha256Digest(record["digest"], "rollback digest") + }; +} + +function verifiedExecution(value: unknown): VerifiedExecutionEvidence { + const record = verifySealedRecord(value, "execution"); + const protocol = record["protocol"]; + assertCint( + protocol === "cint/synthetic-execution/1" || + protocol === "cint/codex-delegation-execution/1" || + protocol === "cint/execution-interruption/1", + "CINT_PROTOCOL_INVALID", + "Adapter execution uses an unsupported protocol" + ); + return { digest: sha256Digest(record["digest"], "execution digest") }; +} + +export function createOutcome(input: AppliedOutcomeInput): VerifiedOutcome; +export function createOutcome(input: RestoredOutcomeInput): RestoredOutcome; +export function createOutcome(value: unknown): VerifiedOutcome | RestoredOutcome { + const input = assertExactKeys( + value, + ["receipt", "execution", "verification", "rollback", "completed_at"], + [], + "outcome" + ); + const receipt = verifyProtocolRecord(input["receipt"], "cint/decision-receipt/1", "receipt"); + const execution = verifiedExecution(input["execution"]); + const verification = verifyOutcomeVerification(input["verification"]); + const rollback = input["rollback"] === null ? null : verifyRestoredRollback(input["rollback"]); + const completedAt = isoInstant(input["completed_at"], "outcome completed_at"); + + if (verification.status === "VERIFIED" && rollback === null) { + return sealRecord({ + protocol: "cint/outcome/1" as const, + status: "VERIFIED" as const, + effect_status: "APPLIED" as const, + receipt_id: receipt.id, + receipt_digest: receipt.digest, + action_digest: receipt.binding.action_digest, + target_digest: receipt.binding.target_digest, + execution_digest: execution.digest, + verification_digest: verification.digest, + rollback_digest: null, + final_state_digest: verification.actual_sha256, + completed_at: completedAt + }); + } + + assertCint(rollback !== null, "CINT_OUTCOME_UNVERIFIED", "Outcome must be verified or demonstrably restored"); + return sealRecord({ + protocol: "cint/outcome/1" as const, + status: "ROLLED_BACK" as const, + effect_status: "RESTORED" as const, + receipt_id: receipt.id, + receipt_digest: receipt.digest, + action_digest: receipt.binding.action_digest, + target_digest: receipt.binding.target_digest, + execution_digest: execution.digest, + verification_digest: verification.digest, + rollback_digest: rollback.digest, + final_state_digest: rollback.actual_sha256, + completed_at: completedAt + }); +} diff --git a/src/cint/policy.js b/src/cint/policy.js deleted file mode 100644 index 4ac7f91..0000000 --- a/src/cint/policy.js +++ /dev/null @@ -1,97 +0,0 @@ -import { - assertCint, - assertExactKeys, - identifier, - integer, - isoInstant, - sealRecord, - stringArray, - verifyProtocolRecord -} from "./canonical.js"; - -export function createPolicySnapshot(input) { - assertExactKeys( - input, - [ - "id", - "version", - "epoch", - "allowed_adapters", - "allowed_action_types", - "denied_action_types", - "require_explicit_request", - "require_declared_effects", - "require_rollback_for_consequential", - "review_on_uncertainty", - "issued_at" - ], - [], - "policy" - ); - for (const field of [ - "require_explicit_request", - "require_declared_effects", - "require_rollback_for_consequential", - "review_on_uncertainty" - ]) { - assertCint(typeof input[field] === "boolean", "CINT_POLICY_INVALID", `policy.${field} must be boolean`); - } - const allowedAdapters = stringArray(input.allowed_adapters, "policy.allowed_adapters", { - minimum: 1, - maximum: 32, - bytes: 128 - }).map((value) => identifier(value, "policy.allowed_adapters item")); - const allowedActionTypes = stringArray(input.allowed_action_types, "policy.allowed_action_types", { - minimum: 1, - maximum: 64, - bytes: 128 - }).map((value) => identifier(value, "policy.allowed_action_types item")); - const deniedActionTypes = stringArray(input.denied_action_types, "policy.denied_action_types", { - minimum: 0, - maximum: 64, - bytes: 128 - }).map((value) => identifier(value, "policy.denied_action_types item")); - assertCint( - deniedActionTypes.every((value) => !allowedActionTypes.includes(value)), - "CINT_POLICY_CONTRADICTION", - "An action type cannot be both allowed and denied" - ); - return sealRecord({ - protocol: "cint/policy/1", - id: identifier(input.id, "policy.id"), - version: identifier(input.version, "policy.version"), - status: "ACTIVE", - epoch: integer(input.epoch, "policy.epoch", { minimum: 1 }), - allowed_adapters: allowedAdapters, - allowed_action_types: allowedActionTypes, - denied_action_types: deniedActionTypes, - require_explicit_request: input.require_explicit_request, - require_declared_effects: input.require_declared_effects, - require_rollback_for_consequential: input.require_rollback_for_consequential, - review_on_uncertainty: input.review_on_uncertainty, - issued_at: isoInstant(input.issued_at, "policy.issued_at") - }); -} - -export function evaluatePolicy({ policy, intent, adapter_capability }) { - verifyProtocolRecord(policy, "cint/policy/1", "policy"); - verifyProtocolRecord(intent, "cint/intent/1", "intent"); - verifyProtocolRecord(adapter_capability, "cint/adapter-capability/1", "adapter capability"); - const reasons = []; - if (policy.status !== "ACTIVE") reasons.push("CINT_POLICY_INACTIVE"); - if (!policy.allowed_adapters.includes(intent.action.adapter)) reasons.push("CINT_POLICY_ADAPTER_DENIED"); - if (!policy.allowed_action_types.includes(intent.action.type)) reasons.push("CINT_POLICY_ACTION_DENIED"); - if (policy.denied_action_types.includes(intent.action.type)) reasons.push("CINT_POLICY_ACTION_DENIED"); - if (!adapter_capability.action_types.includes(intent.action.type)) reasons.push("CINT_ADAPTER_ACTION_UNSUPPORTED"); - if (!adapter_capability.consequence_classes.includes(intent.action.consequence)) { - reasons.push("CINT_ADAPTER_CONSEQUENCE_UNSUPPORTED"); - } - if ( - intent.action.consequence === "CONSEQUENTIAL" && - policy.require_rollback_for_consequential && - !adapter_capability.rollback - ) { - reasons.push("CINT_ROLLBACK_REQUIRED"); - } - return { allowed: reasons.length === 0, reasons }; -} diff --git a/src/cint/policy.ts b/src/cint/policy.ts new file mode 100644 index 0000000..499a172 --- /dev/null +++ b/src/cint/policy.ts @@ -0,0 +1,125 @@ +import { + assertCint, + assertExactKeys, + identifier, + integer, + isoInstant, + sealRecord, + stringArray, + verifyProtocolRecord +} from "./canonical.js"; +import type { AdapterId, Epoch, Identifier, PolicyId } from "./types/brands.js"; +import type { + AdapterCapability, + IntentRecord, + PolicySnapshot +} from "./types/records.js"; + +const BOOLEAN_FIELDS = [ + "require_explicit_request", + "require_declared_effects", + "require_rollback_for_consequential", + "review_on_uncertainty" +] as const; + +export interface PolicyEvaluationInput { + readonly policy: PolicySnapshot; + readonly intent: IntentRecord; + readonly adapter_capability: AdapterCapability; +} + +export interface PolicyEvaluationResult { + readonly allowed: boolean; + readonly reasons: readonly string[]; +} + +function checkedBoolean(value: unknown, field: string): boolean { + assertCint(typeof value === "boolean", "CINT_POLICY_INVALID", `policy.${field} must be boolean`); + return value; +} + +export function createPolicySnapshot(value: unknown): PolicySnapshot { + const input = assertExactKeys( + value, + [ + "id", + "version", + "epoch", + "allowed_adapters", + "allowed_action_types", + "denied_action_types", + "require_explicit_request", + "require_declared_effects", + "require_rollback_for_consequential", + "review_on_uncertainty", + "issued_at" + ], + [], + "policy" + ); + for (const field of BOOLEAN_FIELDS) { + assertCint(typeof input[field] === "boolean", "CINT_POLICY_INVALID", `policy.${field} must be boolean`); + } + const allowedAdapters = stringArray(input["allowed_adapters"], "policy.allowed_adapters", { + minimum: 1, + maximum: 32, + bytes: 128 + }).map((item) => identifier(item, "policy.allowed_adapters item")); + const allowedActionTypes = stringArray(input["allowed_action_types"], "policy.allowed_action_types", { + minimum: 1, + maximum: 64, + bytes: 128 + }).map((item) => identifier(item, "policy.allowed_action_types item")); + const deniedActionTypes = stringArray(input["denied_action_types"], "policy.denied_action_types", { + minimum: 0, + maximum: 64, + bytes: 128 + }).map((item) => identifier(item, "policy.denied_action_types item")); + assertCint( + deniedActionTypes.every((item) => !allowedActionTypes.includes(item)), + "CINT_POLICY_CONTRADICTION", + "An action type cannot be both allowed and denied" + ); + return sealRecord({ + protocol: "cint/policy/1" as const, + id: identifier(input["id"], "policy.id"), + version: identifier(input["version"], "policy.version"), + status: "ACTIVE" as const, + epoch: integer(input["epoch"], "policy.epoch", { minimum: 1 }), + allowed_adapters: allowedAdapters, + allowed_action_types: allowedActionTypes, + denied_action_types: deniedActionTypes, + require_explicit_request: checkedBoolean(input["require_explicit_request"], "require_explicit_request"), + require_declared_effects: checkedBoolean(input["require_declared_effects"], "require_declared_effects"), + require_rollback_for_consequential: checkedBoolean(input["require_rollback_for_consequential"], "require_rollback_for_consequential"), + review_on_uncertainty: checkedBoolean(input["review_on_uncertainty"], "review_on_uncertainty"), + issued_at: isoInstant(input["issued_at"], "policy.issued_at") + }); +} + +export function evaluatePolicy(input: PolicyEvaluationInput): PolicyEvaluationResult { + const policy = verifyProtocolRecord(input.policy, "cint/policy/1", "policy"); + const intent = verifyProtocolRecord(input.intent, "cint/intent/1", "intent"); + const adapterCapability = verifyProtocolRecord( + input.adapter_capability, + "cint/adapter-capability/1", + "adapter capability" + ); + const reasons: string[] = []; + if (policy.status !== "ACTIVE") reasons.push("CINT_POLICY_INACTIVE"); + if (!policy.allowed_adapters.includes(intent.action.adapter)) reasons.push("CINT_POLICY_ADAPTER_DENIED"); + if (!policy.allowed_action_types.includes(intent.action.type)) reasons.push("CINT_POLICY_ACTION_DENIED"); + if (policy.denied_action_types.includes(intent.action.type)) reasons.push("CINT_POLICY_ACTION_DENIED"); + if (!adapterCapability.action_types.includes(intent.action.type)) reasons.push("CINT_ADAPTER_ACTION_UNSUPPORTED"); + if (!adapterCapability.consequence_classes.includes(intent.action.consequence)) { + reasons.push("CINT_ADAPTER_CONSEQUENCE_UNSUPPORTED"); + } + if ( + intent.action.consequence === "CONSEQUENTIAL" && + policy.require_rollback_for_consequential && + !adapterCapability.rollback + ) { + reasons.push("CINT_ROLLBACK_REQUIRED"); + } + return { allowed: reasons.length === 0, reasons }; +} diff --git a/src/cint/principal.js b/src/cint/principal.js deleted file mode 100644 index 2ab9275..0000000 --- a/src/cint/principal.js +++ /dev/null @@ -1,37 +0,0 @@ -import { - assertCint, - assertExactKeys, - assertJsonValue, - identifier, - isoInstant, - sealRecord, - stringArray -} from "./canonical.js"; - -const PRINCIPAL_TYPES = new Set(["HUMAN", "SERVICE", "AGENT"]); - -export function resolvePrincipal(input) { - assertExactKeys( - input, - ["id", "type", "authenticated", "authority_chain", "attributes", "resolved_at"], - [], - "principal" - ); - const type = String(input.type); - assertCint(PRINCIPAL_TYPES.has(type), "CINT_PRINCIPAL_TYPE", `Unsupported principal type: ${type}`); - assertCint(typeof input.authenticated === "boolean", "CINT_PRINCIPAL_AUTH", "principal.authenticated must be boolean"); - const authorityChain = stringArray(input.authority_chain, "principal.authority_chain", { - minimum: 0, - maximum: 16, - bytes: 128 - }).map((value) => identifier(value, "principal.authority_chain item")); - return sealRecord({ - protocol: "cint/principal/1", - id: identifier(input.id, "principal.id"), - type, - authenticated: input.authenticated, - authority_chain: authorityChain, - attributes: assertJsonValue(input.attributes, "principal.attributes"), - resolved_at: isoInstant(input.resolved_at, "principal.resolved_at") - }); -} diff --git a/src/cint/principal.ts b/src/cint/principal.ts new file mode 100644 index 0000000..b4a0eb1 --- /dev/null +++ b/src/cint/principal.ts @@ -0,0 +1,51 @@ +import { + assertCint, + assertExactKeys, + assertJsonValue, + identifier, + isPlainRecord, + isoInstant, + sealRecord, + stringArray +} from "./canonical.js"; +import type { AuthorityId, PrincipalId } from "./types/brands.js"; +import type { JsonObject, JsonValue } from "./types/protocols.js"; +import type { PrincipalRecord, PrincipalType } from "./types/records.js"; + +const PRINCIPAL_TYPES: ReadonlySet = new Set(["HUMAN", "SERVICE", "AGENT"]); + +function isPrincipalType(value: string): value is PrincipalType { + return PRINCIPAL_TYPES.has(value); +} + +function isJsonObject(value: JsonValue): value is JsonObject { + return isPlainRecord(value); +} + +export function resolvePrincipal(value: unknown): PrincipalRecord { + const input = assertExactKeys( + value, + ["id", "type", "authenticated", "authority_chain", "attributes", "resolved_at"], + [], + "principal" + ); + const type = String(input["type"]); + assertCint(isPrincipalType(type), "CINT_PRINCIPAL_TYPE", `Unsupported principal type: ${type}`); + assertCint(typeof input["authenticated"] === "boolean", "CINT_PRINCIPAL_AUTH", "principal.authenticated must be boolean"); + const authorityChain = stringArray(input["authority_chain"], "principal.authority_chain", { + minimum: 0, + maximum: 16, + bytes: 128 + }).map((item) => identifier(item, "principal.authority_chain item")); + const attributes = assertJsonValue(input["attributes"], "principal.attributes"); + assertCint(isJsonObject(attributes), "CINT_SCHEMA_INVALID", "principal.attributes must be an object"); + return sealRecord({ + protocol: "cint/principal/1" as const, + id: identifier(input["id"], "principal.id"), + type, + authenticated: input["authenticated"], + authority_chain: authorityChain, + attributes, + resolved_at: isoInstant(input["resolved_at"], "principal.resolved_at") + }); +} diff --git a/src/cint/receipt.js b/src/cint/receipt.ts similarity index 50% rename from src/cint/receipt.js rename to src/cint/receipt.ts index a0ae7be..cd60799 100644 --- a/src/cint/receipt.js +++ b/src/cint/receipt.ts @@ -1,70 +1,102 @@ import { createHmac, randomUUID, timingSafeEqual } from "node:crypto"; -import { canonicalJson } from "../util.js"; import { assertCint, assertExactKeys, + assertJsonValue, boundedString, + canonicalJson, canonicalDigest, identifier, + isPlainRecord, isoInstant, sealRecord, sha256Digest, verifyProtocolRecord } from "./canonical.js"; +import type { + HmacSha256Signature, + Identifier, + ReceiptId +} from "./types/brands.js"; +import type { + AdmitDecision, + IssuedDecisionReceipt +} from "./types/records.js"; +import type { JsonObject } from "./types/protocols.js"; -function unsignedReceipt(receipt) { - return Object.fromEntries( +function unsignedReceipt(receipt: IssuedDecisionReceipt): JsonObject { + const unsigned = assertJsonValue(Object.fromEntries( Object.entries(receipt).filter(([key]) => key !== "digest" && key !== "signature") - ); + ), "unsigned receipt"); + assertCint(isPlainRecord(unsigned), "CINT_OBJECT_INVALID", "Unsigned receipt must be an object"); + return unsigned; +} + +export interface ReceiptVerificationOptions { + readonly now?: unknown; +} + +export interface ReceiptIssuanceInput { + readonly decision: AdmitDecision; + readonly issued_at: unknown; + readonly id?: unknown; + readonly nonce?: unknown; } export class DecisionReceiptAuthority { - #key; + readonly issuer_id: Identifier; + #key: Buffer; - constructor(input) { - assertExactKeys(input, ["issuer_id", "key"], [], "receipt authority"); - this.issuer_id = identifier(input.issuer_id, "receipt authority issuer_id"); - const key = Buffer.isBuffer(input.key) ? Buffer.from(input.key) : Buffer.from(String(input.key), "utf8"); + constructor(value: unknown) { + const input = assertExactKeys(value, ["issuer_id", "key"], [], "receipt authority"); + this.issuer_id = identifier(input["issuer_id"], "receipt authority issuer_id"); + const rawKey = input["key"]; + const key = Buffer.isBuffer(rawKey) ? Buffer.from(rawKey) : Buffer.from(String(rawKey), "utf8"); assertCint(key.byteLength >= 32, "CINT_RECEIPT_KEY_INVALID", "Receipt authority key must contain at least 32 bytes"); this.#key = key; Object.freeze(this); } - #sign(value) { - return createHmac("sha256", this.#key).update(canonicalJson(value)).digest("hex"); + #sign(value: unknown): HmacSha256Signature { + return sha256Digest( + createHmac("sha256", this.#key).update(canonicalJson(assertJsonValue(value))).digest("hex"), + "receipt signature" + ); } - issue(input) { - assertExactKeys(input, ["decision", "issued_at"], ["id", "nonce"], "receipt issuance"); - verifyProtocolRecord(input.decision, "cint/decision/1", "decision"); + issue(input: ReceiptIssuanceInput): IssuedDecisionReceipt; + issue(value: unknown): IssuedDecisionReceipt { + const input = assertExactKeys(value, ["decision", "issued_at"], ["id", "nonce"], "receipt issuance"); + const decision = verifyProtocolRecord(input["decision"], "cint/decision/1", "decision"); assertCint( - input.decision.protocol === "cint/decision/1" && input.decision.status === "ADMIT" && input.decision.receipt_eligible, + decision.status === "ADMIT" && decision.receipt_eligible, "CINT_RECEIPT_DECISION_INELIGIBLE", "Only a receipt-eligible ADMIT decision can produce a receipt" ); - const issuedAt = isoInstant(input.issued_at, "receipt issued_at"); - assertCint(Date.parse(issuedAt) >= Date.parse(input.decision.issued_at), "CINT_RECEIPT_TIME", "Receipt issuance precedes its decision"); - assertCint(Date.parse(issuedAt) < Date.parse(input.decision.expires_at), "CINT_RECEIPT_EXPIRED", "Decision expired before receipt issuance"); + const admitDecision: AdmitDecision = decision; + const issuedAt = isoInstant(input["issued_at"], "receipt issued_at"); + assertCint(Date.parse(issuedAt) >= Date.parse(admitDecision.issued_at), "CINT_RECEIPT_TIME", "Receipt issuance precedes its decision"); + assertCint(Date.parse(issuedAt) < Date.parse(admitDecision.expires_at), "CINT_RECEIPT_EXPIRED", "Decision expired before receipt issuance"); const unsigned = { - protocol: "cint/decision-receipt/1", - id: identifier(input.id ?? `receipt.${randomUUID()}`, "receipt.id"), + protocol: "cint/decision-receipt/1" as const, + id: identifier(input["id"] ?? `receipt.${randomUUID()}`, "receipt.id"), issuer_id: this.issuer_id, - decision_id: input.decision.id, - decision_digest: input.decision.digest, - status: "ISSUED", + decision_id: admitDecision.id, + decision_digest: admitDecision.digest, + status: "ISSUED" as const, issued_at: issuedAt, - expires_at: input.decision.expires_at, - nonce: boundedString(input.nonce ?? randomUUID(), "receipt.nonce", { minimum: 16, maximum: 128 }), - binding: input.decision.binding, - binding_digest: input.decision.binding_digest, - signature_algorithm: "HMAC-SHA256" + expires_at: admitDecision.expires_at, + nonce: boundedString(input["nonce"] ?? randomUUID(), "receipt.nonce", { minimum: 16, maximum: 128 }), + binding: admitDecision.binding, + binding_digest: admitDecision.binding_digest, + signature_algorithm: "HMAC-SHA256" as const }; return sealRecord({ ...unsigned, signature: this.#sign(unsigned) }); } - verify(receipt, options = {}) { - verifyProtocolRecord(receipt, "cint/decision-receipt/1", "receipt"); + verify(value: unknown, options: ReceiptVerificationOptions = {}): IssuedDecisionReceipt { + const receipt = verifyProtocolRecord(value, "cint/decision-receipt/1", "receipt"); assertExactKeys( receipt, [ diff --git a/src/cint/revalidation.js b/src/cint/revalidation.js deleted file mode 100644 index e4c8a4b..0000000 --- a/src/cint/revalidation.js +++ /dev/null @@ -1,136 +0,0 @@ -import { isoInstant, sealRecord, verifyProtocolRecord } from "./canonical.js"; -import { runCounterIntentChallenge } from "./challenge.js"; - -function unique(values) { - return [...new Set(values)]; -} - -export function revalidateReceipt(input) { - const checkedAt = isoInstant(input.now, "revalidation time"); - const reasonCodes = []; - let receiptAuthenticated = false; - try { - input.receipt_authority.verify(input.receipt); - receiptAuthenticated = true; - } catch (error) { - reasonCodes.push(error.code ?? "CINT_RECEIPT_INVALID"); - } - if (receiptAuthenticated) { - try { - input.receipt_authority.verify(input.receipt, { now: checkedAt }); - } catch (error) { - reasonCodes.push(error.code ?? "CINT_RECEIPT_INVALID"); - } - } - - for (const [label, record, protocol] of [ - ["intent", input.intent, "cint/intent/1"], - ["principal", input.principal, "cint/principal/1"], - ["authority", input.authority, "cint/authority/1"], - ["policy", input.policy, "cint/policy/1"], - ["adapter_capability", input.adapter_capability, "cint/adapter-capability/1"], - ["machine_state", input.machine_state, "cint/machine-state/1"] - ]) { - try { - verifyProtocolRecord(record, protocol, label); - } catch (error) { - reasonCodes.push(error.code ?? "CINT_RECORD_INVALID"); - } - } - - const binding = input.receipt?.binding ?? {}; - if (input.intent?.action_digest !== binding.action_digest) reasonCodes.push("CINT_ACTION_DIGEST_CHANGED"); - if (input.intent?.target_digest !== binding.target_digest) reasonCodes.push("CINT_TARGET_CHANGED"); - if (input.intent?.context_digest !== binding.context_digest) reasonCodes.push("CINT_CONTEXT_CHANGED"); - if (input.intent?.digest !== binding.intent_digest) reasonCodes.push("CINT_INTENT_CHANGED"); - if (input.principal?.digest !== binding.principal_digest) reasonCodes.push("CINT_PRINCIPAL_CHANGED"); - if ( - input.authority?.id !== binding.authority_id || - input.authority?.digest !== binding.authority_digest || - input.authority?.epoch !== binding.authority_epoch || - input.authority?.status !== "ACTIVE" - ) { - reasonCodes.push("CINT_AUTHORITY_CHANGED"); - } - if ( - input.policy?.id !== binding.policy_id || - input.policy?.digest !== binding.policy_digest || - input.policy?.epoch !== binding.policy_epoch || - input.policy?.status !== "ACTIVE" - ) { - reasonCodes.push("CINT_POLICY_CHANGED"); - } - if ( - input.adapter_capability?.id !== binding.adapter_id || - input.adapter_capability?.digest !== binding.adapter_capability_digest - ) { - reasonCodes.push("CINT_ADAPTER_CHANGED"); - } - if ( - input.machine_state?.id !== binding.machine_state_id || - input.machine_state?.epoch !== binding.machine_state_epoch || - input.machine_state?.state_digest !== binding.machine_state_digest - ) { - reasonCodes.push("CINT_MACHINE_STATE_CHANGED"); - } - if (input.machine_state?.available !== true) reasonCodes.push("CINT_UNAVAILABLE"); - - if (receiptAuthenticated && reasonCodes.every((code) => code === "CINT_RECEIPT_EXPIRED")) { - try { - const challenge = runCounterIntentChallenge({ - intent: input.intent, - principal: input.principal, - authority: input.authority, - policy: input.policy, - adapter_capability: input.adapter_capability, - machine_state: input.machine_state, - now: checkedAt - }); - if (challenge.status !== "CLEAR") { - reasonCodes.push(...challenge.reasons.map((reason) => reason.code)); - } - } catch (error) { - reasonCodes.push(error.code ?? "CINT_REVALIDATION_FAILED"); - } - } - - const reasons = unique(reasonCodes); - const rejected = reasons.some((code) => - [ - "CINT_RECEIPT_SIGNATURE_INVALID", - "CINT_RECEIPT_ISSUER_MISMATCH", - "CINT_RECEIPT_BINDING_INVALID", - "CINT_ACTION_DIGEST_CHANGED", - "CINT_TARGET_CHANGED", - "CINT_CONTEXT_CHANGED", - "CINT_INTENT_CHANGED", - "CINT_PROTOCOL_INVALID", - "CINT_SCHEMA_INVALID", - "CINT_RECORD_TAMPERED" - ].includes(code) - ); - const unavailable = reasons.includes("CINT_UNAVAILABLE"); - const status = reasons.length === 0 ? "VALID" : unavailable ? "FAIL_CLOSED" : rejected ? "REJECTED" : "REVOKED"; - return sealRecord({ - protocol: "cint/revalidation/1", - receipt_id: input.receipt?.id ?? "receipt.invalid", - status, - checked_at: checkedAt, - binding_digest: input.receipt?.binding_digest ?? "0".repeat(64), - current: { - intent_digest: input.intent?.digest ?? null, - action_digest: input.intent?.action_digest ?? null, - target_digest: input.intent?.target_digest ?? null, - context_digest: input.intent?.context_digest ?? null, - principal_digest: input.principal?.digest ?? null, - authority_digest: input.authority?.digest ?? null, - authority_epoch: input.authority?.epoch ?? null, - policy_digest: input.policy?.digest ?? null, - policy_epoch: input.policy?.epoch ?? null, - adapter_capability_digest: input.adapter_capability?.digest ?? null, - machine_state_digest: input.machine_state?.state_digest ?? null, - machine_state_epoch: input.machine_state?.epoch ?? null - }, - reason_codes: reasons - }); -} diff --git a/src/cint/revalidation.ts b/src/cint/revalidation.ts new file mode 100644 index 0000000..3767053 --- /dev/null +++ b/src/cint/revalidation.ts @@ -0,0 +1,194 @@ +import { + isPlainRecord, + isoInstant, + sealRecord, + verifyProtocolRecord +} from "./canonical.js"; +import { runCounterIntentChallenge } from "./challenge.js"; +import type { CanonicalInstant } from "./types/brands.js"; +import type { + IssuedDecisionReceipt, + Revalidation +} from "./types/records.js"; + +export interface ReceiptVerifier { + verify(receipt: unknown, options?: { readonly now?: unknown }): IssuedDecisionReceipt; +} + +export interface RevalidationInput { + readonly receipt: unknown; + readonly receipt_authority: ReceiptVerifier; + readonly intent: unknown; + readonly principal: unknown; + readonly authority: unknown; + readonly policy: unknown; + readonly adapter_capability: unknown; + readonly machine_state: unknown; + readonly now: unknown; +} + +function unique(values: readonly string[]): string[] { + return [...new Set(values)]; +} + +function containsOnlyReceiptExpiry(values: readonly string[]): boolean { + return values.every((code) => code === "CINT_RECEIPT_EXPIRED"); +} + +function field(record: unknown, name: string): unknown { + return isPlainRecord(record) ? record[name] : undefined; +} + +function errorCode(error: unknown, fallback: string): string { + const code = error !== null && (typeof error === "object" || typeof error === "function") + ? Reflect.get(error, "code") + : undefined; + return typeof code === "string" ? code : fallback; +} + +function collectProtocolFailure( + record: unknown, + protocol: + | "cint/intent/1" + | "cint/principal/1" + | "cint/authority/1" + | "cint/policy/1" + | "cint/adapter-capability/1" + | "cint/machine-state/1", + label: string, + reasonCodes: string[] +): void { + try { + verifyProtocolRecord(record, protocol, label); + } catch (error) { + reasonCodes.push(errorCode(error, "CINT_RECORD_INVALID")); + } +} + +function challengeAt(input: RevalidationInput, checkedAt: CanonicalInstant): ReturnType { + return runCounterIntentChallenge({ + intent: verifyProtocolRecord(input.intent, "cint/intent/1", "intent"), + principal: verifyProtocolRecord(input.principal, "cint/principal/1", "principal"), + authority: verifyProtocolRecord(input.authority, "cint/authority/1", "authority"), + policy: verifyProtocolRecord(input.policy, "cint/policy/1", "policy"), + adapter_capability: verifyProtocolRecord(input.adapter_capability, "cint/adapter-capability/1", "adapter_capability"), + machine_state: verifyProtocolRecord(input.machine_state, "cint/machine-state/1", "machine_state"), + now: checkedAt + }); +} + +export function revalidateReceipt(input: RevalidationInput): Revalidation { + const checkedAt = isoInstant(input.now, "revalidation time"); + const reasonCodes: string[] = []; + let receiptAuthenticated = false; + try { + input.receipt_authority.verify(input.receipt); + receiptAuthenticated = true; + } catch (error) { + reasonCodes.push(errorCode(error, "CINT_RECEIPT_INVALID")); + } + if (receiptAuthenticated) { + try { + input.receipt_authority.verify(input.receipt, { now: checkedAt }); + } catch (error) { + reasonCodes.push(errorCode(error, "CINT_RECEIPT_INVALID")); + } + } + + collectProtocolFailure(input.intent, "cint/intent/1", "intent", reasonCodes); + collectProtocolFailure(input.principal, "cint/principal/1", "principal", reasonCodes); + collectProtocolFailure(input.authority, "cint/authority/1", "authority", reasonCodes); + collectProtocolFailure(input.policy, "cint/policy/1", "policy", reasonCodes); + collectProtocolFailure(input.adapter_capability, "cint/adapter-capability/1", "adapter_capability", reasonCodes); + collectProtocolFailure(input.machine_state, "cint/machine-state/1", "machine_state", reasonCodes); + + const rawBinding = field(input.receipt, "binding"); + const binding = isPlainRecord(rawBinding) ? rawBinding : {}; + if (field(input.intent, "action_digest") !== binding["action_digest"]) reasonCodes.push("CINT_ACTION_DIGEST_CHANGED"); + if (field(input.intent, "target_digest") !== binding["target_digest"]) reasonCodes.push("CINT_TARGET_CHANGED"); + if (field(input.intent, "context_digest") !== binding["context_digest"]) reasonCodes.push("CINT_CONTEXT_CHANGED"); + if (field(input.intent, "digest") !== binding["intent_digest"]) reasonCodes.push("CINT_INTENT_CHANGED"); + if (field(input.principal, "digest") !== binding["principal_digest"]) reasonCodes.push("CINT_PRINCIPAL_CHANGED"); + if ( + field(input.authority, "id") !== binding["authority_id"] || + field(input.authority, "digest") !== binding["authority_digest"] || + field(input.authority, "epoch") !== binding["authority_epoch"] || + field(input.authority, "status") !== "ACTIVE" + ) { + reasonCodes.push("CINT_AUTHORITY_CHANGED"); + } + if ( + field(input.policy, "id") !== binding["policy_id"] || + field(input.policy, "digest") !== binding["policy_digest"] || + field(input.policy, "epoch") !== binding["policy_epoch"] || + field(input.policy, "status") !== "ACTIVE" + ) { + reasonCodes.push("CINT_POLICY_CHANGED"); + } + if ( + field(input.adapter_capability, "id") !== binding["adapter_id"] || + field(input.adapter_capability, "digest") !== binding["adapter_capability_digest"] + ) { + reasonCodes.push("CINT_ADAPTER_CHANGED"); + } + if ( + field(input.machine_state, "id") !== binding["machine_state_id"] || + field(input.machine_state, "epoch") !== binding["machine_state_epoch"] || + field(input.machine_state, "state_digest") !== binding["machine_state_digest"] + ) { + reasonCodes.push("CINT_MACHINE_STATE_CHANGED"); + } + if (field(input.machine_state, "available") !== true) reasonCodes.push("CINT_UNAVAILABLE"); + + if (receiptAuthenticated && containsOnlyReceiptExpiry(reasonCodes)) { + try { + const challenge = challengeAt(input, checkedAt); + if (challenge.status !== "CLEAR") { + reasonCodes.push(...challenge.reasons.map((reason) => reason.code)); + } + } catch (error) { + reasonCodes.push(errorCode(error, "CINT_REVALIDATION_FAILED")); + } + } + + const reasons = unique(reasonCodes); + const rejected = reasons.some((code) => + [ + "CINT_RECEIPT_SIGNATURE_INVALID", + "CINT_RECEIPT_ISSUER_MISMATCH", + "CINT_RECEIPT_BINDING_INVALID", + "CINT_ACTION_DIGEST_CHANGED", + "CINT_TARGET_CHANGED", + "CINT_CONTEXT_CHANGED", + "CINT_INTENT_CHANGED", + "CINT_PROTOCOL_INVALID", + "CINT_SCHEMA_INVALID", + "CINT_RECORD_TAMPERED" + ].includes(code) + ); + const unavailable = reasons.includes("CINT_UNAVAILABLE"); + const status = reasons.length === 0 ? "VALID" : unavailable ? "FAIL_CLOSED" : rejected ? "REJECTED" : "REVOKED"; + const sealed = sealRecord({ + protocol: "cint/revalidation/1" as const, + receipt_id: field(input.receipt, "id") ?? "receipt.invalid", + status, + checked_at: checkedAt, + binding_digest: field(input.receipt, "binding_digest") ?? "0".repeat(64), + current: { + intent_digest: field(input.intent, "digest") ?? null, + action_digest: field(input.intent, "action_digest") ?? null, + target_digest: field(input.intent, "target_digest") ?? null, + context_digest: field(input.intent, "context_digest") ?? null, + principal_digest: field(input.principal, "digest") ?? null, + authority_digest: field(input.authority, "digest") ?? null, + authority_epoch: field(input.authority, "epoch") ?? null, + policy_digest: field(input.policy, "digest") ?? null, + policy_epoch: field(input.policy, "epoch") ?? null, + adapter_capability_digest: field(input.adapter_capability, "digest") ?? null, + machine_state_digest: field(input.machine_state, "state_digest") ?? null, + machine_state_epoch: field(input.machine_state, "epoch") ?? null + }, + reason_codes: reasons + }); + return verifyProtocolRecord(sealed, "cint/revalidation/1", "revalidation"); +} diff --git a/src/cint/rollback.js b/src/cint/rollback.js deleted file mode 100644 index e0ed371..0000000 --- a/src/cint/rollback.js +++ /dev/null @@ -1,16 +0,0 @@ -import { sealRecord, verifySealedRecord } from "./canonical.js"; - -export async function performRollback(adapter, prepared, options) { - try { - const result = await adapter.rollback(prepared, options); - verifySealedRecord(result, "rollback"); - return result; - } catch (error) { - return sealRecord({ - protocol: "cint/rollback-failure/1", - status: "FAILED", - error_code: error.code ?? "CINT_ROLLBACK_FAILED", - rolled_back_at: options.at - }); - } -} diff --git a/src/cint/rollback.ts b/src/cint/rollback.ts new file mode 100644 index 0000000..19da458 --- /dev/null +++ b/src/cint/rollback.ts @@ -0,0 +1,72 @@ +import { + assertCint, + identifier, + isoInstant, + sealRecord, + sha256Digest, + verifySealedRecord +} from "./canonical.js"; +import { verifyRestoredRollback } from "./outcome.js"; +import type { Identifier, RecordDigest } from "./types/brands.js"; +import type { FailedRollback, RollbackResult } from "./types/records.js"; + +export interface RollbackOperationOptions { + readonly at: unknown; + readonly signal?: AbortSignal | undefined; +} + +export interface RollbackAdapter { + rollback?(prepared: unknown, options: RollbackOperationOptions): Promise; +} + +function property(value: unknown, name: string): unknown { + return value !== null && (typeof value === "object" || typeof value === "function") + ? Reflect.get(value, name) + : undefined; +} + +function errorCode(error: unknown): string { + const code = property(error, "code"); + return typeof code === "string" ? code : "CINT_ROLLBACK_FAILED"; +} + +function failedRollback(value: unknown): FailedRollback { + const record = verifySealedRecord(value, "rollback failure"); + const protocol = record["protocol"]; + assertCint( + (protocol === "cint/rollback/1" || protocol === "cint/rollback-failure/1") && record["status"] === "FAILED", + "CINT_ROLLBACK_FAILED", + "Rollback returned invalid failure evidence" + ); + return { + protocol, + status: "FAILED", + digest: sha256Digest(record["digest"], "rollback failure digest") + }; +} + +export async function performRollback( + adapter: RollbackAdapter, + prepared: unknown, + options: RollbackOperationOptions +): Promise; +export async function performRollback( + adapterValue: unknown, + prepared: unknown, + options: RollbackOperationOptions +): Promise { + try { + const rollback = property(adapterValue, "rollback"); + assertCint(typeof rollback === "function", "CINT_ROLLBACK_UNSUPPORTED", "Adapter has no rollback operation"); + const result: unknown = await Reflect.apply(rollback, adapterValue, [prepared, options]); + const record = verifySealedRecord(result, "rollback"); + return record["status"] === "RESTORED" ? verifyRestoredRollback(result) : failedRollback(result); + } catch (error) { + return sealRecord({ + protocol: "cint/rollback-failure/1" as const, + status: "FAILED" as const, + error_code: identifier(errorCode(error), "rollback failure code"), + rolled_back_at: isoInstant(options.at, "rollback failure time") + }); + } +} diff --git a/src/cint/schema.js b/src/cint/schema.js deleted file mode 100644 index 8d5ebf3..0000000 --- a/src/cint/schema.js +++ /dev/null @@ -1,64 +0,0 @@ -import { readFileSync } from "node:fs"; - -import Ajv2020 from "ajv/dist/2020.js"; -import addFormats from "ajv-formats"; - -const SCHEMA_DIRECTORY = new URL("../../schemas/cint/", import.meta.url); - -const SCHEMA_FILES = Object.freeze([ - ["cint/adapter-capability/1", "adapter-capability.schema.json"], - ["cint/authority/1", "authority.schema.json"], - ["cint/challenge/1", "challenge.schema.json"], - ["cint/decision/1", "decision.schema.json"], - ["cint/execution-result/1", "execution-result.schema.json"], - ["cint/intent/1", "intent.schema.json"], - ["cint/machine-state/1", "machine-state.schema.json"], - ["cint/outcome/1", "outcome.schema.json"], - ["cint/policy/1", "policy.schema.json"], - ["cint/principal/1", "principal.schema.json"], - ["cint/decision-receipt/1", "receipt.schema.json"], - ["cint/revalidation/1", "revalidation.schema.json"], - ["cint/evidence-seal/1", "seal.schema.json"] -]); - -const ajv = new Ajv2020({ allErrors: true, strict: true }); -addFormats(ajv); - -const schemas = SCHEMA_FILES.map(([protocol, file]) => { - const schema = JSON.parse(readFileSync(new URL(file, SCHEMA_DIRECTORY), "utf8")); - if (schema.properties?.protocol?.const !== protocol) { - throw new Error(`CINT schema protocol mismatch: ${file}`); - } - ajv.addSchema(schema); - return [protocol, schema.$id]; -}); - -const validators = new Map( - schemas.map(([protocol, schemaId]) => { - const validator = ajv.getSchema(schemaId); - if (!validator) throw new Error(`CINT schema did not compile: ${schemaId}`); - return [protocol, validator]; - }) -); - -export const CINT_SCHEMA_PROTOCOLS = Object.freeze(SCHEMA_FILES.map(([protocol]) => protocol)); - -function safeErrors(errors = []) { - return errors.map(({ instancePath, keyword, message, params }) => ({ - instancePath, - keyword, - message, - params - })); -} - -export function validateCintSchema(value, protocol = value?.protocol) { - const validator = validators.get(protocol); - if (!validator) return Object.freeze({ known: false, valid: false, errors: [] }); - const valid = validator(value); - return Object.freeze({ - known: true, - valid, - errors: valid ? [] : safeErrors(validator.errors) - }); -} diff --git a/src/cint/schema.ts b/src/cint/schema.ts new file mode 100644 index 0000000..79e1899 --- /dev/null +++ b/src/cint/schema.ts @@ -0,0 +1,123 @@ +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; + +import { + Ajv2020, + type ErrorObject, + type ValidateFunction +} from "ajv/dist/2020.js"; + +import type { CintProtocol } from "./types/protocols.js"; + +const SCHEMA_DIRECTORY = new URL("../../schemas/cint/", import.meta.url); + +const SCHEMA_FILES: readonly (readonly [CintProtocol, string])[] = Object.freeze([ + ["cint/adapter-capability/1", "adapter-capability.schema.json"], + ["cint/authority/1", "authority.schema.json"], + ["cint/challenge/1", "challenge.schema.json"], + ["cint/decision/1", "decision.schema.json"], + ["cint/execution-result/1", "execution-result.schema.json"], + ["cint/intent/1", "intent.schema.json"], + ["cint/machine-state/1", "machine-state.schema.json"], + ["cint/outcome/1", "outcome.schema.json"], + ["cint/policy/1", "policy.schema.json"], + ["cint/principal/1", "principal.schema.json"], + ["cint/decision-receipt/1", "receipt.schema.json"], + ["cint/revalidation/1", "revalidation.schema.json"], + ["cint/evidence-seal/1", "seal.schema.json"] +]); + +interface SchemaDocument { + readonly $id: string; + readonly properties: { + readonly protocol: { + readonly const: string; + }; + }; +} + +export interface CintSchemaError { + readonly instancePath: string; + readonly keyword: string; + readonly message?: string; + readonly params: Readonly>; +} + +export interface CintSchemaValidation { + readonly known: boolean; + readonly valid: boolean; + readonly errors: readonly CintSchemaError[]; +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isSchemaDocument(value: unknown, protocol: CintProtocol): value is SchemaDocument { + if (!isRecord(value) || typeof value["$id"] !== "string") return false; + const properties = value["properties"]; + if (!isRecord(properties)) return false; + const protocolProperty = properties["protocol"]; + return isRecord(protocolProperty) && protocolProperty["const"] === protocol; +} + +function readSchema(file: string, protocol: CintProtocol): SchemaDocument { + const parsed: unknown = JSON.parse(readFileSync(new URL(file, SCHEMA_DIRECTORY), "utf8")); + if (!isSchemaDocument(parsed, protocol)) { + throw new Error(`CINT schema protocol mismatch: ${file}`); + } + return parsed; +} + +function protocolOf(value: unknown): unknown { + return isRecord(value) ? value["protocol"] : undefined; +} + +const require = createRequire(import.meta.url); +const addFormats: (instance: Ajv2020) => Ajv2020 = require("ajv-formats"); +const ajv = new Ajv2020({ allErrors: true, strict: true }); +addFormats(ajv); + +const schemas = SCHEMA_FILES.map(([protocol, file]) => { + const schema = readSchema(file, protocol); + ajv.addSchema(schema); + return [protocol, schema.$id] as const; +}); + +const validators = new Map>( + schemas.map(([protocol, schemaId]) => { + const validator = ajv.getSchema(schemaId); + if (!validator) throw new Error(`CINT schema did not compile: ${schemaId}`); + return [protocol, validator] as const; + }) +); + +export const CINT_SCHEMA_PROTOCOLS: readonly CintProtocol[] = Object.freeze( + SCHEMA_FILES.map(([protocol]) => protocol) +); + +function safeErrors(errors: readonly ErrorObject[] | null | undefined = []): CintSchemaError[] { + return (errors ?? []).map(({ instancePath, keyword, message, params }) => { + const safe: CintSchemaError = message === undefined + ? { instancePath, keyword, params } + : { instancePath, keyword, message, params }; + return safe; + }); +} + +export function validateCintSchema( + value: unknown, + protocol: unknown = protocolOf(value) +): Readonly { + if (typeof protocol !== "string") { + return Object.freeze({ known: false, valid: false, errors: [] }); + } + const validator = validators.get(protocol as CintProtocol); + if (!validator) return Object.freeze({ known: false, valid: false, errors: [] }); + const valid = validator(value); + return Object.freeze({ + known: true, + valid, + errors: valid ? [] : safeErrors(validator.errors) + }); +} diff --git a/src/cint/seal.js b/src/cint/seal.js deleted file mode 100644 index 080a62b..0000000 --- a/src/cint/seal.js +++ /dev/null @@ -1,107 +0,0 @@ -import { createHmac, randomUUID, timingSafeEqual } from "node:crypto"; - -import { canonicalJson } from "../util.js"; -import { - assertCint, - assertExactKeys, - identifier, - isoInstant, - sealRecord, - sha256Digest, - verifyProtocolRecord, - verifySealedRecord -} from "./canonical.js"; - -function unsignedSeal(seal) { - return Object.fromEntries(Object.entries(seal).filter(([key]) => key !== "digest" && key !== "signature")); -} - -export class OutcomeSealAuthority { - #key; - - constructor(input) { - assertExactKeys(input, ["issuer_id", "key"], [], "seal authority"); - this.issuer_id = identifier(input.issuer_id, "seal authority issuer_id"); - const key = Buffer.isBuffer(input.key) ? Buffer.from(input.key) : Buffer.from(String(input.key), "utf8"); - assertCint(key.byteLength >= 32, "CINT_SEAL_KEY_INVALID", "Seal authority key must contain at least 32 bytes"); - this.#key = key; - Object.freeze(this); - } - - #sign(value) { - return createHmac("sha256", this.#key).update(canonicalJson(value)).digest("hex"); - } - - issue(input) { - assertExactKeys( - input, - ["receipt", "consumption", "revalidation", "outcome", "ledger_head", "issued_at"], - ["id"], - "outcome seal issuance" - ); - verifyProtocolRecord(input.receipt, "cint/decision-receipt/1", "receipt"); - verifySealedRecord(input.consumption, "consumption"); - verifyProtocolRecord(input.revalidation, "cint/revalidation/1", "revalidation"); - verifyProtocolRecord(input.outcome, "cint/outcome/1", "outcome"); - verifySealedRecord(input.ledger_head, "ledger head"); - assertCint(["VERIFIED", "ROLLED_BACK"].includes(input.outcome.status), "CINT_SEAL_OUTCOME_INVALID", "Only verified or restored outcomes can be sealed"); - assertCint(input.outcome.receipt_digest === input.receipt.digest, "CINT_SEAL_BINDING_INVALID", "Outcome is bound to a different receipt"); - assertCint(input.consumption.receipt_digest === input.receipt.digest, "CINT_SEAL_BINDING_INVALID", "Consumption is bound to a different receipt"); - assertCint(input.revalidation.receipt_id === input.receipt.id, "CINT_SEAL_BINDING_INVALID", "Revalidation is bound to a different receipt"); - const unsigned = { - protocol: "cint/evidence-seal/1", - id: identifier(input.id ?? `seal.${randomUUID()}`, "evidence seal id"), - issuer_id: this.issuer_id, - status: "SEALED", - outcome_status: input.outcome.status, - receipt_id: input.receipt.id, - receipt_digest: input.receipt.digest, - consumption_digest: input.consumption.digest, - revalidation_digest: input.revalidation.digest, - outcome_digest: input.outcome.digest, - ledger_head_digest: input.ledger_head.digest, - issued_at: isoInstant(input.issued_at, "evidence seal issued_at"), - signature_algorithm: "HMAC-SHA256" - }; - return sealRecord({ ...unsigned, signature: this.#sign(unsigned) }); - } - - verify(seal) { - verifyProtocolRecord(seal, "cint/evidence-seal/1", "evidence seal"); - assertExactKeys( - seal, - [ - "protocol", - "id", - "issuer_id", - "status", - "outcome_status", - "receipt_id", - "receipt_digest", - "consumption_digest", - "revalidation_digest", - "outcome_digest", - "ledger_head_digest", - "issued_at", - "signature_algorithm", - "signature", - "digest" - ], - [], - "evidence seal" - ); - assertCint(seal.protocol === "cint/evidence-seal/1" && seal.status === "SEALED", "CINT_SEAL_INVALID", "Unsupported evidence seal"); - assertCint(seal.issuer_id === this.issuer_id, "CINT_SEAL_ISSUER_MISMATCH", "Evidence seal issuer mismatch"); - for (const field of ["receipt_digest", "consumption_digest", "revalidation_digest", "outcome_digest", "ledger_head_digest", "signature"]) { - sha256Digest(seal[field], `evidence seal ${field}`); - } - const expected = Buffer.from(this.#sign(unsignedSeal(seal)), "hex"); - const actual = Buffer.from(seal.signature, "hex"); - assertCint( - expected.byteLength === actual.byteLength && timingSafeEqual(expected, actual), - "CINT_SEAL_SIGNATURE_INVALID", - "Evidence seal signature is invalid" - ); - return seal; - } -} diff --git a/src/cint/seal.ts b/src/cint/seal.ts new file mode 100644 index 0000000..07f7d40 --- /dev/null +++ b/src/cint/seal.ts @@ -0,0 +1,181 @@ +import { createHmac, randomUUID, timingSafeEqual } from "node:crypto"; + +import { + assertCint, + assertExactKeys, + assertJsonValue, + canonicalJson, + identifier, + isPlainRecord, + isoInstant, + sealRecord, + sha256Digest, + verifyProtocolRecord, + verifySealedRecord +} from "./canonical.js"; +import { verifyExecutionLedgerEntry, type ExecutionLedgerEntry } from "./evidence.js"; +import type { + ConsumptionDigest, + HmacSha256Signature, + Identifier, + ReceiptDigest, + ReceiptId, + RevalidationDigest, + SealId +} from "./types/brands.js"; +import type { + ConsumedReceiptRecord, + EvidenceSeal, + IssuedDecisionReceipt, + Outcome, + Revalidation +} from "./types/records.js"; +import type { JsonObject } from "./types/protocols.js"; + +export interface OutcomeSealInput { + readonly receipt: IssuedDecisionReceipt; + readonly consumption: ConsumedReceiptRecord; + readonly revalidation: Revalidation; + readonly outcome: Outcome; + readonly ledger_head: ExecutionLedgerEntry; + readonly issued_at: unknown; + readonly id?: unknown; +} + +function unsignedSeal(seal: EvidenceSeal): JsonObject { + const unsigned = assertJsonValue( + Object.fromEntries(Object.entries(seal).filter(([key]) => key !== "digest" && key !== "signature")), + "unsigned evidence seal" + ); + assertCint(isPlainRecord(unsigned), "CINT_OBJECT_INVALID", "Unsigned evidence seal must be an object"); + return unsigned; +} + +function consumedReceipt(value: unknown): ConsumedReceiptRecord { + const record = verifySealedRecord(value, "consumption"); + assertCint( + record["protocol"] === "cint/receipt-store-entry/1" && record["state"] === "CONSUMED", + "CINT_RECEIPT_STORE_MISMATCH", + "Evidence sealing requires a consumed receipt record" + ); + return { + protocol: "cint/receipt-store-entry/1", + state: "CONSUMED", + receipt_id: identifier(record["receipt_id"], "consumption receipt_id"), + receipt_digest: sha256Digest(record["receipt_digest"], "consumption receipt_digest"), + consumed_at: isoInstant(record["consumed_at"], "consumption consumed_at"), + revalidation_digest: sha256Digest(record["revalidation_digest"], "consumption revalidation_digest"), + digest: sha256Digest(record["digest"], "consumption digest") + }; +} + +function ledgerHead(value: unknown): ExecutionLedgerEntry { + return verifyExecutionLedgerEntry(value, "ledger head"); +} + +export class OutcomeSealAuthority { + readonly issuer_id: Identifier; + #key: Buffer; + + constructor(value: unknown) { + const input = assertExactKeys(value, ["issuer_id", "key"], [], "seal authority"); + this.issuer_id = identifier(input["issuer_id"], "seal authority issuer_id"); + const rawKey = input["key"]; + const key = Buffer.isBuffer(rawKey) ? Buffer.from(rawKey) : Buffer.from(String(rawKey), "utf8"); + assertCint(key.byteLength >= 32, "CINT_SEAL_KEY_INVALID", "Seal authority key must contain at least 32 bytes"); + this.#key = key; + Object.freeze(this); + } + + #sign(value: unknown): HmacSha256Signature { + return sha256Digest( + createHmac("sha256", this.#key).update(canonicalJson(assertJsonValue(value))).digest("hex"), + "evidence seal signature" + ); + } + + issue(input: OutcomeSealInput): EvidenceSeal; + issue(value: unknown): EvidenceSeal { + const input = assertExactKeys( + value, + ["receipt", "consumption", "revalidation", "outcome", "ledger_head", "issued_at"], + ["id"], + "outcome seal issuance" + ); + const receipt = verifyProtocolRecord(input["receipt"], "cint/decision-receipt/1", "receipt"); + const consumption = consumedReceipt(input["consumption"]); + const revalidation = verifyProtocolRecord(input["revalidation"], "cint/revalidation/1", "revalidation"); + const outcome = verifyProtocolRecord(input["outcome"], "cint/outcome/1", "outcome"); + const head = ledgerHead(input["ledger_head"]); + assertCint(outcome.status === "VERIFIED" || outcome.status === "ROLLED_BACK", "CINT_SEAL_OUTCOME_INVALID", "Only verified or restored outcomes can be sealed"); + assertCint(outcome.receipt_digest === receipt.digest, "CINT_SEAL_BINDING_INVALID", "Outcome is bound to a different receipt"); + assertCint(consumption.receipt_digest === receipt.digest, "CINT_SEAL_BINDING_INVALID", "Consumption is bound to a different receipt"); + assertCint(revalidation.receipt_id === receipt.id, "CINT_SEAL_BINDING_INVALID", "Revalidation is bound to a different receipt"); + const unsigned = { + protocol: "cint/evidence-seal/1" as const, + id: identifier(input["id"] ?? `seal.${randomUUID()}`, "evidence seal id"), + issuer_id: this.issuer_id, + status: "SEALED" as const, + outcome_status: outcome.status, + receipt_id: receipt.id, + receipt_digest: receipt.digest, + consumption_digest: consumption.digest, + revalidation_digest: revalidation.digest, + outcome_digest: outcome.digest, + ledger_head_digest: head.digest, + issued_at: isoInstant(input["issued_at"], "evidence seal issued_at"), + signature_algorithm: "HMAC-SHA256" as const + }; + return sealRecord({ ...unsigned, signature: this.#sign(unsigned) }); + } + + verify(value: unknown): EvidenceSeal { + const seal = verifyProtocolRecord(value, "cint/evidence-seal/1", "evidence seal"); + assertExactKeys( + seal, + [ + "protocol", + "id", + "issuer_id", + "status", + "outcome_status", + "receipt_id", + "receipt_digest", + "consumption_digest", + "revalidation_digest", + "outcome_digest", + "ledger_head_digest", + "issued_at", + "signature_algorithm", + "signature", + "digest" + ], + [], + "evidence seal" + ); + assertCint( + seal.protocol === "cint/evidence-seal/1" && seal.status === "SEALED", + "CINT_SEAL_INVALID", + "Unsupported evidence seal" + ); + assertCint(seal.issuer_id === this.issuer_id, "CINT_SEAL_ISSUER_MISMATCH", "Evidence seal issuer mismatch"); + for (const field of [ + "receipt_digest", + "consumption_digest", + "revalidation_digest", + "outcome_digest", + "ledger_head_digest", + "signature" + ] as const) { + sha256Digest(seal[field], `evidence seal ${field}`); + } + const expected = Buffer.from(this.#sign(unsignedSeal(seal)), "hex"); + const actual = Buffer.from(seal.signature, "hex"); + assertCint( + expected.byteLength === actual.byteLength && timingSafeEqual(expected, actual), + "CINT_SEAL_SIGNATURE_INVALID", + "Evidence seal signature is invalid" + ); + return seal; + } +} diff --git a/src/cint/state-machine.js b/src/cint/state-machine.js deleted file mode 100644 index eaffc55..0000000 --- a/src/cint/state-machine.js +++ /dev/null @@ -1,82 +0,0 @@ -import { - assertCint, - assertExactKeys, - identifier, - isoInstant, - sealRecord, - sha256Digest, - verifySealedRecord -} from "./canonical.js"; - -export const CINT_STATES = Object.freeze([ - "REQUESTED", - "CHALLENGED", - "DENIED", - "REVIEW", - "ADMITTED", - "RECEIPT_ISSUED", - "REVALIDATED", - "REVOKED", - "RECEIPT_CONSUMED", - "EXECUTING", - "INTERRUPTED", - "VERIFIED", - "ROLLED_BACK", - "SEALED", - "FAIL_CLOSED", - "REPLAY_REJECTED" -]); - -const TRANSITIONS = Object.freeze({ - REQUESTED: ["CHALLENGED", "FAIL_CLOSED"], - CHALLENGED: ["DENIED", "REVIEW", "ADMITTED", "FAIL_CLOSED"], - ADMITTED: ["RECEIPT_ISSUED", "REVOKED", "FAIL_CLOSED"], - RECEIPT_ISSUED: ["REVALIDATED", "REVOKED", "REPLAY_REJECTED", "FAIL_CLOSED"], - REVALIDATED: ["RECEIPT_CONSUMED", "REVOKED", "REPLAY_REJECTED", "FAIL_CLOSED"], - RECEIPT_CONSUMED: ["EXECUTING", "FAIL_CLOSED"], - EXECUTING: ["VERIFIED", "INTERRUPTED", "ROLLED_BACK", "FAIL_CLOSED"], - INTERRUPTED: ["ROLLED_BACK", "FAIL_CLOSED"], - VERIFIED: ["SEALED", "ROLLED_BACK", "FAIL_CLOSED"], - DENIED: [], - REVIEW: [], - REVOKED: [], - ROLLED_BACK: ["SEALED"], - SEALED: [], - FAIL_CLOSED: [], - REPLAY_REJECTED: [] -}); - -export function createStateMachine(input) { - assertExactKeys(input, ["id", "subject_id", "created_at"], [], "state machine"); - const createdAt = isoInstant(input.created_at, "state machine created_at"); - return sealRecord({ - protocol: "cint/state-machine/1", - id: identifier(input.id, "state machine id"), - subject_id: identifier(input.subject_id, "state machine subject id"), - state: "REQUESTED", - sequence: 0, - history: [{ state: "REQUESTED", at: createdAt, evidence_digest: null }] - }); -} - -export function transitionState(machine, input) { - verifySealedRecord(machine, "state machine"); - assertExactKeys(input, ["state", "at", "evidence_digest"], [], "state transition"); - assertCint(CINT_STATES.includes(input.state), "CINT_STATE_INVALID", `Unknown CINT state: ${input.state}`); - assertCint(TRANSITIONS[machine.state].includes(input.state), "CINT_STATE_TRANSITION", `Transition ${machine.state} -> ${input.state} is forbidden`); - if (input.evidence_digest !== null) sha256Digest(input.evidence_digest, "state transition evidence digest"); - const unsigned = Object.fromEntries(Object.entries(machine).filter(([key]) => key !== "digest")); - return sealRecord({ - ...unsigned, - state: input.state, - sequence: machine.sequence + 1, - history: [ - ...machine.history, - { - state: input.state, - at: isoInstant(input.at, "state transition time"), - evidence_digest: input.evidence_digest - } - ] - }); -} diff --git a/src/cint/state-machine.ts b/src/cint/state-machine.ts new file mode 100644 index 0000000..ae26e78 --- /dev/null +++ b/src/cint/state-machine.ts @@ -0,0 +1,160 @@ +import { + assertCint, + assertExactKeys, + identifier, + integer, + isoInstant, + sealRecord, + sha256Digest, + verifySealedRecord +} from "./canonical.js"; +import type { + CanonicalInstant, + Identifier, + RecordDigest +} from "./types/brands.js"; +import type { SealedRecord } from "./types/records.js"; +import type { CintLifecycleState } from "./types/states.js"; + +export const CINT_STATES = Object.freeze([ + "REQUESTED", + "CHALLENGED", + "DENIED", + "REVIEW", + "ADMITTED", + "RECEIPT_ISSUED", + "REVALIDATED", + "REVOKED", + "RECEIPT_CONSUMED", + "EXECUTING", + "INTERRUPTED", + "VERIFIED", + "ROLLED_BACK", + "SEALED", + "FAIL_CLOSED", + "REPLAY_REJECTED" +] as const); + +const TRANSITIONS: Readonly> = Object.freeze({ + REQUESTED: ["CHALLENGED", "FAIL_CLOSED"], + CHALLENGED: ["DENIED", "REVIEW", "ADMITTED", "FAIL_CLOSED"], + ADMITTED: ["RECEIPT_ISSUED", "REVOKED", "FAIL_CLOSED"], + RECEIPT_ISSUED: ["REVALIDATED", "REVOKED", "REPLAY_REJECTED", "FAIL_CLOSED"], + REVALIDATED: ["RECEIPT_CONSUMED", "REVOKED", "REPLAY_REJECTED", "FAIL_CLOSED"], + RECEIPT_CONSUMED: ["EXECUTING", "FAIL_CLOSED"], + EXECUTING: ["VERIFIED", "INTERRUPTED", "ROLLED_BACK", "FAIL_CLOSED"], + INTERRUPTED: ["ROLLED_BACK", "FAIL_CLOSED"], + VERIFIED: ["SEALED", "ROLLED_BACK", "FAIL_CLOSED"], + DENIED: [], + REVIEW: [], + REVOKED: [], + ROLLED_BACK: ["SEALED"], + SEALED: [], + FAIL_CLOSED: [], + REPLAY_REJECTED: [] +}); + +export interface StateHistoryEntry { + readonly state: CintLifecycleState; + readonly at: CanonicalInstant; + readonly evidence_digest: RecordDigest | null; +} + +export interface CintStateMachine extends SealedRecord<"cint/state-machine/1"> { + readonly id: Identifier; + readonly subject_id: Identifier; + readonly state: CintLifecycleState; + readonly sequence: number; + readonly history: readonly StateHistoryEntry[]; +} + +function lifecycleState(value: unknown): CintLifecycleState { + switch (value) { + case "REQUESTED": + case "CHALLENGED": + case "DENIED": + case "REVIEW": + case "ADMITTED": + case "RECEIPT_ISSUED": + case "REVALIDATED": + case "REVOKED": + case "RECEIPT_CONSUMED": + case "EXECUTING": + case "INTERRUPTED": + case "VERIFIED": + case "ROLLED_BACK": + case "SEALED": + case "FAIL_CLOSED": + case "REPLAY_REJECTED": + return value; + default: + assertCint(false, "CINT_STATE_INVALID", `Unknown CINT state: ${String(value)}`); + } +} + +function stateHistory(value: unknown): readonly StateHistoryEntry[] { + assertCint(Array.isArray(value), "CINT_STATE_INVALID", "State machine history must be an array"); + return value.map((entry, index) => { + const checked = assertExactKeys(entry, ["state", "at", "evidence_digest"], [], `state history[${index}]`); + const rawEvidenceDigest = checked["evidence_digest"]; + return { + state: lifecycleState(checked["state"]), + at: isoInstant(checked["at"], `state history[${index}].at`), + evidence_digest: rawEvidenceDigest === null + ? null + : sha256Digest(rawEvidenceDigest, `state history[${index}].evidence_digest`) + }; + }); +} + +function verifyStateMachine(value: unknown): CintStateMachine { + const record = verifySealedRecord(value, "state machine"); + assertCint(record["protocol"] === "cint/state-machine/1", "CINT_PROTOCOL_INVALID", "State machine uses an unsupported protocol"); + return { + protocol: "cint/state-machine/1", + id: identifier(record["id"], "state machine id"), + subject_id: identifier(record["subject_id"], "state machine subject id"), + state: lifecycleState(record["state"]), + sequence: integer(record["sequence"], "state machine sequence"), + history: stateHistory(record["history"]), + digest: sha256Digest(record["digest"], "state machine digest") + }; +} + +export function createStateMachine(value: unknown): CintStateMachine { + const input = assertExactKeys(value, ["id", "subject_id", "created_at"], [], "state machine"); + const createdAt = isoInstant(input["created_at"], "state machine created_at"); + return sealRecord({ + protocol: "cint/state-machine/1" as const, + id: identifier(input["id"], "state machine id"), + subject_id: identifier(input["subject_id"], "state machine subject id"), + state: "REQUESTED" as const, + sequence: 0, + history: [{ state: "REQUESTED", at: createdAt, evidence_digest: null }] + }); +} + +export function transitionState(machineValue: unknown, inputValue: unknown): CintStateMachine { + const machine = verifyStateMachine(machineValue); + const input = assertExactKeys(inputValue, ["state", "at", "evidence_digest"], [], "state transition"); + const nextState = lifecycleState(input["state"]); + assertCint(TRANSITIONS[machine.state].includes(nextState), "CINT_STATE_TRANSITION", `Transition ${machine.state} -> ${nextState} is forbidden`); + const rawEvidenceDigest = input["evidence_digest"]; + const evidenceDigest = rawEvidenceDigest === null + ? null + : sha256Digest(rawEvidenceDigest, "state transition evidence digest"); + const { digest: _digest, ...unsigned } = machine; + return sealRecord({ + ...unsigned, + state: nextState, + sequence: machine.sequence + 1, + history: [ + ...machine.history, + { + state: nextState, + at: isoInstant(input["at"], "state transition time"), + evidence_digest: evidenceDigest + } + ] + }); +} diff --git a/src/cint/store.js b/src/cint/store.ts similarity index 52% rename from src/cint/store.js rename to src/cint/store.ts index d49884a..e78d594 100644 --- a/src/cint/store.js +++ b/src/cint/store.ts @@ -1,36 +1,89 @@ import { access, mkdir, open, readFile, unlink } from "node:fs/promises"; import path from "node:path"; +import type { FileHandle } from "node:fs/promises"; -import { canonicalJson, sha256 } from "../util.js"; import { CintError, assertCint, assertExactKeys, + assertJsonValue, + canonicalJson, identifier, immutableRecord, isoInstant, parseCanonicalJson, sealRecord, + sha256, sha256Digest, verifyProtocolRecord, verifySealedRecord } from "./canonical.js"; +import type { + ReceiptId, + RevalidationDigest +} from "./types/brands.js"; +import type { + ConsumedReceiptRecord, + IssuedDecisionReceipt, + PendingReceiptRecord +} from "./types/records.js"; -async function exists(filePath) { +interface ReceiptPaths { + readonly pending: string; + readonly consumed: string; + readonly rejected: string; + readonly lock: string; +} + +export type ReceiptStoreInspection = + | Readonly<{ state: "CONSUMED"; path: "consumed" }> + | Readonly<{ state: "REJECTED"; path: "rejected" }> + | Readonly<{ state: "LOCKED"; path: "locks" }> + | Readonly<{ state: "PENDING"; path: "pending" }> + | Readonly<{ state: "ABSENT"; path: null }>; + +interface ReceiptConsumptionResult { + readonly status?: unknown; + readonly reason_codes?: unknown; + readonly digest?: unknown; +} + +type RevalidationCallback = (receipt: IssuedDecisionReceipt) => unknown; + +function errorCode(error: unknown): unknown { + return error !== null && (typeof error === "object" || typeof error === "function") + ? Reflect.get(error, "code") + : undefined; +} + +function isRevalidationCallback(value: unknown): value is RevalidationCallback { + return typeof value === "function"; +} + +function consumptionResult(value: unknown): ReceiptConsumptionResult { + assertCint(value !== null && typeof value === "object", "CINT_REVALIDATION_INVALID", "Revalidation returned no result"); + return { + status: Reflect.get(value, "status"), + reason_codes: Reflect.get(value, "reason_codes"), + digest: Reflect.get(value, "digest") + }; +} + +async function exists(filePath: string): Promise { try { await access(filePath); return true; } catch (error) { - if (error.code === "ENOENT") return false; + if (errorCode(error) === "ENOENT") return false; throw error; } } -async function writeExclusive(filePath, value) { - let handle; +async function writeExclusive(filePath: string, value: unknown): Promise { + let handle: FileHandle | undefined; try { handle = await open(filePath, "wx", 0o600); - await handle.writeFile(canonicalJson(value), "utf8"); + await handle.writeFile(canonicalJson(assertJsonValue(value)), "utf8"); await handle.sync(); } finally { if (handle) await handle.close(); @@ -38,7 +91,13 @@ async function writeExclusive(filePath, value) { } export class FileReceiptStore { - constructor(root) { + readonly root: string; + readonly pending: string; + readonly consumed: string; + readonly rejected: string; + readonly locks: string; + + constructor(root: string) { this.root = path.resolve(root); this.pending = path.join(this.root, "pending"); this.consumed = path.join(this.root, "consumed"); @@ -46,15 +105,15 @@ export class FileReceiptStore { this.locks = path.join(this.root, "locks"); } - async initialize() { + async initialize(): Promise { for (const directory of [this.pending, this.consumed, this.rejected, this.locks]) { await mkdir(directory, { recursive: true, mode: 0o700 }); } return this; } - #paths(receiptId) { - identifier(receiptId, "receipt id"); + #paths(value: unknown): ReceiptPaths { + const receiptId = identifier(value, "receipt id"); const name = `${sha256(receiptId)}.json`; return { pending: path.join(this.pending, name), @@ -64,30 +123,30 @@ export class FileReceiptStore { }; } - async register(receipt, input) { - verifyProtocolRecord(receipt, "cint/decision-receipt/1", "receipt"); + async register(receiptValue: unknown, inputValue: unknown): Promise { + const receipt = verifyProtocolRecord(receiptValue, "cint/decision-receipt/1", "receipt"); assertCint( receipt.protocol === "cint/decision-receipt/1" && receipt.status === "ISSUED", "CINT_RECEIPT_PROTOCOL", "Store accepts only issued CINT decision receipts" ); - assertExactKeys(input, ["registered_at"], [], "receipt registration"); + const input = assertExactKeys(inputValue, ["registered_at"], [], "receipt registration"); await this.initialize(); const paths = this.#paths(receipt.id); if ((await exists(paths.consumed)) || (await exists(paths.rejected))) { throw new CintError("CINT_RECEIPT_REPLAY_REJECTED", "Receipt already reached a terminal store state"); } const entry = sealRecord({ - protocol: "cint/receipt-store-entry/1", - state: "PENDING", + protocol: "cint/receipt-store-entry/1" as const, + state: "PENDING" as const, receipt_id: receipt.id, receipt_digest: receipt.digest, - registered_at: isoInstant(input.registered_at, "receipt registered_at") + registered_at: isoInstant(input["registered_at"], "receipt registered_at") }); try { await writeExclusive(paths.pending, entry); } catch (error) { - if (error.code === "EEXIST") { + if (errorCode(error) === "EEXIST") { throw new CintError("CINT_RECEIPT_ALREADY_REGISTERED", "Receipt is already registered"); } throw error; @@ -95,7 +154,7 @@ export class FileReceiptStore { return entry; } - async inspect(receiptId) { + async inspect(receiptId: unknown): Promise { await this.initialize(); const paths = this.#paths(receiptId); if (await exists(paths.consumed)) return immutableRecord({ state: "CONSUMED", path: "consumed" }); @@ -105,26 +164,27 @@ export class FileReceiptStore { return immutableRecord({ state: "ABSENT", path: null }); } - async consume(receipt, input) { - verifyProtocolRecord(receipt, "cint/decision-receipt/1", "receipt"); + async consume(receiptValue: unknown, inputValue: unknown): Promise { + const receipt = verifyProtocolRecord(receiptValue, "cint/decision-receipt/1", "receipt"); assertCint( receipt.protocol === "cint/decision-receipt/1" && receipt.status === "ISSUED", "CINT_RECEIPT_PROTOCOL", "Store accepts only issued CINT decision receipts" ); - assertExactKeys(input, ["consumed_at", "revalidate"], [], "receipt consumption"); - assertCint(typeof input.revalidate === "function", "CINT_REVALIDATION_INVALID", "Receipt consumption requires a revalidation function"); + const input = assertExactKeys(inputValue, ["consumed_at", "revalidate"], [], "receipt consumption"); + const revalidate = input["revalidate"]; + assertCint(isRevalidationCallback(revalidate), "CINT_REVALIDATION_INVALID", "Receipt consumption requires a revalidation function"); await this.initialize(); - const consumedAt = isoInstant(input.consumed_at, "receipt consumed_at"); + const consumedAt = isoInstant(input["consumed_at"], "receipt consumed_at"); const paths = this.#paths(receipt.id); - let lock; + let lock: FileHandle | undefined; try { lock = await open(paths.lock, "wx", 0o600); await lock.writeFile(canonicalJson({ receipt_id: receipt.id, locked_at: consumedAt }), "utf8"); await lock.sync(); } catch (error) { if (lock) await lock.close().catch(() => {}); - if (error.code === "EEXIST") { + if (errorCode(error) === "EEXIST") { throw new CintError("CINT_RECEIPT_REPLAY_REJECTED", "Receipt is already in flight or consumed"); } throw error; @@ -135,36 +195,41 @@ export class FileReceiptStore { throw new CintError("CINT_RECEIPT_REPLAY_REJECTED", "Receipt already reached a terminal store state"); } const pendingText = await readFile(paths.pending, "utf8").catch((error) => { - if (error.code === "ENOENT") return null; + if (errorCode(error) === "ENOENT") return null; throw error; }); assertCint(pendingText !== null, "CINT_RECEIPT_UNREGISTERED", "Receipt is not registered"); const pendingEntry = parseCanonicalJson(pendingText, "pending receipt entry"); - verifySealedRecord(pendingEntry, "pending receipt entry"); + const verifiedPendingEntry = verifySealedRecord(pendingEntry, "pending receipt entry"); assertCint( - pendingEntry.receipt_id === receipt.id && pendingEntry.receipt_digest === receipt.digest, + verifiedPendingEntry["receipt_id"] === receipt.id && verifiedPendingEntry["receipt_digest"] === receipt.digest, "CINT_RECEIPT_STORE_MISMATCH", "Registered receipt digest does not match the presented receipt" ); - let revalidation; + let rawRevalidation: unknown; if (Date.parse(consumedAt) >= Date.parse(receipt.expires_at)) { - revalidation = { status: "REVOKED", reason_codes: ["CINT_RECEIPT_EXPIRED"], digest: receipt.digest }; + rawRevalidation = { status: "REVOKED", reason_codes: ["CINT_RECEIPT_EXPIRED"], digest: receipt.digest }; } else { - revalidation = await input.revalidate(receipt); + rawRevalidation = await revalidate(receipt); } - assertCint(revalidation && typeof revalidation === "object", "CINT_REVALIDATION_INVALID", "Revalidation returned no result"); - const revalidationDigest = revalidation.digest ?? receipt.digest; - sha256Digest(revalidationDigest, "revalidation digest"); + const revalidation = consumptionResult(rawRevalidation); + const revalidationDigest = sha256Digest( + revalidation.digest ?? receipt.digest, + "revalidation digest" + ); if (revalidation.status !== "VALID") { + const reasonCodes = Array.isArray(revalidation.reason_codes) + ? revalidation.reason_codes.map(String) + : ["CINT_REVALIDATION_FAILED"]; const rejectedEntry = sealRecord({ - protocol: "cint/receipt-store-entry/1", - state: "REJECTED", + protocol: "cint/receipt-store-entry/1" as const, + state: "REJECTED" as const, receipt_id: receipt.id, receipt_digest: receipt.digest, rejected_at: consumedAt, revalidation_digest: revalidationDigest, - reason_codes: Array.isArray(revalidation.reason_codes) ? revalidation.reason_codes.map(String) : ["CINT_REVALIDATION_FAILED"] + reason_codes: reasonCodes }); await writeExclusive(paths.rejected, rejectedEntry); await unlink(paths.pending); @@ -173,8 +238,8 @@ export class FileReceiptStore { }); } const consumedEntry = sealRecord({ - protocol: "cint/receipt-store-entry/1", - state: "CONSUMED", + protocol: "cint/receipt-store-entry/1" as const, + state: "CONSUMED" as const, receipt_id: receipt.id, receipt_digest: receipt.digest, consumed_at: consumedAt, @@ -184,6 +249,7 @@ export class FileReceiptStore { await unlink(paths.pending); return consumedEntry; } finally { + assertCint(lock !== undefined, "CINT_RECEIPT_REPLAY_REJECTED", "Receipt lock was not acquired"); await lock.close().catch(() => {}); await unlink(paths.lock).catch(() => {}); } diff --git a/src/cint/types/adapters.ts b/src/cint/types/adapters.ts new file mode 100644 index 0000000..0c34b37 --- /dev/null +++ b/src/cint/types/adapters.ts @@ -0,0 +1,46 @@ +import type { AdapterId, CanonicalInstant } from "./brands.js"; +import type { + AdapterCapability, + AdapterExecution, + IntentRecord, + OutcomeVerification, + RestoredRollback +} from "./records.js"; + +declare const preparedAction: unique symbol; +declare const verifiedAdapterOutput: unique symbol; + +export interface PreparedAction { + readonly [preparedAction]: "PreparedAction"; +} + +export interface VerifiedAdapterOutput { + readonly [verifiedAdapterOutput]: "VerifiedAdapterOutput"; + readonly execution: AdapterExecution; + readonly verification: OutcomeVerification; +} + +export interface AdapterOperationOptions { + readonly at: CanonicalInstant; + readonly signal?: AbortSignal; +} + +export interface CintAdapter { + readonly id: AdapterId; + readonly capability: AdapterCapability; + prepare(intent: IntentRecord, options: AdapterOperationOptions): Promise; + execute(prepared: Prepared, options: AdapterOperationOptions): Promise; + verify(prepared: Prepared, execution: AdapterExecution, options: AdapterOperationOptions): Promise; + rollback?(prepared: Prepared, options: AdapterOperationOptions): Promise; +} + +export type LegacyAgentFloorAdmission = "ADMITTED" | "REJECTED"; + +export interface LegacyAgentFloorBoundary { + readonly id: AdapterId; + readonly capability: AdapterCapability; + prepare(intent: IntentRecord, options: AdapterOperationOptions): Promise; + executeCandidate(prepared: Prepared, options: AdapterOperationOptions): Promise; + verifyOutcome(prepared: Prepared, execution: AdapterExecution, options: AdapterOperationOptions): Promise; + restore?(prepared: Prepared, options: AdapterOperationOptions): Promise; +} diff --git a/src/cint/types/brands.ts b/src/cint/types/brands.ts new file mode 100644 index 0000000..eb77bdd --- /dev/null +++ b/src/cint/types/brands.ts @@ -0,0 +1,45 @@ +declare const scalarBrand: unique symbol; +declare const roleBrand: unique symbol; + +export type Brand = Value & { + readonly [scalarBrand]: Name; +}; + +type Role = Base & { + readonly [roleBrand]: Name; +}; + +export type CanonicalJson = Brand; +export type CanonicalInstant = Brand; +export type ProtocolIdentifier = Brand; +export type Identifier = Brand; +export type Sha256Digest = Brand; +export type HmacSha256Signature = Role; + +export type PrincipalId = Role; +export type AuthorityId = Role; +export type PolicyId = Role; +export type IntentId = Role; +export type DecisionId = Role; +export type ReceiptId = Role; +export type AdapterId = Role; +export type MachineStateId = Role; +export type SealId = Role; + +export type TargetDigest = Role; +export type ActionDigest = Role; +export type ContextDigest = Role; +export type RecordDigest = Role; +export type IntentDigest = Role; +export type DecisionDigest = Role; +export type ReceiptDigest = Role; +export type BindingDigest = Role; +export type OutcomeDigest = Role; +export type ExecutionDigest = Role; +export type VerificationDigest = Role; +export type RevalidationDigest = Role; +export type ConsumptionDigest = Role; +export type EvidenceSealDigest = Role; +export type MachineStateDigest = Role; + +export type Epoch = Brand; diff --git a/src/cint/types/errors.ts b/src/cint/types/errors.ts new file mode 100644 index 0000000..b1c762c --- /dev/null +++ b/src/cint/types/errors.ts @@ -0,0 +1,108 @@ +export type CintErrorCode = + | "CINT_ACTION_DIGEST_CHANGED" + | "CINT_ADAPTER_ACTION_UNSUPPORTED" + | "CINT_ADAPTER_CAPABILITY_INVALID" + | "CINT_ADAPTER_CHANGED" + | "CINT_ADAPTER_CONSEQUENCE_UNSUPPORTED" + | "CINT_ADAPTER_MISMATCH" + | "CINT_ADAPTER_PREPARE_UNSAFE" + | "CINT_ARRAY_DUPLICATE" + | "CINT_ARRAY_INVALID" + | "CINT_AUTHORITY_ACTION_DENIED" + | "CINT_AUTHORITY_CHAIN_MISMATCH" + | "CINT_AUTHORITY_CHANGED" + | "CINT_AUTHORITY_EXPIRED" + | "CINT_AUTHORITY_GRANTS" + | "CINT_AUTHORITY_INACTIVE" + | "CINT_AUTHORITY_NOT_YET_VALID" + | "CINT_AUTHORITY_POLICY_DENIED" + | "CINT_AUTHORITY_PRINCIPAL_MISMATCH" + | "CINT_AUTHORITY_ROLLBACK" + | "CINT_AUTHORITY_ROLLBACK_REQUIRED" + | "CINT_AUTHORITY_TIME" + | "CINT_CLI_COMMAND" + | "CINT_CODEX_ARGS_INVALID" + | "CINT_CODEX_PACKET_CHANGED" + | "CINT_CODEX_TASK_CHANGED" + | "CINT_CONSEQUENCE_INVALID" + | "CINT_CONTEXT_CHANGED" + | "CINT_COUNTER_INTENT_UNRESOLVED" + | "CINT_DECISION_TIME" + | "CINT_DIGEST_INVALID" + | "CINT_DIGEST_PRESET" + | "CINT_EFFECT_UNDECLARED" + | "CINT_EXECUTION_FAILED" + | "CINT_EXECUTION_INTERRUPTED" + | "CINT_FAIL_CLOSED" + | "CINT_IDENTIFIER_INVALID" + | "CINT_INTEGER_INVALID" + | "CINT_INTENT_CHANGED" + | "CINT_JSON_INVALID" + | "CINT_JSON_NOT_CANONICAL" + | "CINT_LEDGER_CHAIN_INVALID" + | "CINT_LEDGER_LOCKED" + | "CINT_MACHINE_STATE_CHANGED" + | "CINT_MACHINE_STATE_INVALID" + | "CINT_OBJECT_INVALID" + | "CINT_OUTCOME_UNVERIFIED" + | "CINT_POLICY_ACTION_DENIED" + | "CINT_POLICY_ADAPTER_DENIED" + | "CINT_POLICY_CHANGED" + | "CINT_POLICY_CONTRADICTION" + | "CINT_POLICY_INACTIVE" + | "CINT_POLICY_INVALID" + | "CINT_PRINCIPAL_AUTH" + | "CINT_PRINCIPAL_CHANGED" + | "CINT_PRINCIPAL_MISMATCH" + | "CINT_PRINCIPAL_TYPE" + | "CINT_PRINCIPAL_UNAUTHENTICATED" + | "CINT_PROTOCOL_INVALID" + | "CINT_RECEIPT_ALREADY_REGISTERED" + | "CINT_RECEIPT_BINDING_INVALID" + | "CINT_RECEIPT_DECISION_INELIGIBLE" + | "CINT_RECEIPT_EXPIRED" + | "CINT_RECEIPT_INVALID" + | "CINT_RECEIPT_ISSUER_MISMATCH" + | "CINT_RECEIPT_KEY_INVALID" + | "CINT_RECEIPT_PROTOCOL" + | "CINT_RECEIPT_REPLAY_REJECTED" + | "CINT_RECEIPT_REVOKED" + | "CINT_RECEIPT_SIGNATURE_INVALID" + | "CINT_RECEIPT_STATUS" + | "CINT_RECEIPT_STORE_MISMATCH" + | "CINT_RECEIPT_TIME" + | "CINT_RECEIPT_UNREGISTERED" + | "CINT_RECORD_INVALID" + | "CINT_RECORD_TAMPERED" + | "CINT_REQUIRED_FIELD" + | "CINT_REVALIDATION_FAILED" + | "CINT_REVALIDATION_INVALID" + | "CINT_ROLLBACK_FAILED" + | "CINT_ROLLBACK_REQUIRED" + | "CINT_ROLLBACK_UNSUPPORTED" + | "CINT_SCHEMA_INVALID" + | "CINT_SCHEMA_UNAVAILABLE" + | "CINT_SEAL_BINDING_INVALID" + | "CINT_SEAL_FAILED" + | "CINT_SEAL_INVALID" + | "CINT_SEAL_ISSUER_MISMATCH" + | "CINT_SEAL_KEY_INVALID" + | "CINT_SEAL_OUTCOME_INVALID" + | "CINT_SEAL_SIGNATURE_INVALID" + | "CINT_SILENT_REQUEST" + | "CINT_STATE_INVALID" + | "CINT_STATE_TRANSITION" + | "CINT_STRING_INVALID" + | "CINT_TARGET_CHANGED" + | "CINT_TARGET_INVALID" + | "CINT_TARGET_STATE_CHANGED" + | "CINT_TIME_INVALID" + | "CINT_UNAVAILABLE" + | "CINT_UNKNOWN_FIELD"; + +export interface CintErrorShape { + readonly name: "CintError"; + readonly code: CintErrorCode; + readonly message: string; + readonly details?: Readonly>; +} diff --git a/src/cint/types/protocols.ts b/src/cint/types/protocols.ts new file mode 100644 index 0000000..bc0a281 --- /dev/null +++ b/src/cint/types/protocols.ts @@ -0,0 +1,37 @@ +export type CintProtocol = + | "cint/adapter-capability/1" + | "cint/authority/1" + | "cint/challenge/1" + | "cint/decision/1" + | "cint/execution-result/1" + | "cint/intent/1" + | "cint/machine-state/1" + | "cint/outcome/1" + | "cint/policy/1" + | "cint/principal/1" + | "cint/decision-receipt/1" + | "cint/revalidation/1" + | "cint/evidence-seal/1"; + +export type InternalCintProtocol = + | CintProtocol + | "cint/receipt-store-entry/1" + | "cint/outcome-verification/1" + | "cint/rollback/1" + | "cint/rollback-failure/1" + | "cint/state-machine/1" + | "cint/synthetic-execution/1" + | "cint/codex-delegation-execution/1" + | "cint/execution-interruption/1" + | "cint/execution-ledger/1"; + +export type JsonPrimitive = null | boolean | number | string; +export type JsonValue = JsonPrimitive | JsonObject | readonly JsonValue[]; +export interface JsonObject { + readonly [key: string]: JsonValue; +} + +export interface UnknownProtocolRecord { + readonly protocol?: unknown; + readonly [key: string]: unknown; +} diff --git a/src/cint/types/records.ts b/src/cint/types/records.ts new file mode 100644 index 0000000..a8dc0eb --- /dev/null +++ b/src/cint/types/records.ts @@ -0,0 +1,399 @@ +import type { + ActionDigest, + AdapterId, + AuthorityId, + BindingDigest, + CanonicalInstant, + ConsumptionDigest, + ContextDigest, + DecisionDigest, + DecisionId, + Epoch, + EvidenceSealDigest, + ExecutionDigest, + HmacSha256Signature, + Identifier, + IntentDigest, + IntentId, + MachineStateDigest, + MachineStateId, + OutcomeDigest, + PolicyId, + PrincipalId, + ReceiptDigest, + ReceiptId, + RecordDigest, + RevalidationDigest, + SealId, + Sha256Digest, + TargetDigest, + VerificationDigest +} from "./brands.js"; +import type { InternalCintProtocol, JsonObject, JsonValue } from "./protocols.js"; + +export interface SealedRecord< + Protocol extends InternalCintProtocol, + Digest extends Sha256Digest = RecordDigest +> { + readonly protocol: Protocol; + readonly digest: Digest; +} + +export type ConsequenceClass = "READ_ONLY" | "CONSEQUENTIAL"; + +export interface CintAction { + readonly adapter: AdapterId; + readonly type: Identifier; + readonly target: JsonValue; + readonly parameters: JsonValue; + readonly consequence: ConsequenceClass; +} + +export interface AdapterCapability extends SealedRecord<"cint/adapter-capability/1"> { + readonly id: AdapterId; + readonly action_types: readonly Identifier[]; + readonly consequence_classes: readonly ConsequenceClass[]; + readonly prepare_side_effect_free: boolean; + readonly rollback: boolean; + readonly interrupt: boolean; + readonly outcome_verification: boolean; +} + +export interface IntentRecord extends SealedRecord<"cint/intent/1", IntentDigest> { + readonly id: IntentId; + readonly principal_id: PrincipalId; + readonly request: string | null; + readonly action: CintAction; + readonly declared_effects: readonly string[]; + readonly context: JsonValue; + readonly uncertainties: readonly string[]; + readonly created_at: CanonicalInstant; + readonly action_digest: ActionDigest; + readonly target_digest: TargetDigest; + readonly context_digest: ContextDigest; +} + +export type PrincipalType = "HUMAN" | "SERVICE" | "AGENT"; + +export interface PrincipalRecord extends SealedRecord<"cint/principal/1"> { + readonly id: PrincipalId; + readonly type: PrincipalType; + readonly authenticated: boolean; + readonly authority_chain: readonly AuthorityId[]; + readonly attributes: JsonObject; + readonly resolved_at: CanonicalInstant; +} + +export interface AuthorityGrant { + readonly adapter: AdapterId; + readonly type: Identifier; + readonly target_digest: TargetDigest; +} + +interface AuthorityBase extends SealedRecord<"cint/authority/1"> { + readonly id: AuthorityId; + readonly principal_id: PrincipalId; + readonly issuer_id: Identifier; + readonly epoch: Epoch; + readonly grants: readonly AuthorityGrant[]; + readonly policy_ids: readonly PolicyId[]; + readonly require_rollback: boolean; + readonly issued_at: CanonicalInstant; + readonly not_before: CanonicalInstant; + readonly expires_at: CanonicalInstant; +} + +export interface ActiveAuthority extends AuthorityBase { + readonly status: "ACTIVE"; + readonly revoked_at: null; + readonly revocation_reason: null; +} + +export interface RevokedAuthority extends AuthorityBase { + readonly status: "REVOKED"; + readonly revoked_at: CanonicalInstant; + readonly revocation_reason: string; +} + +export type AuthorityRecord = ActiveAuthority | RevokedAuthority; + +export interface PolicySnapshot extends SealedRecord<"cint/policy/1"> { + readonly id: PolicyId; + readonly version: Identifier; + readonly status: "ACTIVE"; + readonly epoch: Epoch; + readonly allowed_adapters: readonly AdapterId[]; + readonly allowed_action_types: readonly Identifier[]; + readonly denied_action_types: readonly Identifier[]; + readonly require_explicit_request: boolean; + readonly require_declared_effects: boolean; + readonly require_rollback_for_consequential: boolean; + readonly review_on_uncertainty: boolean; + readonly issued_at: CanonicalInstant; +} + +export interface MachineStateSnapshot extends SealedRecord<"cint/machine-state/1"> { + readonly id: MachineStateId; + readonly epoch: Epoch; + readonly available: boolean; + readonly observed_at: CanonicalInstant; + readonly state_digest: MachineStateDigest; +} + +export interface ChallengeReason { + readonly code: Identifier; + readonly disposition: "DENY" | "REVIEW"; + readonly message: string; +} + +export interface ChallengeRecord extends SealedRecord<"cint/challenge/1"> { + readonly status: "CLEAR" | "DENIED" | "REVIEW"; + readonly checked_at: CanonicalInstant; + readonly intent_digest: IntentDigest; + readonly principal_digest: RecordDigest; + readonly authority_digest: RecordDigest; + readonly policy_digest: RecordDigest; + readonly adapter_capability_digest: RecordDigest; + readonly machine_state_digest: MachineStateDigest; + readonly reasons: readonly ChallengeReason[]; +} + +export interface DecisionBinding { + readonly intent_digest: IntentDigest; + readonly action_digest: ActionDigest; + readonly target_digest: TargetDigest; + readonly context_digest: ContextDigest; + readonly principal_digest: RecordDigest; + readonly authority_id: AuthorityId; + readonly authority_digest: RecordDigest; + readonly authority_epoch: Epoch; + readonly policy_id: PolicyId; + readonly policy_digest: RecordDigest; + readonly policy_epoch: Epoch; + readonly adapter_id: AdapterId; + readonly adapter_capability_digest: RecordDigest; + readonly machine_state_id: MachineStateId; + readonly machine_state_epoch: Epoch; + readonly machine_state_digest: MachineStateDigest; +} + +interface DecisionBase extends SealedRecord<"cint/decision/1", DecisionDigest> { + readonly id: DecisionId; + readonly issued_at: CanonicalInstant; + readonly expires_at: CanonicalInstant; + readonly binding: DecisionBinding; + readonly binding_digest: BindingDigest; + readonly challenge_digest: RecordDigest; + readonly reason_codes: readonly Identifier[]; + readonly execution_authority: "NONE"; +} + +export interface AdmitDecision extends DecisionBase { + readonly status: "ADMIT"; + readonly receipt_eligible: true; +} + +export interface DenyDecision extends DecisionBase { + readonly status: "DENY"; + readonly receipt_eligible: false; +} + +export interface ReviewDecision extends DecisionBase { + readonly status: "REVIEW"; + readonly receipt_eligible: false; +} + +export type Decision = AdmitDecision | DenyDecision | ReviewDecision; + +export interface IssuedDecisionReceipt extends SealedRecord<"cint/decision-receipt/1", ReceiptDigest> { + readonly id: ReceiptId; + readonly issuer_id: Identifier; + readonly decision_id: DecisionId; + readonly decision_digest: DecisionDigest; + readonly status: "ISSUED"; + readonly issued_at: CanonicalInstant; + readonly expires_at: CanonicalInstant; + readonly nonce: string; + readonly binding: DecisionBinding; + readonly binding_digest: BindingDigest; + readonly signature_algorithm: "HMAC-SHA256"; + readonly signature: HmacSha256Signature; +} + +export interface PendingReceiptRecord extends SealedRecord<"cint/receipt-store-entry/1"> { + readonly state: "PENDING"; + readonly receipt_id: ReceiptId; + readonly receipt_digest: ReceiptDigest; + readonly registered_at: CanonicalInstant; +} + +export interface ConsumedReceiptRecord extends SealedRecord<"cint/receipt-store-entry/1", ConsumptionDigest> { + readonly state: "CONSUMED"; + readonly receipt_id: ReceiptId; + readonly receipt_digest: ReceiptDigest; + readonly consumed_at: CanonicalInstant; + readonly revalidation_digest: RevalidationDigest; +} + +export interface RejectedReceiptRecord extends SealedRecord<"cint/receipt-store-entry/1"> { + readonly state: "REJECTED"; + readonly receipt_id: ReceiptId; + readonly receipt_digest: ReceiptDigest; + readonly rejected_at: CanonicalInstant; + readonly revalidation_digest: RevalidationDigest; + readonly reason_codes: readonly Identifier[]; +} + +export interface RevalidationCurrent { + readonly intent_digest: IntentDigest | null; + readonly action_digest: ActionDigest | null; + readonly target_digest: TargetDigest | null; + readonly context_digest: ContextDigest | null; + readonly principal_digest: RecordDigest | null; + readonly authority_digest: RecordDigest | null; + readonly authority_epoch: Epoch | null; + readonly policy_digest: RecordDigest | null; + readonly policy_epoch: Epoch | null; + readonly adapter_capability_digest: RecordDigest | null; + readonly machine_state_digest: MachineStateDigest | null; + readonly machine_state_epoch: Epoch | null; +} + +interface RevalidationBase extends SealedRecord<"cint/revalidation/1", RevalidationDigest> { + readonly receipt_id: ReceiptId; + readonly checked_at: CanonicalInstant; + readonly binding_digest: BindingDigest; + readonly current: RevalidationCurrent; + readonly reason_codes: readonly Identifier[]; +} + +export interface ValidRevalidation extends RevalidationBase { + readonly status: "VALID"; +} + +export interface RevokedRevalidation extends RevalidationBase { + readonly status: "REVOKED"; +} + +export interface RejectedRevalidation extends RevalidationBase { + readonly status: "REJECTED"; +} + +export interface FailClosedRevalidation extends RevalidationBase { + readonly status: "FAIL_CLOSED"; +} + +export type Revalidation = + | ValidRevalidation + | RevokedRevalidation + | RejectedRevalidation + | FailClosedRevalidation; + +export interface AdapterExecution extends SealedRecord< + "cint/synthetic-execution/1" | "cint/codex-delegation-execution/1" | "cint/execution-interruption/1", + ExecutionDigest +> {} + +interface OutcomeVerificationBase extends SealedRecord<"cint/outcome-verification/1", VerificationDigest> { + readonly target: string; + readonly expected_sha256: TargetDigest; + readonly actual_sha256: TargetDigest; + readonly checked_at: CanonicalInstant; +} + +export interface VerifiedOutcomeVerification extends OutcomeVerificationBase { + readonly status: "VERIFIED"; +} + +export interface DivergedOutcomeVerification extends OutcomeVerificationBase { + readonly status: "DIVERGED"; +} + +export type OutcomeVerification = VerifiedOutcomeVerification | DivergedOutcomeVerification; + +export interface RestoredRollback extends SealedRecord<"cint/rollback/1"> { + readonly status: "RESTORED"; + readonly target: string; + readonly expected_sha256: TargetDigest; + readonly actual_sha256: TargetDigest; + readonly rolled_back_at: CanonicalInstant; +} + +export interface FailedRollback extends SealedRecord<"cint/rollback/1" | "cint/rollback-failure/1"> { + readonly status: "FAILED"; +} + +export type RollbackResult = RestoredRollback | FailedRollback; + +interface OutcomeBase extends SealedRecord<"cint/outcome/1", OutcomeDigest> { + readonly receipt_id: ReceiptId; + readonly receipt_digest: ReceiptDigest; + readonly action_digest: ActionDigest; + readonly target_digest: TargetDigest; + readonly execution_digest: ExecutionDigest; + readonly verification_digest: VerificationDigest; + readonly final_state_digest: TargetDigest; + readonly completed_at: CanonicalInstant; +} + +export interface VerifiedOutcome extends OutcomeBase { + readonly status: "VERIFIED"; + readonly effect_status: "APPLIED"; + readonly rollback_digest: null; +} + +export interface RestoredOutcome extends OutcomeBase { + readonly status: "ROLLED_BACK"; + readonly effect_status: "RESTORED"; + readonly rollback_digest: RecordDigest; +} + +export type Outcome = VerifiedOutcome | RestoredOutcome; + +export interface EvidenceSeal extends SealedRecord<"cint/evidence-seal/1", EvidenceSealDigest> { + readonly id: SealId; + readonly issuer_id: Identifier; + readonly status: "SEALED"; + readonly outcome_status: Outcome["status"]; + readonly receipt_id: ReceiptId; + readonly receipt_digest: ReceiptDigest; + readonly consumption_digest: ConsumptionDigest; + readonly revalidation_digest: RevalidationDigest; + readonly outcome_digest: OutcomeDigest; + readonly ledger_head_digest: RecordDigest; + readonly issued_at: CanonicalInstant; + readonly signature_algorithm: "HMAC-SHA256"; + readonly signature: HmacSha256Signature; +} + +interface ExecutionResultBase extends SealedRecord<"cint/execution-result/1"> { + readonly receipt_id: string | null; + readonly receipt_digest: ReceiptDigest | null; + readonly consumption_digest: ConsumptionDigest | null; + readonly revalidation_digest: RevalidationDigest | null; + readonly action_started: boolean; + readonly completed_at: CanonicalInstant; +} + +export interface SealedExecutionResult extends ExecutionResultBase { + readonly status: "SEALED"; + readonly outcome: VerifiedOutcome; + readonly evidence_seal: EvidenceSeal; + readonly error_code: null; +} + +export interface RolledBackExecutionResult extends ExecutionResultBase { + readonly status: "ROLLED_BACK"; + readonly outcome: RestoredOutcome; + readonly evidence_seal: EvidenceSeal; + readonly error_code: string | null; +} + +export interface FailedExecutionResult extends ExecutionResultBase { + readonly status: "REPLAY_REJECTED" | "REJECTED" | "REVOKED" | "FAIL_CLOSED"; + readonly outcome: null; + readonly evidence_seal: null; + readonly error_code: string; +} + +export type ExecutionResult = SealedExecutionResult | RolledBackExecutionResult | FailedExecutionResult; diff --git a/src/cint/types/states.ts b/src/cint/types/states.ts new file mode 100644 index 0000000..a1cb271 --- /dev/null +++ b/src/cint/types/states.ts @@ -0,0 +1,85 @@ +export type DecisionStatus = "ADMIT" | "DENY" | "REVIEW"; +export type RevalidationStatus = "VALID" | "REVOKED" | "REJECTED" | "FAIL_CLOSED"; +export type ExecutionResultStatus = "SEALED" | "ROLLED_BACK" | "REPLAY_REJECTED" | "REJECTED" | "REVOKED" | "FAIL_CLOSED"; +export type OutcomeStatus = "VERIFIED" | "ROLLED_BACK"; +export type ReceiptStoreState = "ABSENT" | "PENDING" | "LOCKED" | "CONSUMED" | "REJECTED"; +export type AuthorityState = "ACTIVE" | "REVOKED"; +export type CintLifecycleState = + | "REQUESTED" + | "CHALLENGED" + | "DENIED" + | "REVIEW" + | "ADMITTED" + | "RECEIPT_ISSUED" + | "REVALIDATED" + | "REVOKED" + | "RECEIPT_CONSUMED" + | "EXECUTING" + | "INTERRUPTED" + | "VERIFIED" + | "ROLLED_BACK" + | "SEALED" + | "FAIL_CLOSED" + | "REPLAY_REJECTED"; + +export function assertNever(value: never): never { + throw new Error(`Unhandled CINT state: ${String(value)}`); +} + +export function decisionDisposition(status: DecisionStatus): "issue" | "terminal" { + switch (status) { + case "ADMIT": return "issue"; + case "DENY": return "terminal"; + case "REVIEW": return "terminal"; + default: return assertNever(status); + } +} + +export function revalidationDisposition(status: RevalidationStatus): "continue" | "terminal" { + switch (status) { + case "VALID": return "continue"; + case "REVOKED": return "terminal"; + case "REJECTED": return "terminal"; + case "FAIL_CLOSED": return "terminal"; + default: return assertNever(status); + } +} + +export function executionDisposition(status: ExecutionResultStatus): "sealed" | "terminal" { + switch (status) { + case "SEALED": return "sealed"; + case "ROLLED_BACK": return "sealed"; + case "REPLAY_REJECTED": return "terminal"; + case "REJECTED": return "terminal"; + case "REVOKED": return "terminal"; + case "FAIL_CLOSED": return "terminal"; + default: return assertNever(status); + } +} + +export function outcomeDisposition(status: OutcomeStatus): "verified" | "restored" { + switch (status) { + case "VERIFIED": return "verified"; + case "ROLLED_BACK": return "restored"; + default: return assertNever(status); + } +} + +export function receiptStoreDisposition(state: ReceiptStoreState): "open" | "terminal" | "missing" { + switch (state) { + case "ABSENT": return "missing"; + case "PENDING": return "open"; + case "LOCKED": return "open"; + case "CONSUMED": return "terminal"; + case "REJECTED": return "terminal"; + default: return assertNever(state); + } +} + +export function authorityDisposition(state: AuthorityState): "active" | "terminal" { + switch (state) { + case "ACTIVE": return "active"; + case "REVOKED": return "terminal"; + default: return assertNever(state); + } +} diff --git a/src/cli.d.ts b/src/cli.d.ts new file mode 100644 index 0000000..b7a991c --- /dev/null +++ b/src/cli.d.ts @@ -0,0 +1 @@ +export function main(argv?: readonly string[]): Promise; diff --git a/tests/cint-cli.test.js b/tests/cint-cli.test.ts similarity index 90% rename from tests/cint-cli.test.js rename to tests/cint-cli.test.ts index f15513b..e3f49f9 100644 --- a/tests/cint-cli.test.js +++ b/tests/cint-cli.test.ts @@ -2,11 +2,12 @@ import assert from "node:assert/strict"; import test from "node:test"; import { CINT_IDENTITY, main, renderCintHelp } from "../src/cint/cli.js"; +import { parseJsonRecord } from "./cint-test-support.js"; test("CINT CLI reports public review exposure without default-product or release authority", async () => { let output = ""; assert.equal(await main(["identity"], { write: (value) => (output += value) }), 0); - const identity = JSON.parse(output); + const identity = parseJsonRecord(output); assert.deepEqual(identity, CINT_IDENTITY); assert.equal(identity.public_display, "SI1 CINT"); assert.equal(identity.release_state, "PUBLIC_R0_SOURCE"); diff --git a/tests/cint-codex-adapter.test.js b/tests/cint-codex-adapter.test.ts similarity index 74% rename from tests/cint-codex-adapter.test.js rename to tests/cint-codex-adapter.test.ts index 3c107bd..3277dfd 100644 --- a/tests/cint-codex-adapter.test.js +++ b/tests/cint-codex-adapter.test.ts @@ -3,6 +3,7 @@ import { chmod, mkdtemp, readFile, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; +import { fileURLToPath } from "node:url"; import { DecisionReceiptAuthority, @@ -15,31 +16,43 @@ import { createPolicySnapshot, decide, executeWithReceipt, + isPlainRecord, + isoInstant, resolvePrincipal } from "../src/cint/index.js"; import { CodexDelegationCintAdapter, createCodexDelegationAction } from "../src/cint/adapters/codex/index.js"; -import { DEMO_SPEC, PROJECT_ROOT } from "../src/demo.js"; -import { loadTaskSpec } from "../src/policy.js"; +import { loadLegacyTaskSpec } from "./cint-legacy-test-boundary.js"; +import { + hasErrorCode, + parseJsonRecord, + property, + requireAdmit +} from "./cint-test-support.js"; const T0 = "2026-08-27T00:00:00.000Z"; const T1 = "2026-08-27T00:01:00.000Z"; const T2 = "2026-08-27T00:10:00.000Z"; const EXPIRY = "2026-08-27T01:00:00.000Z"; +const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const DEMO_SPEC = path.join(PROJECT_ROOT, "examples", "demo-task.json"); -async function withAdapterRuntime(run) { +async function createAdapterRuntime() { const temporary = await mkdtemp(path.join(os.tmpdir(), "cint-codex-adapter-test-")); const output = path.join(temporary, "legacy-output"); const mock = path.join(PROJECT_ROOT, "fixtures", "sanitized-af-g0", "mock-codex.mjs"); await chmod(mock, 0o755); - const { spec: loadedSpec } = await loadTaskSpec(DEMO_SPEC); + const loadedSpec = await loadLegacyTaskSpec(PROJECT_ROOT, DEMO_SPEC); const spec = { ...loadedSpec, delegation: { ...loadedSpec.delegation, max_concurrency: 2 } }; const action = await createCodexDelegationAction(spec); + assert.ok(isPlainRecord(action.target), "Codex action target must be an object"); + const packetSha256 = action.target["packet_sha256"]; + assert.equal(typeof packetSha256, "string", "Codex action target must bind packet_sha256"); const intent = createIntent({ id: "intent.codex-adapter.1", principal_id: "principal.operator", @@ -93,7 +106,7 @@ async function withAdapterRuntime(run) { id: "machine.codex-adapter", epoch: 1, available: true, - state: { packet_sha256: action.target.packet_sha256, mode: "READY" }, + state: { packet_sha256: packetSha256, mode: "READY" }, observed_at: T0 }); const snapshot = { @@ -117,7 +130,7 @@ async function withAdapterRuntime(run) { const receipt = receipt_authority.issue({ id: "receipt.codex-adapter.1", nonce: "codex-adapter-receipt-0001", - decision, + decision: requireAdmit(decision), issued_at: T1 }); const store = new FileReceiptStore(path.join(temporary, "receipt-store")); @@ -127,8 +140,8 @@ async function withAdapterRuntime(run) { key: Buffer.alloc(32, 29) }); const ledger = new ExecutionLedger(path.join(temporary, "evidence", "execution.jsonl")); - try { - return await run({ + return { + runtime: { temporary, output, mock, @@ -143,19 +156,31 @@ async function withAdapterRuntime(run) { store, seal_authority, ledger - }); + }, + cleanup: () => rm(temporary, { recursive: true, force: true }) + }; +} + +type AdapterRuntime = Awaited>["runtime"]; + +async function withAdapterRuntime( + run: (runtime: AdapterRuntime) => Promise +): Promise { + const created = await createAdapterRuntime(); + try { + return await run(created.runtime); } finally { - await rm(temporary, { recursive: true, force: true }); + await created.cleanup(); } } test("Codex Adapter 01 exposes no CINT authority operation", async () => { await withAdapterRuntime(async ({ adapter }) => { assert.equal(adapter.capability.consequence_classes[0], "READ_ONLY"); - assert.equal(adapter.decide, undefined); - assert.equal(adapter.issue, undefined); - assert.equal(adapter.consume, undefined); - assert.equal(adapter.seal, undefined); + assert.equal(property(adapter, "decide"), undefined); + assert.equal(property(adapter, "issue"), undefined); + assert.equal(property(adapter, "consume"), undefined); + assert.equal(property(adapter, "seal"), undefined); assert(Object.values(adapter.authority_boundary).every((value) => value === false)); }); }); @@ -177,9 +202,9 @@ test("legacy bounded review runs only under a consumed CINT receipt", async () = assert.equal(result.outcome.status, "VERIFIED"); assert.equal(result.outcome.effect_status, "APPLIED"); runtime.seal_authority.verify(result.evidence_seal); - const legacy = JSON.parse(await readFile(path.join(runtime.output, "run.json"), "utf8")); - assert.equal(legacy.admission.result, "ADMITTED"); - assert.equal(legacy.context_enforcement.inherited_turns, 0); + const legacy = parseJsonRecord(await readFile(path.join(runtime.output, "run.json"), "utf8")); + assert.equal(property(legacy["admission"], "result"), "ADMITTED"); + assert.equal(property(legacy["context_enforcement"], "inherited_turns"), 0); assert.equal((await runtime.store.inspect(runtime.receipt.id)).state, "CONSUMED"); }); }); @@ -193,8 +218,8 @@ test("legacy packet drift is rejected before Codex execution", async () => { codex_binary: runtime.mock }); await assert.rejects( - changedAdapter.prepare(runtime.intent, { at: T1 }), - (error) => error.code === "CINT_CODEX_PACKET_CHANGED" + changedAdapter.prepare(runtime.intent, { at: isoInstant(T1) }), + (error: unknown) => hasErrorCode(error, "CINT_CODEX_PACKET_CHANGED") ); }); }); diff --git a/tests/cint-decision.test.js b/tests/cint-decision.test.ts similarity index 81% rename from tests/cint-decision.test.js rename to tests/cint-decision.test.ts index 9351665..9028a73 100644 --- a/tests/cint-decision.test.js +++ b/tests/cint-decision.test.ts @@ -3,6 +3,7 @@ import test from "node:test"; import { canonicalDigest, + canonicalJson, createAdapterCapability, createAuthorityGrant, createIntent, @@ -15,7 +16,7 @@ import { sealRecord, transitionState } from "../src/cint/index.js"; -import { canonicalJson } from "../src/util.js"; +import { hasErrorCode, hasReason } from "./cint-test-support.js"; const T0 = "2026-08-27T00:00:00.000Z"; const T1 = "2026-08-27T00:01:00.000Z"; @@ -23,7 +24,24 @@ const T2 = "2026-08-27T00:10:00.000Z"; const EXPIRY = "2026-08-27T01:00:00.000Z"; const TARGET = Object.freeze({ path: "sandbox/target.txt" }); -function records(overrides = {}) { +type RecordPatch = Readonly>; + +interface RecordOverrides { + readonly intent?: RecordPatch; + readonly principal?: RecordPatch; + readonly authority?: RecordPatch; + readonly policy?: RecordPatch; + readonly adapter_capability?: RecordPatch; + readonly machine_state?: RecordPatch; +} + +interface DecisionOverrides { + readonly id?: unknown; + readonly now?: unknown; + readonly expires_at?: unknown; +} + +function records(overrides: RecordOverrides = {}) { const intent = createIntent({ id: "intent.demo.1", principal_id: "principal.operator", @@ -104,7 +122,7 @@ function records(overrides = {}) { return { intent, principal, authority, policy, adapter_capability, machine_state }; } -function decideRecords(values, overrides = {}) { +function decideRecords(values: object, overrides: DecisionOverrides = {}) { return decide({ id: overrides.id ?? "decision.demo.1", ...values, @@ -113,8 +131,8 @@ function decideRecords(values, overrides = {}) { }); } -function rehashRecord(record, overrides) { - const { digest, ...unsigned } = record; +function rehashRecord(record: object, overrides: RecordPatch) { + const unsigned = Object.fromEntries(Object.entries(record).filter(([key]) => key !== "digest")); const changed = { ...unsigned, ...overrides }; return { ...changed, digest: canonicalDigest(changed) }; } @@ -124,14 +142,14 @@ test("canonical parser admits only the canonical byte representation", () => { assert.deepEqual(parseCanonicalJson(canonicalJson(value)), { a: [true, null], z: 1 }); assert.throws( () => parseCanonicalJson(JSON.stringify(value, null, 2)), - (error) => error.code === "CINT_JSON_NOT_CANONICAL" + (error: unknown) => hasErrorCode(error, "CINT_JSON_NOT_CANONICAL") ); }); test("strict intent construction rejects unknown fields", () => { assert.throws( () => records({ intent: { undeclared_field: true } }), - (error) => error.code === "CINT_UNKNOWN_FIELD" + (error: unknown) => hasErrorCode(error, "CINT_UNKNOWN_FIELD") ); }); @@ -146,13 +164,13 @@ test("exact current bindings produce ADMIT without executable authority", () => test("silent request is denied", () => { const decision = decideRecords(records({ intent: { request: null } })); assert.equal(decision.status, "DENY"); - assert(decision.reason_codes.includes("CINT_SILENT_REQUEST")); + assert(hasReason(decision, "CINT_SILENT_REQUEST")); }); test("undeclared effect is denied", () => { const decision = decideRecords(records({ intent: { declared_effects: [] } })); assert.equal(decision.status, "DENY"); - assert(decision.reason_codes.includes("CINT_EFFECT_UNDECLARED")); + assert(hasReason(decision, "CINT_EFFECT_UNDECLARED")); }); test("target outside the exact authority binding is denied", () => { @@ -169,26 +187,26 @@ test("target outside the exact authority binding is denied", () => { }); const decision = decideRecords(values); assert.equal(decision.status, "DENY"); - assert(decision.reason_codes.includes("CINT_AUTHORITY_ACTION_DENIED")); + assert(hasReason(decision, "CINT_AUTHORITY_ACTION_DENIED")); }); test("unresolved counter-intent requires REVIEW", () => { const decision = decideRecords(records({ intent: { uncertainties: ["Target ownership needs confirmation"] } })); assert.equal(decision.status, "REVIEW"); assert.equal(decision.receipt_eligible, false); - assert(decision.reason_codes.includes("CINT_COUNTER_INTENT_UNRESOLVED")); + assert(hasReason(decision, "CINT_COUNTER_INTENT_UNRESOLVED")); }); test("unavailable CINT machine state fails closed", () => { const decision = decideRecords(records({ machine_state: { available: false } })); assert.equal(decision.status, "DENY"); - assert(decision.reason_codes.includes("CINT_UNAVAILABLE")); + assert(hasReason(decision, "CINT_UNAVAILABLE")); }); test("consequential action without rollback capability is denied", () => { const decision = decideRecords(records({ adapter_capability: { rollback: false } })); assert.equal(decision.status, "DENY"); - assert(decision.reason_codes.includes("CINT_ROLLBACK_REQUIRED")); + assert(hasReason(decision, "CINT_ROLLBACK_REQUIRED")); }); test("tampered sealed input cannot reach a decision", () => { @@ -196,7 +214,7 @@ test("tampered sealed input cannot reach a decision", () => { const tamperedIntent = { ...values.intent, request: "Different request" }; assert.throws( () => decideRecords({ ...values, intent: tamperedIntent }), - (error) => error.code === "CINT_RECORD_TAMPERED" + (error: unknown) => hasErrorCode(error, "CINT_RECORD_TAMPERED") ); }); @@ -210,7 +228,7 @@ test("rehashed authority with an alien protocol cannot reach ADMIT", () => { }); assert.throws( () => decideRecords({ ...current, authority: forged }, { id: "decision.schema-invalid-authority" }), - (error) => error.code === "CINT_PROTOCOL_INVALID" + (error: unknown) => hasErrorCode(error, "CINT_PROTOCOL_INVALID") ); }); @@ -219,17 +237,17 @@ test("adapter capability and machine state require exact protocols and fields", for (const [field, alienProtocol] of [ ["adapter_capability", "not-cint/adapter-capability/999"], ["machine_state", "not-cint/machine-state/999"] - ]) { + ] as const) { const alien = rehashRecord(current[field], { protocol: alienProtocol, forbidden_field: true }); assert.throws( () => decideRecords({ ...current, [field]: alien }, { id: `decision.alien.${field}` }), - (error) => error.code === "CINT_PROTOCOL_INVALID", + (error: unknown) => hasErrorCode(error, "CINT_PROTOCOL_INVALID"), field ); const extraField = rehashRecord(current[field], { forbidden_field: true }); assert.throws( () => decideRecords({ ...current, [field]: extraField }, { id: `decision.extra.${field}` }), - (error) => error.code === "CINT_SCHEMA_INVALID", + (error: unknown) => hasErrorCode(error, "CINT_SCHEMA_INVALID"), field ); } @@ -238,7 +256,7 @@ test("adapter capability and machine state require exact protocols and fields", test("decision cannot outlive its authority", () => { assert.throws( () => decideRecords(records(), { expires_at: "2026-08-27T02:00:00.000Z" }), - (error) => error.code === "CINT_DECISION_TIME" + (error: unknown) => hasErrorCode(error, "CINT_DECISION_TIME") ); }); @@ -249,6 +267,6 @@ test("state machine permits only declared transitions", () => { assert.equal(admitted.state, "ADMITTED"); assert.throws( () => transitionState(admitted, { state: "EXECUTING", at: T2, evidence_digest: null }), - (error) => error.code === "CINT_STATE_TRANSITION" + (error: unknown) => hasErrorCode(error, "CINT_STATE_TRANSITION") ); }); diff --git a/tests/cint-execution.test.js b/tests/cint-execution.test.ts similarity index 82% rename from tests/cint-execution.test.js rename to tests/cint-execution.test.ts index 4ac6545..ef404ee 100644 --- a/tests/cint-execution.test.js +++ b/tests/cint-execution.test.ts @@ -24,11 +24,21 @@ import { revokeAuthority, runCounterIntentChallenge, sealRecord, + sha256, validateCintSchema, verifyProtocolRecord } from "../src/cint/index.js"; import { SyntheticFilePatchAdapter } from "../src/cint/adapters/synthetic-file-patch.js"; -import { sha256 } from "../src/util.js"; +import type { + ExecutionAdapter, + ExecutionSnapshot +} from "../src/cint/execution.js"; +import { + executeUntrusted, + hasErrorCode, + issueUntrusted, + requireAdmit +} from "./cint-test-support.js"; const T0 = "2026-08-27T00:00:00.000Z"; const T1 = "2026-08-27T00:01:00.000Z"; @@ -36,41 +46,93 @@ const T2_BEFORE = "2026-08-27T00:09:59.999Z"; const T2 = "2026-08-27T00:10:00.000Z"; const EXPIRY = "2026-08-27T01:00:00.000Z"; -function createTestClock(initial = T1) { +function createTestClock(initial: string = T1) { let current = initial; return Object.freeze({ now: () => current, - set: (next) => { + set: (next: string) => { current = next; } }); } -function adapterAdvancingClock(runtime, next, counters = { execute_calls: 0 }) { +function adapterAdvancingClock( + runtime: ExecutionRuntime, + next: string, + counters: { execute_calls: number } = { execute_calls: 0 } +): ExecutionAdapter { + const adapter: ExecutionAdapter = runtime.adapter; return { - id: runtime.adapter.id, - capability: runtime.adapter.capability, - prepare: async (...args) => { - const prepared = await runtime.adapter.prepare(...args); + id: adapter.id, + capability: adapter.capability, + prepare: async (intent, options) => { + const prepared = await adapter.prepare(intent, options); runtime.clock.set(next); return prepared; }, - execute: async (...args) => { + execute: async (prepared, options) => { counters.execute_calls += 1; - return runtime.adapter.execute(...args); + return adapter.execute(prepared, options); + }, + verify: (prepared, execution, options) => adapter.verify(prepared, execution, options), + rollback: async (prepared, options) => { + if (adapter.rollback === undefined) throw new Error("test adapter must support rollback"); + return adapter.rollback(prepared, options); + } + }; +} + +interface AdapterHooks { + readonly capability?: ExecutionAdapter["capability"]; + readonly afterPrepare?: () => void | Promise; + readonly beforeExecute?: () => void | Promise; +} + +function adapterWithHooks(runtime: ExecutionRuntime, hooks: AdapterHooks = {}): ExecutionAdapter { + const adapter: ExecutionAdapter = runtime.adapter; + return { + id: adapter.id, + capability: hooks.capability ?? adapter.capability, + prepare: async (intent, options) => { + const prepared = await adapter.prepare(intent, options); + await hooks.afterPrepare?.(); + return prepared; + }, + execute: async (prepared, options) => { + await hooks.beforeExecute?.(); + return adapter.execute(prepared, options); }, - verify: runtime.adapter.verify.bind(runtime.adapter), - rollback: runtime.adapter.rollback.bind(runtime.adapter) + verify: (prepared, execution, options) => adapter.verify(prepared, execution, options), + rollback: async (prepared, options) => { + if (adapter.rollback === undefined) throw new Error("test adapter must support rollback"); + return adapter.rollback(prepared, options); + } }; } -function rehashRecord(record, overrides) { - const { digest, ...unsigned } = record; +function rehashRecord(record: object, overrides: Readonly>) { + const unsigned = Object.fromEntries(Object.entries(record).filter(([key]) => key !== "digest")); const changed = { ...unsigned, ...overrides }; return { ...changed, digest: canonicalDigest(changed) }; } -async function withRuntime(run) { +interface IntentOverrides { + readonly id?: unknown; + readonly request?: unknown; + readonly action?: unknown; + readonly content?: unknown; + readonly declared_effects?: unknown; + readonly context?: unknown; + readonly uncertainties?: unknown; +} + +interface PolicyOverrides { + readonly version?: unknown; + readonly epoch?: unknown; + readonly issued_at?: unknown; +} + +async function createRuntime() { const root = await mkdtemp(path.join(os.tmpdir(), "cint-execution-test-")); const target = path.join(root, "target.txt"); const initial = Buffer.from("before\n", "utf8"); @@ -78,7 +140,7 @@ async function withRuntime(run) { const targetObject = Object.freeze({ path: "target.txt" }); const beforeSha256 = sha256(initial); - const makeIntent = (overrides = {}) => + const makeIntent = (overrides: IntentOverrides = {}) => createIntent({ id: overrides.id ?? "intent.execution.1", principal_id: "principal.operator", @@ -123,7 +185,7 @@ async function withRuntime(run) { not_before: T0, expires_at: EXPIRY }); - const makePolicy = (overrides = {}) => + const makePolicy = (overrides: PolicyOverrides = {}) => createPolicySnapshot({ id: "policy.execution", version: overrides.version ?? "r0.1", @@ -168,7 +230,7 @@ async function withRuntime(run) { const receipt = receipt_authority.issue({ id: "receipt.execution.1", nonce: "execution-receipt-nonce-0001", - decision, + decision: requireAdmit(decision), issued_at: T1 }); const store = new FileReceiptStore(path.join(root, "receipt-store")); @@ -201,8 +263,8 @@ async function withRuntime(run) { seal_authority, ledger, clock, - execute: (overrides = {}) => - executeWithReceipt({ + execute: (overrides: Readonly> = {}) => + executeUntrusted({ receipt, receipt_authority, store, @@ -215,10 +277,22 @@ async function withRuntime(run) { ...overrides }) }; + return { + runtime, + cleanup: () => rm(root, { recursive: true, force: true }) + }; +} + +type ExecutionRuntime = Awaited>["runtime"]; + +async function withRuntime( + run: (runtime: ExecutionRuntime) => Promise +): Promise { + const created = await createRuntime(); try { - return await run(runtime); + return await run(created.runtime); } finally { - await rm(root, { recursive: true, force: true }); + await created.cleanup(); } } @@ -231,7 +305,9 @@ test("bounded synthetic mutation is admitted, verified, and sealed", async () => assert.equal(await readFile(runtime.target, "utf8"), "after\n"); assert.equal(runtime.seal_authority.verify(result.evidence_seal), result.evidence_seal); assert.equal((await runtime.store.inspect(runtime.receipt.id)).state, "CONSUMED"); - assert((await runtime.ledger.head()).sequence >= 5); + const ledgerHead = await runtime.ledger.head(); + assert.ok(ledgerHead, "sealed execution must have a ledger head"); + assert(ledgerHead.sequence >= 5); }); }); @@ -281,8 +357,8 @@ test("silent, out-of-authority, and rollback-free actions cannot obtain a receip assert.equal(rollbackFree.status, "DENY"); for (const decision of [silent, outside, rollbackFree]) { assert.throws( - () => runtime.receipt_authority.issue({ decision, issued_at: T1 }), - (error) => error.code === "CINT_RECEIPT_DECISION_INELIGIBLE" + () => issueUntrusted(runtime.receipt_authority, { decision, issued_at: T1 }), + (error: unknown) => hasErrorCode(error, "CINT_RECEIPT_DECISION_INELIGIBLE") ); } }); @@ -422,17 +498,12 @@ test("runtime adapter capability mismatch is rejected before preparation", async interrupt: false, outcome_verification: true }); - const adapter = { - id: runtime.adapter.id, + const adapter = adapterWithHooks(runtime, { capability, - prepare: async (...args) => { + afterPrepare: () => { prepareCalls += 1; - return runtime.adapter.prepare(...args); - }, - execute: runtime.adapter.execute.bind(runtime.adapter), - verify: runtime.adapter.verify.bind(runtime.adapter), - rollback: runtime.adapter.rollback.bind(runtime.adapter) - }; + } + }); const result = await runtime.execute({ adapter }); assert.equal(result.status, "FAIL_CLOSED"); assert.equal(result.error_code, "CINT_ADAPTER_MISMATCH"); @@ -470,7 +541,7 @@ test("revalidation rejects alien adapter capability and machine state protocols" for (const [field, protocol] of [ ["adapter_capability", "not-cint/adapter-capability/999"], ["machine_state", "not-cint/machine-state/999"] - ]) { + ] as const) { const record = rehashRecord(runtime.snapshot[field], { protocol, forbidden_field: true }); const revalidation = revalidateReceipt({ receipt: runtime.receipt, @@ -480,7 +551,7 @@ test("revalidation rejects alien adapter capability and machine state protocols" now: T1 }); assert.equal(revalidation.status, "REJECTED", field); - assert(revalidation.reason_codes.includes("CINT_PROTOCOL_INVALID"), field); + assert(revalidation.reason_codes.some((reason) => reason === "CINT_PROTOCOL_INVALID"), field); } }); }); @@ -503,27 +574,20 @@ test("alien machine state fails execution admission before action", async () => test("policy drift introduced during preparation is revalidated before execution", async () => { await withRuntime(async (runtime) => { - let activeSnapshot = runtime.snapshot; + let activeSnapshot: ExecutionSnapshot = runtime.snapshot; let snapshotCalls = 0; let executeCalls = 0; - const adapter = { - id: runtime.adapter.id, - capability: runtime.adapter.capability, - prepare: async (...args) => { - const prepared = await runtime.adapter.prepare(...args); + const adapter = adapterWithHooks(runtime, { + afterPrepare: () => { activeSnapshot = { ...runtime.snapshot, policy: runtime.makePolicy({ version: "r0.2", epoch: 2, issued_at: T1 }) }; - return prepared; }, - execute: async (...args) => { + beforeExecute: () => { executeCalls += 1; - return runtime.adapter.execute(...args); - }, - verify: runtime.adapter.verify.bind(runtime.adapter), - rollback: runtime.adapter.rollback.bind(runtime.adapter) - }; + } + }); const result = await runtime.execute({ adapter, snapshot_provider: async () => { @@ -541,13 +605,10 @@ test("policy drift introduced during preparation is revalidated before execution test("authority revocation introduced during preparation is revalidated before execution", async () => { await withRuntime(async (runtime) => { - let activeSnapshot = runtime.snapshot; + let activeSnapshot: ExecutionSnapshot = runtime.snapshot; let executeCalls = 0; - const adapter = { - id: runtime.adapter.id, - capability: runtime.adapter.capability, - prepare: async (...args) => { - const prepared = await runtime.adapter.prepare(...args); + const adapter = adapterWithHooks(runtime, { + afterPrepare: () => { activeSnapshot = { ...runtime.snapshot, authority: revokeAuthority(runtime.authority, { @@ -555,15 +616,11 @@ test("authority revocation introduced during preparation is revalidated before e reason: "Execution authority withdrawn during preparation" }) }; - return prepared; }, - execute: async (...args) => { + beforeExecute: () => { executeCalls += 1; - return runtime.adapter.execute(...args); - }, - verify: runtime.adapter.verify.bind(runtime.adapter), - rollback: runtime.adapter.rollback.bind(runtime.adapter) - }; + } + }); const result = await runtime.execute({ adapter, snapshot_provider: async () => activeSnapshot }); assert.equal(result.status, "REVOKED"); assert.equal(result.action_started, false); @@ -581,6 +638,8 @@ test("receipt expiry crossed during preparation prevents every action", async () assert.equal(result.status, "REVOKED"); assert.equal(result.error_code, "CINT_RECEIPT_EXPIRED"); assert.equal(result.completed_at, T2); + assert.notEqual(result.revalidation_digest, null); + if (result.revalidation_digest === null) throw new Error("revoked execution must bind revalidation"); assert.match(result.revalidation_digest, /^[a-f0-9]{64}$/); assert.equal(result.action_started, false); assert.equal(counters.execute_calls, 0); @@ -598,8 +657,8 @@ test("authority expiry crossed during preparation is evaluated at the final trus ...runtime.snapshot, now: EXPIRY }); - assert(finalRevalidation.reason_codes.includes("CINT_RECEIPT_EXPIRED")); - assert(finalRevalidation.reason_codes.includes("CINT_AUTHORITY_EXPIRED")); + assert(finalRevalidation.reason_codes.some((reason) => reason === "CINT_RECEIPT_EXPIRED")); + assert(finalRevalidation.reason_codes.some((reason) => reason === "CINT_AUTHORITY_EXPIRED")); const result = await runtime.execute({ adapter: adapterAdvancingClock(runtime, EXPIRY, counters) }); @@ -639,17 +698,11 @@ test("stale caller time cannot override an expired final trusted time", async () await withRuntime(async (runtime) => { runtime.clock.set(T2); let executeCalls = 0; - const adapter = { - id: runtime.adapter.id, - capability: runtime.adapter.capability, - prepare: runtime.adapter.prepare.bind(runtime.adapter), - execute: async (...args) => { + const adapter = adapterWithHooks(runtime, { + beforeExecute: () => { executeCalls += 1; - return runtime.adapter.execute(...args); - }, - verify: runtime.adapter.verify.bind(runtime.adapter), - rollback: runtime.adapter.rollback.bind(runtime.adapter) - }; + } + }); const result = await runtime.execute({ adapter, at: T1 }); assert.equal(result.status, "REVOKED"); assert.equal(result.error_code, "CINT_RECEIPT_EXPIRED"); @@ -662,17 +715,11 @@ test("stale caller time cannot override an expired final trusted time", async () test("trusted clock failure after preparation fails closed without action", async () => { await withRuntime(async (runtime) => { let executeCalls = 0; - const adapter = { - id: runtime.adapter.id, - capability: runtime.adapter.capability, - prepare: runtime.adapter.prepare.bind(runtime.adapter), - execute: async (...args) => { + const adapter = adapterWithHooks(runtime, { + beforeExecute: () => { executeCalls += 1; - return runtime.adapter.execute(...args); - }, - verify: runtime.adapter.verify.bind(runtime.adapter), - rollback: runtime.adapter.rollback.bind(runtime.adapter) - }; + } + }); const result = await runtime.execute({ adapter, clock: { now: () => { throw new Error("clock unavailable"); } } @@ -695,6 +742,8 @@ test("all thirteen public protocol schemas execute on valid and invalid runtime now: T1 }); const result = await runtime.execute(); + assert.equal(result.status, "SEALED"); + if (result.status !== "SEALED") throw new Error("schema fixture execution must seal"); const records = [ runtime.adapter.capability, runtime.authority, @@ -720,7 +769,7 @@ test("all thirteen public protocol schemas execute on valid and invalid runtime const { digest, ...unsigned } = record; assert.throws( () => sealRecord({ ...unsigned, schema_forbidden_field: true }), - (error) => error.code === "CINT_SCHEMA_INVALID", + (error: unknown) => hasErrorCode(error, "CINT_SCHEMA_INVALID"), record.protocol ); const invalidUnsigned = { ...unsigned, schema_forbidden_field: true }; @@ -728,7 +777,7 @@ test("all thirteen public protocol schemas execute on valid and invalid runtime assert.equal(validateCintSchema(forgedInput).valid, false, record.protocol); assert.throws( () => verifyProtocolRecord(forgedInput, record.protocol, "forged input"), - (error) => error.code === "CINT_SCHEMA_INVALID", + (error: unknown) => hasErrorCode(error, "CINT_SCHEMA_INVALID"), record.protocol ); } diff --git a/tests/cint-import-boundary.test.js b/tests/cint-import-boundary.test.ts similarity index 81% rename from tests/cint-import-boundary.test.js rename to tests/cint-import-boundary.test.ts index 22a22cc..bf486a2 100644 --- a/tests/cint-import-boundary.test.js +++ b/tests/cint-import-boundary.test.ts @@ -5,13 +5,15 @@ import { promisify } from "node:util"; import { fileURLToPath, pathToFileURL } from "node:url"; import test from "node:test"; +import { errorTextIncludes, parseJsonRecord } from "./cint-test-support.js"; + const executeFile = promisify(execFile); const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const LOADER = pathToFileURL( path.join(PROJECT_ROOT, "tests", "fixtures", "deny-cint-adapter-loader.mjs") ).href; -async function runDeniedImportProbe(arguments_) { +async function runDeniedImportProbe(arguments_: readonly string[]) { return executeFile(process.execPath, ["--no-warnings", "--experimental-loader", LOADER, ...arguments_], { cwd: PROJECT_ROOT }); @@ -24,12 +26,12 @@ test("the supported CINT core entrypoint loads without Adapter 01", async () => test("the identity CLI loads without Adapter 01 or the legacy CLI", async () => { const { stdout } = await runDeniedImportProbe([path.join(PROJECT_ROOT, "bin", "cint.js"), "identity"]); - assert.equal(JSON.parse(stdout).product_code, "CINT"); + assert.equal(parseJsonRecord(stdout)["product_code"], "CINT"); }); test("the legacy CLI crosses the adapter boundary only when explicitly invoked", async () => { await assert.rejects( runDeniedImportProbe([path.join(PROJECT_ROOT, "bin", "cint.js"), "legacy", "doctor"]), - (error) => error.stderr.includes("CINT_IMPORT_BOUNDARY") + (error: unknown) => errorTextIncludes(error, "stderr", "CINT_IMPORT_BOUNDARY") ); }); diff --git a/tests/cint-legacy-test-boundary.ts b/tests/cint-legacy-test-boundary.ts new file mode 100644 index 0000000..9ba1e06 --- /dev/null +++ b/tests/cint-legacy-test-boundary.ts @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +import { isPlainRecord } from "../src/cint/index.js"; + +export interface LegacyTaskSpec { + readonly root_task: string; + readonly delegation: Readonly>; + readonly [key: string]: unknown; +} + +export async function loadLegacyTaskSpec( + projectRoot: string, + specPath: string +): Promise { + const policyUrl = pathToFileURL( + path.join(projectRoot, "src", "adapters", "codex-delegation", "policy.js") + ).href; + const legacyModule: unknown = await import(policyUrl); + assert.ok(isPlainRecord(legacyModule), "legacy policy module must expose named exports"); + const loader = legacyModule["loadTaskSpec"]; + if (typeof loader !== "function") throw new Error("legacy policy module must expose loadTaskSpec"); + const loaded: unknown = await Reflect.apply(loader, undefined, [specPath]); + assert.ok(isPlainRecord(loaded), "legacy task loader must return an object"); + const spec = loaded["spec"]; + assert.ok(isPlainRecord(spec), "legacy task loader must return a validated spec"); + const rootTask = spec["root_task"]; + const delegation = spec["delegation"]; + if (typeof rootTask !== "string") throw new Error("legacy spec root_task must be a string"); + assert.ok(isPlainRecord(delegation), "legacy spec delegation must be an object"); + return Object.freeze({ + ...spec, + root_task: rootTask, + delegation: Object.freeze({ ...delegation }) + }); +} diff --git a/tests/cint-receipt.test.js b/tests/cint-receipt.test.ts similarity index 82% rename from tests/cint-receipt.test.js rename to tests/cint-receipt.test.ts index 2bb7e5a..662394f 100644 --- a/tests/cint-receipt.test.js +++ b/tests/cint-receipt.test.ts @@ -14,9 +14,15 @@ import { createMachineStateSnapshot, createPolicySnapshot, decide, - resolvePrincipal + resolvePrincipal, + sha256 } from "../src/cint/index.js"; -import { sha256 } from "../src/util.js"; +import type { AdmitDecision } from "../src/cint/types/records.js"; +import { + hasErrorCode, + issueUntrusted, + requireAdmit +} from "./cint-test-support.js"; const T0 = "2026-08-27T00:00:00.000Z"; const T1 = "2026-08-27T00:01:00.000Z"; @@ -25,7 +31,12 @@ const T3 = "2026-08-27T00:11:00.000Z"; const EXPIRY = "2026-08-27T01:00:00.000Z"; const TARGET = Object.freeze({ path: "sandbox/target.txt" }); -function decisionFor(overrides = {}) { +interface DecisionFixtureOverrides { + readonly intent?: Readonly>; + readonly id?: unknown; +} + +function decisionFor(overrides: DecisionFixtureOverrides = {}) { const intent = createIntent({ id: "intent.receipt.1", principal_id: "principal.operator", @@ -118,7 +129,7 @@ function receiptAuthority() { }); } -function receiptFor(decision = decisionFor()) { +function receiptFor(decision: AdmitDecision = requireAdmit(decisionFor())) { return receiptAuthority().issue({ id: "receipt.demo.1", nonce: "receipt-nonce-0000000001", @@ -127,13 +138,15 @@ function receiptFor(decision = decisionFor()) { }); } -function reforge(record, changes) { - const unsigned = { ...record, ...changes }; - delete unsigned.digest; +function reforge(record: object, changes: Readonly>) { + const unsigned = { + ...Object.fromEntries(Object.entries(record).filter(([key]) => key !== "digest")), + ...changes + }; return { ...unsigned, digest: canonicalDigest(unsigned) }; } -async function withStore(run) { +async function withStore(run: (store: FileReceiptStore) => Promise): Promise { const root = await mkdtemp(path.join(os.tmpdir(), "cint-receipt-test-")); try { return await run(new FileReceiptStore(root)); @@ -152,8 +165,8 @@ test("only an ADMIT decision can produce a receipt", () => { const denied = decisionFor({ intent: { request: null }, id: "decision.receipt.denied" }); assert.equal(denied.status, "DENY"); assert.throws( - () => receiptAuthority().issue({ decision: denied, issued_at: T1 }), - (error) => error.code === "CINT_RECEIPT_DECISION_INELIGIBLE" + () => issueUntrusted(receiptAuthority(), { decision: denied, issued_at: T1 }), + (error: unknown) => hasErrorCode(error, "CINT_RECEIPT_DECISION_INELIGIBLE") ); }); @@ -176,14 +189,14 @@ test("rehashed binding forgery fails the receipt signature", () => { }); assert.throws( () => authority.verify(forged, { now: T1 }), - (error) => error.code === "CINT_RECEIPT_SIGNATURE_INVALID" + (error: unknown) => hasErrorCode(error, "CINT_RECEIPT_SIGNATURE_INVALID") ); }); test("expired receipt verification is rejected", () => { assert.throws( () => receiptAuthority().verify(receiptFor(), { now: T2 }), - (error) => error.code === "CINT_RECEIPT_EXPIRED" + (error: unknown) => hasErrorCode(error, "CINT_RECEIPT_EXPIRED") ); }); @@ -196,7 +209,7 @@ test("registered receipt is consumed exactly once", async () => { assert.equal((await store.inspect(receipt.id)).state, "CONSUMED"); await assert.rejects( store.consume(receipt, { consumed_at: T1, revalidate: validRevalidation }), - (error) => error.code === "CINT_RECEIPT_REPLAY_REJECTED" + (error: unknown) => hasErrorCode(error, "CINT_RECEIPT_REPLAY_REJECTED") ); }); }); @@ -211,7 +224,9 @@ test("parallel replay produces one atomic consumer", async () => { ]); assert.equal(attempts.filter((attempt) => attempt.status === "fulfilled").length, 1); assert.equal(attempts.filter((attempt) => attempt.status === "rejected").length, 1); - assert.equal(attempts.find((attempt) => attempt.status === "rejected").reason.code, "CINT_RECEIPT_REPLAY_REJECTED"); + const rejected = attempts.find((attempt): attempt is PromiseRejectedResult => attempt.status === "rejected"); + assert.ok(rejected, "one replay attempt must reject"); + assert.ok(hasErrorCode(rejected.reason, "CINT_RECEIPT_REPLAY_REJECTED")); }); }); @@ -222,12 +237,12 @@ test("presented receipt must match the registered digest", async () => { const other = receiptAuthority().issue({ id: receipt.id, nonce: "receipt-nonce-0000000002", - decision: decisionFor(), + decision: requireAdmit(decisionFor()), issued_at: T1 }); await assert.rejects( store.consume(other, { consumed_at: T1, revalidate: validRevalidation }), - (error) => error.code === "CINT_RECEIPT_STORE_MISMATCH" + (error: unknown) => hasErrorCode(error, "CINT_RECEIPT_STORE_MISMATCH") ); }); }); @@ -245,7 +260,7 @@ test("failed immediate revalidation terminally rejects the receipt", async () => digest: "b".repeat(64) }) }), - (error) => error.code === "CINT_RECEIPT_REVOKED" + (error: unknown) => hasErrorCode(error, "CINT_RECEIPT_REVOKED") ); assert.equal((await store.inspect(receipt.id)).state, "REJECTED"); }); @@ -257,7 +272,7 @@ test("receipt expiry at consumption is terminal and fail closed", async () => { await store.register(receipt, { registered_at: T1 }); await assert.rejects( store.consume(receipt, { consumed_at: T3, revalidate: validRevalidation }), - (error) => error.code === "CINT_RECEIPT_REVOKED" + (error: unknown) => hasErrorCode(error, "CINT_RECEIPT_REVOKED") ); assert.equal((await store.inspect(receipt.id)).state, "REJECTED"); }); @@ -271,7 +286,7 @@ test("ambiguous crash lock remains fail closed", async () => { await writeFile(lock, "crash-residue", "utf8"); await assert.rejects( store.consume(receipt, { consumed_at: T1, revalidate: validRevalidation }), - (error) => error.code === "CINT_RECEIPT_REPLAY_REJECTED" + (error: unknown) => hasErrorCode(error, "CINT_RECEIPT_REPLAY_REJECTED") ); assert.equal((await store.inspect(receipt.id)).state, "LOCKED"); }); diff --git a/tests/cint-test-support.ts b/tests/cint-test-support.ts new file mode 100644 index 0000000..e8e8f0d --- /dev/null +++ b/tests/cint-test-support.ts @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; + +import { + executeWithReceipt, + isPlainRecord, + verifyProtocolRecord +} from "../src/cint/index.js"; +import type { DecisionReceiptAuthority } from "../src/cint/receipt.js"; +import type { + AdmitDecision, + Decision, + ExecutionResult, + IssuedDecisionReceipt +} from "../src/cint/types/records.js"; +import type { JsonRecord } from "../src/cint/canonical.js"; + +export function property(value: unknown, name: string): unknown { + return value !== null && (typeof value === "object" || typeof value === "function") + ? Reflect.get(value, name) + : undefined; +} + +export function hasErrorCode(error: unknown, code: string): boolean { + return property(error, "code") === code; +} + +export function errorTextIncludes(error: unknown, name: string, expected: string): boolean { + const value = property(error, name); + return typeof value === "string" && value.includes(expected); +} + +export function parseJsonRecord(text: string): JsonRecord { + const value: unknown = JSON.parse(text); + assert.ok(isPlainRecord(value), "expected parsed JSON object"); + return value; +} + +export function requireAdmit(decision: Decision): AdmitDecision { + assert.equal(decision.status, "ADMIT", "test fixture must produce ADMIT"); + if (decision.status !== "ADMIT") throw new Error("unreachable non-ADMIT fixture"); + return decision; +} + +export function hasReason(decision: Decision, reason: string): boolean { + return decision.reason_codes.some((candidate) => candidate === reason); +} + +export function issueUntrusted( + authority: DecisionReceiptAuthority, + input: unknown +): IssuedDecisionReceipt { + const value: unknown = Reflect.apply(authority.issue, authority, [input]); + return verifyProtocolRecord(value, "cint/decision-receipt/1", "test receipt"); +} + +export function executeUntrusted(input: unknown): Promise { + const value: unknown = Reflect.apply(executeWithReceipt, undefined, [input]); + assert.ok(value instanceof Promise, "execution boundary must return a promise"); + return value.then((result: unknown) => + verifyProtocolRecord(result, "cint/execution-result/1", "test execution result") + ); +} diff --git a/tests/npm-pack-launch.test.mjs b/tests/npm-pack-launch.test.mjs new file mode 100644 index 0000000..c4588d7 --- /dev/null +++ b/tests/npm-pack-launch.test.mjs @@ -0,0 +1,107 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + NPM_PACK_ARGUMENTS, + npmPackInvocation, + readNpmPackReport, + spawnNpmPack +} from "../scripts/npm-pack-launch.mjs"; + +test("npm CLI metadata uses the current Node executable", () => { + const invocation = npmPackInvocation({ + platform: "win32", + execPath: "C:\\Node\\node.exe", + npmExecPath: "C:\\Node\\node_modules\\npm\\bin\\npm-cli.js", + commandProcessor: "C:\\Windows\\System32\\cmd.exe" + }); + + assert.deepEqual(invocation, { + command: "C:\\Node\\node.exe", + args: ["C:\\Node\\node_modules\\npm\\bin\\npm-cli.js", ...NPM_PACK_ARGUMENTS], + mode: "NPM_CLI_PATH", + shell: false + }); +}); + +test("Windows fallback uses a fixed command processor invocation", () => { + const invocation = npmPackInvocation({ + platform: "win32", + execPath: "C:\\Node\\node.exe", + npmExecPath: undefined, + commandProcessor: "C:\\Windows\\System32\\cmd.exe" + }); + + assert.deepEqual(invocation, { + command: "C:\\Windows\\System32\\cmd.exe", + args: ["/d", "/s", "/c", "npm.cmd pack --dry-run --json --ignore-scripts"], + mode: "WINDOWS_COMMAND_PROCESSOR", + shell: false + }); +}); + +test("POSIX fallback invokes npm without a shell", () => { + const invocation = npmPackInvocation({ + platform: "linux", + execPath: "/usr/bin/node", + npmExecPath: undefined, + commandProcessor: undefined + }); + + assert.deepEqual(invocation, { + command: "npm", + args: [...NPM_PACK_ARGUMENTS], + mode: "POSIX_PATH", + shell: false + }); +}); + +test("spawn errors are reported before status handling", () => { + const launchError = Object.assign(new Error("spawn ENOENT"), { code: "ENOENT" }); + assert.throws( + () => readNpmPackReport({ error: launchError, status: null, signal: null, stderr: "", stdout: "" }), + /npm pack could not start \(ENOENT\): spawn ENOENT/u + ); +}); + +test("null status is reported as a distinct launch failure", () => { + assert.throws( + () => readNpmPackReport({ error: undefined, status: null, signal: "SIGTERM", stderr: "", stdout: "" }), + /npm pack returned no exit status after signal SIGTERM/u + ); +}); + +test("successful JSON dry-run output is admitted unchanged", () => { + let observed; + const successful = { + error: undefined, + status: 0, + signal: null, + stderr: "", + stdout: JSON.stringify([{ files: [{ path: "dist/src/cint/index.js" }] }]) + }; + const { invocation, result } = spawnNpmPack({ + cwd: "/candidate", + platform: "linux", + execPath: "/usr/bin/node", + npmExecPath: "/usr/lib/node_modules/npm/bin/npm-cli.js", + commandProcessor: undefined, + spawn(command, args, options) { + observed = { command, args, options }; + return successful; + } + }); + + assert.equal(invocation.mode, "NPM_CLI_PATH"); + assert.deepEqual(observed, { + command: "/usr/bin/node", + args: ["/usr/lib/node_modules/npm/bin/npm-cli.js", ...NPM_PACK_ARGUMENTS], + options: { + cwd: "/candidate", + encoding: "utf8", + shell: false, + windowsHide: true + } + }); + assert.deepEqual(readNpmPackReport(result), [{ files: [{ path: "dist/src/cint/index.js" }] }]); +}); diff --git a/tests/package-exports.consumer.ts b/tests/package-exports.consumer.ts new file mode 100644 index 0000000..3162916 --- /dev/null +++ b/tests/package-exports.consumer.ts @@ -0,0 +1,9 @@ +import { decide } from "@sntllgnc/cint"; +import { main } from "@sntllgnc/cint/cli"; +import { SyntheticFilePatchAdapter } from "@sntllgnc/cint/adapters/synthetic-file-patch"; +import { CodexDelegationCintAdapter } from "@sntllgnc/cint/adapters/codex-delegation"; + +void decide; +void main; +void SyntheticFilePatchAdapter; +void CodexDelegationCintAdapter; diff --git a/tests/types/contracts.test.ts b/tests/types/contracts.test.ts new file mode 100644 index 0000000..7f732be --- /dev/null +++ b/tests/types/contracts.test.ts @@ -0,0 +1,95 @@ +import type { + ActionDigest, + AuthorityId, + OutcomeDigest, + PolicyId +} from "../../src/cint/types/brands.js"; +import type { + LegacyAgentFloorAdmission, + VerifiedAdapterOutput +} from "../../src/cint/types/adapters.js"; +import type { + AdmitDecision, + AdapterExecution, + AuthorityRecord, + ConsumedReceiptRecord, + Decision, + DenyDecision, + IssuedDecisionReceipt, + Revalidation, + ReviewDecision, + VerifiedOutcome +} from "../../src/cint/types/records.js"; +import type { DecisionReceiptAuthority } from "../../src/cint/receipt.js"; +import type { OutcomeSealAuthority } from "../../src/cint/seal.js"; +import type { ExecutionLedgerEntry } from "../../src/cint/evidence.js"; + +declare const decision: Decision; +declare const admitDecision: AdmitDecision; +declare const denyDecision: DenyDecision; +declare const reviewDecision: ReviewDecision; +declare const issuedReceipt: IssuedDecisionReceipt; +declare const consumedReceipt: ConsumedReceiptRecord; +declare const actionDigest: ActionDigest; +declare const outcomeDigest: OutcomeDigest; +declare const policyId: PolicyId; +declare const authorityId: AuthorityId; +declare const adapterExecution: AdapterExecution; +declare const verifiedAdapterOutput: VerifiedAdapterOutput; +declare const verifiedOutcome: VerifiedOutcome; +declare const legacyAdmission: LegacyAgentFloorAdmission; +declare const unknownProtocolRecord: unknown; +declare const authority: AuthorityRecord; +declare const receiptAuthority: DecisionReceiptAuthority; +declare const sealAuthority: OutcomeSealAuthority; +declare const revalidation: Revalidation; +declare const ledgerEntry: ExecutionLedgerEntry; + +function requireReceipt(value: IssuedDecisionReceipt): void { void value; } +function issueReceipt(value: AdmitDecision): void { void value; } +function requireConsumed(value: ConsumedReceiptRecord): void { void value; } +function requireActionDigest(value: ActionDigest): void { void value; } +function requireAuthorityId(value: AuthorityId): void { void value; } +function requireVerifiedOutput(value: VerifiedAdapterOutput): void { void value; } +function requireVerifiedOutcome(value: VerifiedOutcome): void { void value; } +function requireAuthority(value: AuthorityRecord): void { void value; } + +requireReceipt(issuedReceipt); +issueReceipt(admitDecision); +requireConsumed(consumedReceipt); +requireActionDigest(actionDigest); +requireAuthorityId(authorityId); +requireVerifiedOutput(verifiedAdapterOutput); +requireVerifiedOutcome(verifiedOutcome); +requireAuthority(authority); +receiptAuthority.issue({ decision: admitDecision, issued_at: "2026-08-27T00:00:00.000Z" }); +sealAuthority.issue({ receipt: issuedReceipt, consumption: consumedReceipt, revalidation, outcome: verifiedOutcome, ledger_head: ledgerEntry, issued_at: "2026-08-27T00:00:00.000Z" }); + +// @ts-expect-error a decision is not an issued receipt +requireReceipt(decision); +// @ts-expect-error the concrete receipt authority rejects DENY at compile time +receiptAuthority.issue({ decision: denyDecision, issued_at: "2026-08-27T00:00:00.000Z" }); +// @ts-expect-error the concrete receipt authority rejects REVIEW at compile time +receiptAuthority.issue({ decision: reviewDecision, issued_at: "2026-08-27T00:00:00.000Z" }); +// @ts-expect-error DENY cannot enter receipt issuance +issueReceipt(denyDecision); +// @ts-expect-error REVIEW cannot enter receipt issuance +issueReceipt(reviewDecision); +// @ts-expect-error an issued receipt is not consumed evidence +requireConsumed(issuedReceipt); +// @ts-expect-error issued receipt is not consumed evidence for concrete seal issuance +sealAuthority.issue({ receipt: issuedReceipt, consumption: issuedReceipt, revalidation, outcome: verifiedOutcome, ledger_head: ledgerEntry, issued_at: "2026-08-27T00:00:00.000Z" }); +// @ts-expect-error an outcome digest cannot bind an action +requireActionDigest(outcomeDigest); +// @ts-expect-error a policy identifier is not an authority identifier +requireAuthorityId(policyId); +// @ts-expect-error unverified adapter execution is not verified output +requireVerifiedOutput(adapterExecution); +// @ts-expect-error unverified adapter execution is not a verified outcome +requireVerifiedOutcome(adapterExecution); +// @ts-expect-error unverified adapter execution cannot be sealed as an outcome +sealAuthority.issue({ receipt: issuedReceipt, consumption: consumedReceipt, revalidation, outcome: adapterExecution, ledger_head: ledgerEntry, issued_at: "2026-08-27T00:00:00.000Z" }); +// @ts-expect-error legacy ADMITTED telemetry has no CINT authority +requireAuthority(legacyAdmission); +// @ts-expect-error an unknown protocol record must be validated first +requireAuthority(unknownProtocolRecord); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..9c1db68 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022" + ], + "types": [ + "node" + ], + "strict": true, + "noImplicitAny": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "useUnknownInCatchVariables": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + "forceConsistentCasingInFileNames": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmitOnError": true, + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": false + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..5748fea --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist" + }, + "include": [ + "src/cint/**/*.ts", + "bin/cint.ts" + ] +} diff --git a/tsconfig.cint-test-build.json b/tsconfig.cint-test-build.json new file mode 100644 index 0000000..e446a22 --- /dev/null +++ b/tsconfig.cint-test-build.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./.test-dist", + "declaration": false, + "declarationMap": false, + "sourceMap": false + }, + "include": [ + "src/cint/**/*.ts", + "bin/cint.ts", + "tests/cint-legacy-test-boundary.ts", + "tests/cint-test-support.ts", + "tests/cint-*.test.ts" + ] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..5e52034 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "declaration": false, + "declarationMap": false, + "sourceMap": false + }, + "include": [ + "src/cint/**/*.ts", + "bin/cint.ts", + "tests/cint-legacy-test-boundary.ts", + "tests/cint-test-support.ts", + "tests/cint-*.test.ts", + "tests/types/**/*.ts" + ] +}