From fb75658a389203ba78cc9330c4544555cd417872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Thu, 27 Aug 2026 20:16:21 +0300 Subject: [PATCH 01/16] cint(g1): seal baseline and reconcile foundation --- artifacts/cint-r0/gate-ledger.json | 22 ++++++ artifacts/cint-r0/gates/CINT-R0.G1.json | 23 ++++++ docs/cint-r0/00_BASELINE_SEAL.md | 29 +++++++ docs/cint-r0/01_FOUNDATION_RECONCILIATION.md | 80 ++++++++++++++++++++ scripts/publication-audit.mjs | 7 +- scripts/verify-evidence-manifest.mjs | 39 ++++++++-- 6 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 artifacts/cint-r0/gate-ledger.json create mode 100644 artifacts/cint-r0/gates/CINT-R0.G1.json create mode 100644 docs/cint-r0/00_BASELINE_SEAL.md create mode 100644 docs/cint-r0/01_FOUNDATION_RECONCILIATION.md diff --git a/artifacts/cint-r0/gate-ledger.json b/artifacts/cint-r0/gate-ledger.json new file mode 100644 index 0000000..a283a57 --- /dev/null +++ b/artifacts/cint-r0/gate-ledger.json @@ -0,0 +1,22 @@ +{ + "protocol": "cint/gate-ledger/1", + "baseline": { + "commit": "d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49", + "tag": "v0.1.0-af-g0" + }, + "gates": [ + { + "gate": "CINT-R0.G0", + "name": "Historical baseline freeze", + "status": "PASS", + "evidence": "docs/cint-r0/00_BASELINE_SEAL.md" + }, + { + "gate": "CINT-R0.G1", + "name": "Preservation map and archive plan", + "status": "PASS", + "evidence": "docs/cint-r0/01_FOUNDATION_RECONCILIATION.md" + } + ], + "terminal_verdict": null +} diff --git a/artifacts/cint-r0/gates/CINT-R0.G1.json b/artifacts/cint-r0/gates/CINT-R0.G1.json new file mode 100644 index 0000000..b993f1d --- /dev/null +++ b/artifacts/cint-r0/gates/CINT-R0.G1.json @@ -0,0 +1,23 @@ +{ + "gate": "CINT-R0.G1", + "status": "PASS", + "reviewers": [ + { + "role": "preservation-and-lineage", + "mode": "clean-read-only", + "result": "COMPLETE" + }, + { + "role": "control-gap-and-threat-boundary", + "mode": "clean-read-only", + "result": "COMPLETE" + } + ], + "root_reconciliation": { + "legacy_disposition": "CINT Adapter 01", + "source_modification": "root-only-sequential", + "confirmed_p0_gaps": 4, + "confirmed_p1_gaps": 2 + }, + "evidence": "docs/cint-r0/01_FOUNDATION_RECONCILIATION.md" +} diff --git a/docs/cint-r0/00_BASELINE_SEAL.md b/docs/cint-r0/00_BASELINE_SEAL.md new file mode 100644 index 0000000..da79e8a --- /dev/null +++ b/docs/cint-r0/00_BASELINE_SEAL.md @@ -0,0 +1,29 @@ +# CINT-R0 historical baseline seal + +The CINT-R0 successor begins from the immutable Agent Floor release object below. + +| Field | Sealed value | +|---|---| +| Commit | `d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49` | +| Historical tag | `v0.1.0-af-g0` | +| Historical tests | 14 passed, 0 failed | +| Historical evidence manifest | 53 entries verified | +| Dependency audit | 0 known vulnerabilities | +| Publication audit | Passed | + +The tag and commit remain historical proof. CINT work occurs only on the local +successor branch `cint-r0-reassembly`. + +## Preserved non-actions + +- no push or remote branch creation; +- no change to `main` or `v0.1.0-af-g0`; +- no repository rename, release, package publication, or announcement; +- no action against another repository or live machine state. + +## Baseline acceptance + +The release commit and tag resolve to the same Git object. The complete required +baseline verification sequence passed before any tracked successor file was +created: dependency installation, test suite, demo, dependency audit, +publication audit, and evidence-manifest verification. diff --git a/docs/cint-r0/01_FOUNDATION_RECONCILIATION.md b/docs/cint-r0/01_FOUNDATION_RECONCILIATION.md new file mode 100644 index 0000000..ba085f3 --- /dev/null +++ b/docs/cint-r0/01_FOUNDATION_RECONCILIATION.md @@ -0,0 +1,80 @@ +# CINT-R0 foundation reconciliation + +## Root decision + +Agent Floor is preserved as **CINT Adapter 01 — Delegated Coding-Agent +Execution**. Its verified controls remain useful, but its packet and admission +records are not CINT decision authority. The CINT core alone will resolve the +principal, evaluate current authority and policy, issue an action-bound receipt, +consume it once, revalidate immediately before execution, and seal or roll back +the outcome. + +## Preserve, reframe, archive, replace + +| Disposition | Surface | Decision and evidence | +|---|---|---| +| Preserve | Canonical JSON, hashing, safe paths, atomic JSON | Shared boundary primitives remain behaviorally compatible (`src/util.js:13-127`). | +| Preserve | Task validation and clean delegation | Retain every AF-G0 limit and error meaning (`src/policy.js:6-16`, `src/policy.js:81-175`). | +| Preserve | Packet, lineage, source manifest | Retain as the adapter request envelope, never as executable authority (`src/packet.js:7-90`). | +| Preserve | Ephemeral Codex runner | Retain as a read-only action adapter with its existing process controls (`src/runner.js:198-258`, `src/runner.js:376-501`). | +| Preserve | Request-local accounting | Retain as legacy adapter telemetry, separate from CINT authorization (`src/audit.js:53-208`). | +| Preserve | Evidence and semantic verification | Retain as adapter outcome evidence offered to CINT (`src/admission.js:84-205`). | +| Preserve | Tests, sanitized fixtures, release artifacts | Keep the historical 14-test regression and exact AF-G0 proof values. | +| Reframe | Active product identity | SI1 CINT is the product; Agent Floor becomes Adapter 01. | +| Archive | Build Week, judge, video, and competition narrative | Retain as historical documentation, outside the active product surface. | +| Replace | Parent-controlled final admission | CINT decisions and one-shot receipts become the only authority for consequential action. | + +## Historical compatibility contract + +The following observables cannot change during extraction: + +- `agent-floor/1`, `agent-floor/run/1`, and `agent-floor/admission/1` protocol + records; +- every exported legacy function and every `AF_*` error code; +- clean context, `fork_turns="none"`, depth one, read-only execution, and child + fan-out prohibition; +- packet limit 8 KiB, concurrency two, cycles six, runtime 600 seconds, output + 32 KiB, request-local token ceiling 2,000,000, and 32 allowed paths; +- the AF-G0 sanitized totals: 555,300,000 raw cumulative tokens; 1,492,621 + request-local tokens; 1,380,352 cached input; 103,132 fresh input; 9,137 + output; 16 calls; one duplicate removed; +- source-hash, path, line, exact-excerpt, semantic, and runtime rejection behavior. + +## Confirmed CINT gaps + +| Priority | Gap | Consequence | +|---|---|---| +| P0 | No principal or external decision-bound authority | A valid local task spec currently proceeds directly to child execution. | +| P0 | No signed, expiring, action/target/context-bound receipt | A packet hash cannot grant or prove one-shot authority. | +| P0 | No atomic receipt consumption or replay state | The legacy protocol cannot prove exactly-once authorization. | +| P0 | Adapter creates its packet and returns `ADMITTED` | Legacy verification is incorrectly positioned as final authority (`src/admission.js:184-205`). | +| P1 | Source drift is checked after child execution | Current authority, policy, target, and machine state are not revalidated at the action boundary (`src/admission.js:122-140`). | +| P1 | No fail-closed action adapter, interrupt, outcome, or rollback contract | Divergent consequential outcomes cannot be contained and sealed. | + +## Threat boundary + +Realistic starting capabilities include a principal proposing an overbroad but +well-formed action, hostile content inside an allowed source, a changed local +policy or machine state, and a replayed or modified receipt. CINT must deny any +new capability unless all current bindings survive revalidation. A compromised +operator account, operating system, or CINT signing authority is not treated as +an attacker starting condition for R0. + +The existing documentation assigns packet creation and final admission to Agent +Floor (`docs/THREAT-MODEL.md:14-19`). That remains true for the frozen release, +but the active CINT architecture must place those records below the CINT +decision boundary. + +## Sequential extraction order + +1. Move legacy implementations behind adapter modules while retaining root + compatibility re-exports. +2. Add strict CINT canonical objects and schemas without granting execution. +3. Add principal, authority, policy, challenge, and decision protocols. +4. Add signed decision receipts and durable one-shot consumption. +5. Add immediate revalidation, fail-closed execution, outcome verification, + interrupt, rollback, evidence seal, and ledger. +6. Integrate the preserved Codex adapter only as a CINT action adapter. +7. Replace the active documentation surface and run independent terminal + verification. + diff --git a/scripts/publication-audit.mjs b/scripts/publication-audit.mjs index bee3c5d..025b2eb 100644 --- a/scripts/publication-audit.mjs +++ b/scripts/publication-audit.mjs @@ -8,6 +8,7 @@ const ignoredDirectories = new Set([".git", "node_modules"]); const ignoredPrefixes = ["artifacts/generated/"]; const allowedDotfiles = new Set([".gitignore"]); const privateNames = ["S" + "I1", "Y" + "I1", "S" + "I6", "F" + "YRE"]; +const authorizedPublicNames = [new RegExp("\\b" + "SI1 CINT" + "\\b", "gi")]; const findings = []; const files = []; const excludedMetadata = []; @@ -74,8 +75,12 @@ for (const file of files) { for (const [code, expression] of contentRules) { if (expression.test(content)) findings.push({ code, path: file.relative }); } + const privateNameSurface = authorizedPublicNames.reduce( + (value, expression) => value.replace(expression, "CINT"), + content + ); for (const name of privateNames) { - if (new RegExp(`\\b${name}\\b`, "i").test(content)) { + if (new RegExp(`\\b${name}\\b`, "i").test(privateNameSurface)) { findings.push({ code: "PRIVATE_PROJECT_NAME", path: file.relative }); } } diff --git a/scripts/verify-evidence-manifest.mjs b/scripts/verify-evidence-manifest.mjs index 1b92dd7..873c2ae 100644 --- a/scripts/verify-evidence-manifest.mjs +++ b/scripts/verify-evidence-manifest.mjs @@ -1,21 +1,46 @@ #!/usr/bin/env node import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const manifestPath = path.join(root, "artifacts", "evidence-manifest.json"); -const manifest = JSON.parse(await readFile(manifestPath, "utf8")); +const release = "v0.1.0-af-g0"; +const releaseCommit = "d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49"; +const resolvedRelease = spawnSync("git", ["rev-parse", `${release}^{commit}`], { + cwd: root, + encoding: "utf8" +}); +const immutableReleaseAvailable = + resolvedRelease.status === 0 && resolvedRelease.stdout.trim() === releaseCommit; + +function readImmutableRelease(relativePath) { + const result = spawnSync("git", ["show", `${release}:${relativePath}`], { + cwd: root, + encoding: null, + maxBuffer: 16 * 1024 * 1024 + }); + return result.status === 0 ? result.stdout : null; +} + +const manifestBytes = immutableReleaseAvailable + ? readImmutableRelease("artifacts/evidence-manifest.json") + : await readFile(manifestPath).catch(() => null); +const manifest = manifestBytes === null ? null : JSON.parse(manifestBytes.toString("utf8")); const failures = []; +if (manifest === null) failures.push({ path: "artifacts/evidence-manifest.json", reason: "MISSING" }); -for (const entry of manifest.public_tree.entries) { +for (const entry of manifest?.public_tree?.entries ?? []) { const absolute = path.resolve(root, entry.path); if (absolute !== root && !absolute.startsWith(`${root}${path.sep}`)) { failures.push({ path: entry.path, reason: "OUTSIDE_RELEASE_ROOT" }); continue; } - const bytes = await readFile(absolute).catch(() => null); + const bytes = immutableReleaseAvailable + ? readImmutableRelease(entry.path) + : await readFile(absolute).catch(() => null); if (bytes === null) { failures.push({ path: entry.path, reason: "MISSING" }); continue; @@ -29,9 +54,11 @@ for (const entry of manifest.public_tree.entries) { const result = { gate: "AF-EVIDENCE-MANIFEST", verdict: failures.length === 0 ? "PASS" : "FAIL", - release: manifest.release, - entries_verified: manifest.public_tree.entries.length, - source_archive_status: manifest.source_archive.status, + release: manifest?.release ?? release, + release_commit: releaseCommit, + verification_source: immutableReleaseAvailable ? "immutable-git-release" : "current-release-tree", + entries_verified: manifest?.public_tree?.entries?.length ?? 0, + source_archive_status: manifest?.source_archive?.status ?? "UNAVAILABLE", failures }; From 960fb77f95e6ef95d11a6a6f3022271b506f3859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Thu, 27 Aug 2026 20:18:37 +0300 Subject: [PATCH 02/16] cint(g2): isolate legacy Codex adapter --- artifacts/cint-r0/gate-ledger.json | 6 + artifacts/cint-r0/gates/CINT-R0.G2.json | 16 + docs/cint-r0/01_FOUNDATION_RECONCILIATION.md | 1 - docs/cint-r0/02_LEGACY_EXTRACTION.md | 33 ++ src/adapters/codex-delegation/admission.js | 206 +++++++ src/adapters/codex-delegation/audit.js | 209 ++++++++ src/adapters/codex-delegation/index.js | 23 + src/adapters/codex-delegation/packet.js | 115 ++++ src/adapters/codex-delegation/policy.js | 259 +++++++++ src/adapters/codex-delegation/runner.js | 529 ++++++++++++++++++ src/admission.js | 208 +------- src/audit.js | 211 +------- src/packet.js | 117 +--- src/policy.js | 261 +-------- src/runner.js | 531 +------------------ tests/legacy-adapter.test.js | 30 ++ 16 files changed, 1436 insertions(+), 1319 deletions(-) create mode 100644 artifacts/cint-r0/gates/CINT-R0.G2.json create mode 100644 docs/cint-r0/02_LEGACY_EXTRACTION.md create mode 100644 src/adapters/codex-delegation/admission.js create mode 100644 src/adapters/codex-delegation/audit.js create mode 100644 src/adapters/codex-delegation/index.js create mode 100644 src/adapters/codex-delegation/packet.js create mode 100644 src/adapters/codex-delegation/policy.js create mode 100644 src/adapters/codex-delegation/runner.js create mode 100644 tests/legacy-adapter.test.js diff --git a/artifacts/cint-r0/gate-ledger.json b/artifacts/cint-r0/gate-ledger.json index a283a57..d89c541 100644 --- a/artifacts/cint-r0/gate-ledger.json +++ b/artifacts/cint-r0/gate-ledger.json @@ -16,6 +16,12 @@ "name": "Preservation map and archive plan", "status": "PASS", "evidence": "docs/cint-r0/01_FOUNDATION_RECONCILIATION.md" + }, + { + "gate": "CINT-R0.G2", + "name": "Module extraction with behavioral equivalence", + "status": "PASS", + "evidence": "docs/cint-r0/02_LEGACY_EXTRACTION.md" } ], "terminal_verdict": null diff --git a/artifacts/cint-r0/gates/CINT-R0.G2.json b/artifacts/cint-r0/gates/CINT-R0.G2.json new file mode 100644 index 0000000..2d18203 --- /dev/null +++ b/artifacts/cint-r0/gates/CINT-R0.G2.json @@ -0,0 +1,16 @@ +{ + "gate": "CINT-R0.G2", + "status": "PASS", + "legacy_tests": { + "passed": 14, + "failed": 0 + }, + "adapter_boundary_tests": { + "passed": 2, + "failed": 0 + }, + "historical_evidence_entries": 53, + "compatibility_shims": 5, + "adapter_authority": "NONE", + "evidence": "docs/cint-r0/02_LEGACY_EXTRACTION.md" +} diff --git a/docs/cint-r0/01_FOUNDATION_RECONCILIATION.md b/docs/cint-r0/01_FOUNDATION_RECONCILIATION.md index ba085f3..1d20172 100644 --- a/docs/cint-r0/01_FOUNDATION_RECONCILIATION.md +++ b/docs/cint-r0/01_FOUNDATION_RECONCILIATION.md @@ -77,4 +77,3 @@ decision boundary. 6. Integrate the preserved Codex adapter only as a CINT action adapter. 7. Replace the active documentation surface and run independent terminal verification. - diff --git a/docs/cint-r0/02_LEGACY_EXTRACTION.md b/docs/cint-r0/02_LEGACY_EXTRACTION.md new file mode 100644 index 0000000..2f8fa2f --- /dev/null +++ b/docs/cint-r0/02_LEGACY_EXTRACTION.md @@ -0,0 +1,33 @@ +# CINT-R0 legacy extraction + +The verified Agent Floor implementation now resides under +`src/adapters/codex-delegation/`. Historical root imports remain as thin +re-export shims, so the CLI, examples, tests, and downstream imports keep their +existing behavior. + +## Module map + +| Historical surface | Adapter implementation | Compatibility surface | +|---|---|---| +| Policy validation | `src/adapters/codex-delegation/policy.js` | `src/policy.js` | +| Packet and lineage | `src/adapters/codex-delegation/packet.js` | `src/packet.js` | +| Request-local audit | `src/adapters/codex-delegation/audit.js` | `src/audit.js` | +| Evidence verification | `src/adapters/codex-delegation/admission.js` | `src/admission.js` | +| Ephemeral execution | `src/adapters/codex-delegation/runner.js` | `src/runner.js` | + +`src/adapters/codex-delegation/index.js` is the explicit Adapter 01 boundary. +It declares that the adapter cannot mint a decision, issue or consume a receipt, +self-admit, bypass revalidation, or seal an outcome. + +## Behavioral-equivalence proof + +- all 14 frozen AF-G0 tests pass unchanged; +- the historical demo produces the same admitted regression record; +- direct adapter exports and root compatibility exports are reference-equal; +- all legacy protocol strings, observable error codes, and runtime controls + remain in the moved implementations; +- the immutable `v0.1.0-af-g0` evidence manifest still verifies all 53 release + entries from the historical Git object. + +No CINT decision or execution authority is introduced at this gate. + diff --git a/src/adapters/codex-delegation/admission.js b/src/adapters/codex-delegation/admission.js new file mode 100644 index 0000000..bff5fea --- /dev/null +++ b/src/adapters/codex-delegation/admission.js @@ -0,0 +1,206 @@ +import { readFile } from "node:fs/promises"; +import { resolveInside, normalizeLine, sha256 } from "../../util.js"; + +function rejection(code, message, details = undefined) { + return { code, message, ...(details === undefined ? {} : { details }) }; +} + +function validateOutputShape(output) { + const reasons = []; + if (!output || typeof output !== "object" || Array.isArray(output)) { + return [rejection("AF_OUTPUT_INVALID", "Worker output must be an object")]; + } + if (typeof output.finding_code !== "string" || !/^[A-Z][A-Z0-9_]{1,79}$/.test(output.finding_code)) { + reasons.push(rejection("AF_OUTPUT_INVALID", "finding_code must be an uppercase machine identifier")); + } + if (!["FOUND", "NO_FINDING", "BLOCKED"].includes(output.status)) { + reasons.push(rejection("AF_OUTPUT_INVALID", "status must be FOUND, NO_FINDING, or BLOCKED")); + } + if (typeof output.finding !== "string" || output.finding.trim().length === 0) { + reasons.push(rejection("AF_OUTPUT_INVALID", "finding must be a non-empty string")); + } + if (typeof output.summary !== "string" || output.summary.trim().length === 0) { + reasons.push(rejection("AF_OUTPUT_INVALID", "summary must be a non-empty string")); + } + if (!Array.isArray(output.evidence)) { + reasons.push(rejection("AF_OUTPUT_INVALID", "evidence must be an array")); + } + if (!["ADMIT", "REJECT"].includes(output.recommendation)) { + reasons.push(rejection("AF_OUTPUT_INVALID", "recommendation must be ADMIT or REJECT")); + } + if (output.status === "FOUND" && Array.isArray(output.evidence) && output.evidence.length === 0) { + reasons.push(rejection("AF_EVIDENCE_MISSING", "FOUND results require at least one evidence reference")); + } + return reasons; +} + +async function verifyEvidence(spec, evidence) { + if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) { + throw rejection("AF_EVIDENCE_INVALID", "Evidence entry must be an object"); + } + if (!spec.worker.allowed_paths.includes(evidence.path)) { + throw rejection("AF_EVIDENCE_OUTSIDE_AUTHORITY", `Evidence path was not granted to the worker: ${evidence.path}`); + } + if (!Number.isSafeInteger(evidence.line) || evidence.line < 1) { + throw rejection("AF_EVIDENCE_LINE_INVALID", "Evidence line must be a positive integer", { line: evidence.line }); + } + if (typeof evidence.claim !== "string" || evidence.claim.trim().length === 0) { + throw rejection("AF_EVIDENCE_CLAIM_INVALID", "Evidence claim must be a non-empty string"); + } + if (typeof evidence.excerpt !== "string" || normalizeLine(evidence.excerpt).length < 4) { + throw rejection("AF_EVIDENCE_EXCERPT_INVALID", "Evidence excerpt must contain at least four normalized characters"); + } + let resolved; + try { + resolved = await resolveInside(spec.repository_boundary, evidence.path); + } catch (error) { + throw rejection(error.code ?? "AF_EVIDENCE_FILE_MISSING", error.message, error.details); + } + const text = await readFile(resolved.absolute, "utf8"); + const lines = text.split(/\r?\n/); + if (evidence.line > lines.length) { + throw rejection("AF_EVIDENCE_LINE_MISSING", `Evidence line ${evidence.line} does not exist in ${resolved.relative}`, { lineCount: lines.length }); + } + const actual = lines[evidence.line - 1]; + const normalizedActual = normalizeLine(actual); + const normalizedExcerpt = normalizeLine(evidence.excerpt); + if (!normalizedActual.includes(normalizedExcerpt)) { + throw rejection( + "AF_EVIDENCE_EXCERPT_MISMATCH", + `Evidence excerpt does not match ${resolved.relative}:${evidence.line}`, + { expectedExcerpt: normalizedExcerpt, actualLine: normalizedActual } + ); + } + return { + reference: `${resolved.relative}:${evidence.line}`, + path: resolved.relative, + line: evidence.line, + claim: evidence.claim.trim(), + excerpt: normalizedExcerpt, + evidence_sha256: sha256(`${resolved.relative}:${evidence.line}:${normalizedActual}`) + }; +} + +export async function admitWorkerResult({ spec, packetRecord, audit, output, runtime = {} }) { + const reasons = validateOutputShape(output); + const evidenceRecords = []; + if (output && typeof output === "object") { + if (!spec.admission.allowed_statuses.includes(output.status)) { + reasons.push( + rejection("AF_ADMISSION_STATUS_FORBIDDEN", `Worker status is outside the parent admission policy: ${output.status}`) + ); + } + if (!spec.admission.allowed_finding_codes.includes(output.finding_code)) { + reasons.push( + rejection("AF_FINDING_CODE_FORBIDDEN", `Finding code is outside the parent admission policy: ${output.finding_code}`) + ); + } + const evidenceClaims = Array.isArray(output.evidence) + ? output.evidence.map((item) => item?.claim ?? "").join(" ") + : ""; + const semanticText = normalizeLine(`${output.finding ?? ""} ${output.summary ?? ""} ${evidenceClaims}`).toLowerCase(); + for (const term of spec.admission.required_output_terms) { + if (!semanticText.includes(normalizeLine(term).toLowerCase())) { + reasons.push(rejection("AF_REQUIRED_TERM_MISSING", `Required admission term is absent: ${term}`)); + } + } + for (const term of spec.admission.forbidden_output_terms) { + if (semanticText.includes(normalizeLine(term).toLowerCase())) { + reasons.push(rejection("AF_FORBIDDEN_TERM_PRESENT", `Forbidden admission term is present: ${term}`)); + } + } + if (Array.isArray(output.evidence) && output.evidence.length < spec.admission.minimum_evidence) { + reasons.push( + rejection("AF_EVIDENCE_MINIMUM", "Worker supplied fewer evidence items than the parent admission policy requires", { + actual: output.evidence.length, + minimum: spec.admission.minimum_evidence + }) + ); + } + } + + for (const source of packetRecord?.packet?.authority?.source_manifest ?? []) { + try { + const resolved = await resolveInside(spec.repository_boundary, source.path); + const bytes = await readFile(resolved.absolute); + const currentHash = sha256(bytes); + if (bytes.byteLength !== source.bytes || currentHash !== source.sha256) { + reasons.push( + rejection("AF_SOURCE_CHANGED", `Authority source changed after packet creation: ${source.path}`, { + packet_sha256: source.sha256, + current_sha256: currentHash, + packet_bytes: source.bytes, + current_bytes: bytes.byteLength + }) + ); + } + } catch (error) { + reasons.push(rejection(error.code ?? "AF_SOURCE_CHANGED", error.message, error.details)); + } + } + + if (runtime.exitCode !== undefined && runtime.exitCode !== 0) { + reasons.push(rejection("AF_CHILD_EXIT_NONZERO", `Child process exited with code ${runtime.exitCode}`)); + } + for (const violation of runtime.violations ?? []) { + reasons.push(rejection(violation.code, violation.message, violation.details)); + } + if (!audit) { + reasons.push(rejection("AF_USAGE_MISSING", "Request-local usage audit is missing")); + } else { + if (audit.context_mode !== "clean" || audit.full_history_detected) { + reasons.push(rejection("AF_CONTEXT_NOT_CLEAN", "A full-history or non-clean run cannot be admitted")); + } + if (audit.incremental_tokens > spec.delegation.max_incremental_tokens) { + reasons.push( + rejection("AF_TOKEN_LIMIT_EXCEEDED", "Incremental token usage exceeded the worker budget", { + actual: audit.incremental_tokens, + limit: spec.delegation.max_incremental_tokens + }) + ); + } + } + if (runtime.outputBytes !== undefined && runtime.outputBytes > spec.delegation.max_output_bytes) { + reasons.push( + rejection("AF_OUTPUT_LIMIT_EXCEEDED", "Worker output exceeded the byte limit", { + actual: runtime.outputBytes, + limit: spec.delegation.max_output_bytes + }) + ); + } + if (output?.status === "BLOCKED") { + reasons.push(rejection("AF_WORKER_BLOCKED", "Blocked worker results are not admissible")); + } + if (Array.isArray(output?.evidence)) { + for (const evidence of output.evidence) { + try { + evidenceRecords.push(await verifyEvidence(spec, evidence)); + } catch (reason) { + reasons.push(reason?.code ? reason : rejection("AF_EVIDENCE_INVALID", String(reason))); + } + } + } + + const result = reasons.length === 0 ? "ADMITTED" : "REJECTED"; + return { + protocol: "agent-floor/admission/1", + worker: spec.worker.id, + parent: spec.root_task_id, + context_mode: audit?.context_mode ?? "unknown", + packet_sha256: packetRecord.packet_sha256, + model: spec.delegation.model, + reasoning_effort: spec.delegation.reasoning_effort, + model_calls: audit?.model_calls ?? 0, + incremental_tokens: audit?.incremental_tokens ?? null, + cached_input_tokens: audit?.cached_input_tokens ?? null, + fresh_input_tokens: audit?.fresh_input_tokens ?? null, + output_tokens: audit?.output_tokens ?? null, + result, + finding: output?.finding ?? null, + finding_code: output?.finding_code ?? null, + target_recommendation: output?.recommendation ?? null, + evidence: evidenceRecords.map((record) => record.reference), + evidence_records: evidenceRecords, + rejection_reasons: reasons + }; +} diff --git a/src/adapters/codex-delegation/audit.js b/src/adapters/codex-delegation/audit.js new file mode 100644 index 0000000..4c1ffbf --- /dev/null +++ b/src/adapters/codex-delegation/audit.js @@ -0,0 +1,209 @@ +import { readFile } from "node:fs/promises"; +import { AgentFloorError, assertFloor } from "../../errors.js"; +import { asFiniteInteger, canonicalJson, parseJsonLines, sha256 } from "../../util.js"; + +function normalizeUsage(raw, label) { + assertFloor(raw && typeof raw === "object", "AF_USAGE_MALFORMED", `${label} is missing`); + const input = asFiniteInteger(raw.input_tokens ?? 0, `${label}.input_tokens`); + const cached = asFiniteInteger(raw.cached_input_tokens ?? 0, `${label}.cached_input_tokens`); + const output = asFiniteInteger(raw.output_tokens ?? 0, `${label}.output_tokens`); + const reasoning = asFiniteInteger(raw.reasoning_output_tokens ?? 0, `${label}.reasoning_output_tokens`); + assertFloor(cached <= input, "AF_USAGE_MALFORMED", `${label} cached input exceeds total input`); + const computedTotal = input + output; + if (raw.total_tokens !== undefined && raw.total_tokens !== null) { + const suppliedTotal = asFiniteInteger(raw.total_tokens, `${label}.total_tokens`); + assertFloor( + suppliedTotal === computedTotal, + "AF_USAGE_MALFORMED", + `${label} total_tokens does not equal input_tokens + output_tokens`, + { suppliedTotal, computedTotal } + ); + } + return { + input_tokens: input, + cached_input_tokens: cached, + fresh_input_tokens: input - cached, + output_tokens: output, + reasoning_output_tokens: reasoning, + total_tokens: computedTotal + }; +} + +function sumUsage(records) { + return records.reduce( + (sum, record) => ({ + input_tokens: sum.input_tokens + record.input_tokens, + cached_input_tokens: sum.cached_input_tokens + record.cached_input_tokens, + fresh_input_tokens: sum.fresh_input_tokens + record.fresh_input_tokens, + output_tokens: sum.output_tokens + record.output_tokens, + reasoning_output_tokens: sum.reasoning_output_tokens + record.reasoning_output_tokens, + total_tokens: sum.total_tokens + record.total_tokens + }), + { + input_tokens: 0, + cached_input_tokens: 0, + fresh_input_tokens: 0, + output_tokens: 0, + reasoning_output_tokens: 0, + total_tokens: 0 + } + ); +} + +export function auditCodexEvents(events, options = {}) { + assertFloor(Array.isArray(events), "AF_USAGE_MALFORMED", "Codex events must be an array"); + const usages = []; + let failed = false; + let threadId = null; + for (const event of events) { + if (event?.type === "thread.started") threadId = event.thread_id ?? null; + if (event?.type === "turn.failed" || event?.type === "error") failed = true; + if (event?.type === "turn.completed" && event.usage) { + usages.push(normalizeUsage(event.usage, `turn.completed[${usages.length}]`)); + } + } + assertFloor(usages.length > 0, "AF_USAGE_MISSING", "No turn.completed usage events were found"); + const total = sumUsage(usages); + return { + source: "codex-exec-jsonl", + thread_id: threadId, + context_mode: options.contextMode ?? "clean", + full_history_detected: false, + model_calls: usages.length, + input_tokens: total.input_tokens, + cached_input_tokens: total.cached_input_tokens, + fresh_input_tokens: total.fresh_input_tokens, + output_tokens: total.output_tokens, + reasoning_output_tokens: total.reasoning_output_tokens, + incremental_tokens: total.total_tokens, + raw_cumulative_tokens: null, + accounting_status: failed ? "RUN_FAILED_WITH_USAGE" : "REQUEST_LOCAL", + duplicate_usage_events: 0 + }; +} + +function totalTokenValue(event) { + const value = event?.payload?.info?.total_token_usage?.total_tokens; + return Number.isSafeInteger(value) && value >= 0 ? value : null; +} + +function isBoundary(event, worker) { + if (event?.type === "agent_floor.boundary") { + return !worker || event?.payload?.worker === worker; + } + return event?.type === "event_msg" && event?.payload?.type === "agent_floor.boundary" && (!worker || event?.payload?.worker === worker); +} + +function detectForkTurns(events) { + for (const event of events) { + if (event?.type !== "session_meta") continue; + const candidates = [ + event?.payload?.fork_turns, + event?.payload?.source?.fork_turns, + event?.payload?.metadata?.fork_turns + ]; + const found = candidates.find((value) => value !== undefined); + if (found !== undefined) return found; + } + return null; +} + +export function auditRolloutEvents(events, options = {}) { + assertFloor(Array.isArray(events), "AF_USAGE_MALFORMED", "Rollout events must be an array"); + let boundaryIndex = -1; + if (options.after) { + boundaryIndex = events.findIndex((event) => typeof event?.timestamp === "string" && event.timestamp >= options.after); + if (boundaryIndex >= 0) boundaryIndex -= 1; + } else { + boundaryIndex = events.findIndex((event) => isBoundary(event, options.worker)); + } + assertFloor( + boundaryIndex >= 0, + "AF_USAGE_BOUNDARY_MISSING", + "A worker boundary is required; refusing to attribute the entire inherited rollout" + ); + + let baseline = null; + for (let index = 0; index <= boundaryIndex; index += 1) { + const value = totalTokenValue(events[index]); + if (value !== null) baseline = value; + } + + const usageRecords = []; + const fingerprints = new Set(); + let duplicates = 0; + let rawCumulative = baseline; + for (let index = boundaryIndex + 1; index < events.length; index += 1) { + const event = events[index]; + const rawLast = event?.payload?.info?.last_token_usage; + const cumulative = totalTokenValue(event); + if (cumulative !== null && (rawCumulative === null || cumulative > rawCumulative)) { + rawCumulative = cumulative; + } + if (!rawLast) continue; + const fingerprint = sha256( + canonicalJson({ timestamp: event.timestamp ?? null, last: rawLast, cumulative }) + ); + if (fingerprints.has(fingerprint)) { + duplicates += 1; + continue; + } + fingerprints.add(fingerprint); + usageRecords.push(normalizeUsage(rawLast, `last_token_usage[${usageRecords.length}]`)); + } + assertFloor(usageRecords.length > 0, "AF_USAGE_MISSING", "No request-local last_token_usage records were found after the boundary"); + const total = sumUsage(usageRecords); + const cumulativeDelta = baseline === null || rawCumulative === null ? null : rawCumulative - baseline; + const consistent = cumulativeDelta === null ? null : cumulativeDelta === total.total_tokens; + const forkTurns = detectForkTurns(events); + const fullHistory = forkTurns === "all"; + + return { + source: "codex-rollout-token-count", + context_mode: fullHistory ? "full_history" : "unknown", + fork_turns: forkTurns, + full_history_detected: fullHistory, + boundary_index: boundaryIndex, + model_calls: usageRecords.length, + input_tokens: total.input_tokens, + cached_input_tokens: total.cached_input_tokens, + fresh_input_tokens: total.fresh_input_tokens, + output_tokens: total.output_tokens, + reasoning_output_tokens: total.reasoning_output_tokens, + incremental_tokens: total.total_tokens, + baseline_cumulative_tokens: baseline, + raw_cumulative_tokens: rawCumulative, + cumulative_delta_tokens: cumulativeDelta, + cumulative_delta_matches_incremental: consistent, + naive_overstatement_tokens: + rawCumulative === null ? null : rawCumulative - total.total_tokens, + naive_overstatement_factor: + rawCumulative === null ? null : Number((rawCumulative / total.total_tokens).toFixed(3)), + accounting_status: consistent === false ? "CUMULATIVE_MISMATCH" : "INCREMENTAL_VERIFIED", + duplicate_usage_events: duplicates + }; +} + +export async function auditTraceFile(tracePath, options = {}) { + let text; + try { + text = await readFile(tracePath, "utf8"); + } catch (error) { + throw new AgentFloorError("AF_FILE_READ_FAILED", `Cannot read trace ${tracePath}`, { cause: error.message }, 1); + } + const events = parseJsonLines(text, tracePath); + const kind = options.kind ?? inferTraceKind(events); + if (kind === "codex") return auditCodexEvents(events, options); + if (kind === "rollout") return auditRolloutEvents(events, options); + throw new AgentFloorError("AF_TRACE_KIND_INVALID", `Unsupported trace kind: ${kind}`); +} + +export function inferTraceKind(events) { + if (events.some((event) => event?.type === "session_meta" || event?.type === "event_msg" || event?.type === "agent_floor.boundary")) { + return "rollout"; + } + if (events.some((event) => event?.type === "thread.started" || event?.type === "turn.completed")) { + return "codex"; + } + throw new AgentFloorError("AF_TRACE_KIND_UNKNOWN", "Trace does not contain recognized Codex events"); +} diff --git a/src/adapters/codex-delegation/index.js b/src/adapters/codex-delegation/index.js new file mode 100644 index 0000000..8ee55de --- /dev/null +++ b/src/adapters/codex-delegation/index.js @@ -0,0 +1,23 @@ +export * from "./admission.js"; +export * from "./audit.js"; +export * from "./packet.js"; +export * from "./policy.js"; +export * from "./runner.js"; + +export const CODEX_DELEGATION_ADAPTER = Object.freeze({ + id: "cint.adapter.codex-delegation", + display_name: "CINT Adapter 01 — Delegated Coding-Agent Execution", + legacy_protocols: Object.freeze([ + "agent-floor/1", + "agent-floor/run/1", + "agent-floor/admission/1" + ]), + 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 + }) +}); diff --git a/src/adapters/codex-delegation/packet.js b/src/adapters/codex-delegation/packet.js new file mode 100644 index 0000000..4d7fff4 --- /dev/null +++ b/src/adapters/codex-delegation/packet.js @@ -0,0 +1,115 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { AgentFloorError } from "../../errors.js"; +import { FLOOR_LIMITS } from "./policy.js"; +import { canonicalJson, resolveInside, sha256 } from "../../util.js"; + +export async function createChildPacket(spec) { + const sourceManifest = []; + for (const relativePath of spec.worker.allowed_paths) { + const resolved = await resolveInside(spec.repository_boundary, relativePath); + const bytes = await readFile(resolved.absolute); + sourceManifest.push({ + path: resolved.relative, + bytes: bytes.byteLength, + sha256: sha256(bytes) + }); + } + + const packet = { + protocol: "agent-floor/1", + root: { + id: spec.root_task_id, + task: spec.root_task + }, + worker: { + id: spec.worker.id, + objective: spec.worker.objective + }, + lineage: { + parent: spec.root_task_id, + child: spec.worker.id, + depth: 1 + }, + context: { + mode: "clean", + fork_turns: "none", + inherited_turns: 0 + }, + authority: { + repository_boundary: ".", + allowed_paths: spec.worker.allowed_paths, + source_manifest: sourceManifest, + filesystem: "read-only", + network: "denied", + delegation: "denied" + }, + execution: { + model: spec.delegation.model, + reasoning_effort: spec.delegation.reasoning_effort, + max_depth: spec.delegation.max_depth, + max_concurrency: spec.delegation.max_concurrency, + max_cycles: spec.delegation.max_cycles, + max_runtime_seconds: spec.delegation.max_runtime_seconds, + max_output_bytes: spec.delegation.max_output_bytes, + max_incremental_tokens: spec.delegation.max_incremental_tokens + }, + evidence_contract: { + requirements: spec.worker.evidence_requirements, + citation_shape: "repository-relative path + one-based line + exact excerpt + claim", + admission_rule: "Only real in-boundary lines whose normalized text contains the excerpt can be admitted." + }, + admission_policy: spec.admission, + completion_contract: { + format: "worker-output.schema.json", + statuses: ["FOUND", "NO_FINDING", "BLOCKED"], + stop: "Return one schema-valid JSON object, then stop." + } + }; + + const serialized = canonicalJson(packet); + const packetBytes = Buffer.byteLength(serialized, "utf8"); + if (packetBytes > FLOOR_LIMITS.packetBytes) { + throw new AgentFloorError( + "AF_PACKET_TOO_LARGE", + `Child packet is ${packetBytes} bytes; hard limit is ${FLOOR_LIMITS.packetBytes}`, + { packetBytes, limit: FLOOR_LIMITS.packetBytes } + ); + } + const packetSha256 = sha256(serialized); + return { + packet, + packet_bytes: packetBytes, + packet_sha256: packetSha256, + lineage: { + parent: spec.root_task_id, + child: spec.worker.id, + depth: 1, + context_mode: "clean", + packet_sha256: packetSha256 + } + }; +} + +export function renderPacketPrompt(packetRecord) { + return [ + "Execute only the bounded child packet below.", + "Do not infer or request parent conversation history.", + "Return only the JSON object required by the completion contract.", + "", + JSON.stringify(packetRecord.packet, null, 2) + ].join("\n"); +} + +export function renderWorkerInstructions() { + return [ + "# Agent Floor bounded worker", + "", + "You are one read-only child worker operating from a self-contained packet.", + "The packet is the complete task context. No parent transcript exists or may be requested.", + "Do not delegate, spawn agents, use network access, or read outside the declared repository boundary.", + "Use only the allowed paths. Stop when the objective is answered or the evidence is insufficient.", + "Every finding must cite a repository-relative file, a one-based line, an exact excerpt from that line, and a claim.", + "Return exactly one JSON object matching the supplied output schema. Do not wrap it in Markdown." + ].join("\n"); +} diff --git a/src/adapters/codex-delegation/policy.js b/src/adapters/codex-delegation/policy.js new file mode 100644 index 0000000..63a076d --- /dev/null +++ b/src/adapters/codex-delegation/policy.js @@ -0,0 +1,259 @@ +import { readFile, realpath, stat } from "node:fs/promises"; +import path from "node:path"; +import { AgentFloorError, assertFloor } from "../../errors.js"; +import { assertSafeRelativePath, isWithin } from "../../util.js"; + +export const FLOOR_LIMITS = Object.freeze({ + packetBytes: 8 * 1024, + maxDepth: 1, + maxConcurrency: 2, + maxCycles: 6, + maxRuntimeSeconds: 600, + maxOutputBytes: 32 * 1024, + maxIncrementalTokens: 2_000_000, + maxAllowedPaths: 32, + maxEvidenceRequirements: 8 +}); + +export const ALLOWED_MODELS = Object.freeze([ + "gpt-5.6", + "gpt-5.6-terra", + "gpt-5.6-sol" +]); + +export const ALLOWED_REASONING_EFFORTS = Object.freeze([ + "minimal", + "low", + "medium", + "high", + "xhigh", + "ultra", + "max" +]); + +function boundedString(value, label, maximum) { + assertFloor(typeof value === "string", "AF_SPEC_INVALID", `${label} must be a string`); + const trimmed = value.trim(); + assertFloor(trimmed.length > 0, "AF_SPEC_INVALID", `${label} cannot be empty`); + assertFloor( + Buffer.byteLength(trimmed, "utf8") <= maximum, + "AF_SPEC_TOO_LARGE", + `${label} exceeds ${maximum} bytes` + ); + return trimmed; +} + +function identifier(value, label) { + const checked = boundedString(value, label, 80); + assertFloor( + /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(checked), + "AF_SPEC_INVALID", + `${label} may contain only letters, numbers, dot, underscore, and hyphen` + ); + return checked; +} + +function boundedInteger(value, label, minimum, maximum) { + assertFloor( + Number.isSafeInteger(value) && value >= minimum && value <= maximum, + "AF_LIMIT_INVALID", + `${label} must be an integer from ${minimum} through ${maximum}`, + { value, minimum, maximum } + ); + return value; +} + +function boundedStringArray(value, label, options = {}) { + const minimum = options.minimum ?? 0; + const maximum = options.maximum ?? 16; + const bytes = options.bytes ?? 160; + assertFloor(Array.isArray(value), "AF_SPEC_INVALID", `${label} must be an array`); + assertFloor( + value.length >= minimum && value.length <= maximum, + "AF_SPEC_INVALID", + `${label} must contain ${minimum}-${maximum} entries` + ); + const checked = value.map((item, index) => boundedString(item, `${label}[${index}]`, bytes)); + assertFloor(new Set(checked).size === checked.length, "AF_SPEC_INVALID", `${label} contains duplicates`); + return checked; +} + +export async function validateTaskSpec(raw, options = {}) { + assertFloor(raw && typeof raw === "object" && !Array.isArray(raw), "AF_SPEC_INVALID", "Task spec must be an object"); + assertFloor(raw.version === 1, "AF_SPEC_VERSION", "Only Agent Floor task spec version 1 is accepted"); + + const specDir = path.resolve(options.specDir ?? process.cwd()); + const boundaryInput = boundedString(raw.repository_boundary, "repository_boundary", 4096); + const boundaryCandidate = path.resolve(specDir, boundaryInput); + let boundary; + try { + boundary = await realpath(boundaryCandidate); + } catch (error) { + throw new AgentFloorError( + "AF_REPOSITORY_MISSING", + `Repository boundary does not exist: ${boundaryCandidate}`, + { cause: error.message } + ); + } + const boundaryStat = await stat(boundary); + assertFloor(boundaryStat.isDirectory(), "AF_REPOSITORY_INVALID", "Repository boundary must be a directory"); + + const worker = raw.worker; + assertFloor(worker && typeof worker === "object" && !Array.isArray(worker), "AF_SPEC_INVALID", "worker must be an object"); + assertFloor(!("children" in worker), "AF_DEPTH_EXCEEDED", "Nested child declarations are forbidden at AF-G0"); + + const allowedPathsInput = worker.allowed_paths; + assertFloor(Array.isArray(allowedPathsInput), "AF_SPEC_INVALID", "worker.allowed_paths must be an array"); + assertFloor( + allowedPathsInput.length > 0 && allowedPathsInput.length <= FLOOR_LIMITS.maxAllowedPaths, + "AF_SPEC_INVALID", + `worker.allowed_paths must contain 1-${FLOOR_LIMITS.maxAllowedPaths} paths` + ); + const allowedPaths = []; + for (const candidate of allowedPathsInput) { + const relative = assertSafeRelativePath(candidate); + const absoluteCandidate = path.resolve(boundary, relative); + let resolved; + try { + resolved = await realpath(absoluteCandidate); + } catch (error) { + throw new AgentFloorError( + "AF_AUTHORITY_FILE_MISSING", + `Allowed path does not exist: ${relative}`, + { cause: error.message } + ); + } + assertFloor(isWithin(boundary, resolved), "AF_PATH_ESCAPE", `Allowed path escapes repository boundary: ${relative}`); + const fileStat = await stat(resolved); + assertFloor(fileStat.isFile(), "AF_PATH_NOT_FILE", `Allowed path is not a regular file: ${relative}`); + allowedPaths.push(relative); + } + assertFloor(new Set(allowedPaths).size === allowedPaths.length, "AF_SPEC_INVALID", "worker.allowed_paths contains duplicates"); + + const requirementsInput = worker.evidence_requirements; + assertFloor(Array.isArray(requirementsInput), "AF_SPEC_INVALID", "worker.evidence_requirements must be an array"); + assertFloor( + requirementsInput.length > 0 && requirementsInput.length <= FLOOR_LIMITS.maxEvidenceRequirements, + "AF_SPEC_INVALID", + `worker.evidence_requirements must contain 1-${FLOOR_LIMITS.maxEvidenceRequirements} entries` + ); + const evidenceRequirements = requirementsInput.map((value, index) => + boundedString(value, `worker.evidence_requirements[${index}]`, 512) + ); + + const delegation = raw.delegation; + assertFloor(delegation && typeof delegation === "object" && !Array.isArray(delegation), "AF_SPEC_INVALID", "delegation must be an object"); + if (delegation.fork_turns === "all") { + throw new AgentFloorError( + "AF_CONTEXT_FULL_HISTORY_FORBIDDEN", + "fork_turns=all is mechanically forbidden; use a clean self-contained packet" + ); + } + assertFloor( + delegation.context_mode === "clean" && delegation.fork_turns === "none", + "AF_CONTEXT_NOT_CLEAN", + "context_mode must be clean and fork_turns must be none" + ); + assertFloor(delegation.sandbox === "read-only", "AF_AUTHORITY_TOO_BROAD", "AF-G0 workers must use the read-only sandbox"); + assertFloor(ALLOWED_MODELS.includes(delegation.model), "AF_MODEL_FORBIDDEN", `Model is outside the GPT-5.6 allowlist: ${delegation.model}`); + assertFloor( + ALLOWED_REASONING_EFFORTS.includes(delegation.reasoning_effort), + "AF_REASONING_FORBIDDEN", + `Unsupported reasoning effort: ${delegation.reasoning_effort}` + ); + + const maxDepth = boundedInteger(delegation.max_depth, "delegation.max_depth", 1, FLOOR_LIMITS.maxDepth); + const maxConcurrency = boundedInteger(delegation.max_concurrency, "delegation.max_concurrency", 1, FLOOR_LIMITS.maxConcurrency); + const maxCycles = boundedInteger(delegation.max_cycles, "delegation.max_cycles", 1, FLOOR_LIMITS.maxCycles); + const maxRuntimeSeconds = boundedInteger(delegation.max_runtime_seconds, "delegation.max_runtime_seconds", 1, FLOOR_LIMITS.maxRuntimeSeconds); + const maxOutputBytes = boundedInteger(delegation.max_output_bytes, "delegation.max_output_bytes", 256, FLOOR_LIMITS.maxOutputBytes); + const maxIncrementalTokens = boundedInteger( + delegation.max_incremental_tokens, + "delegation.max_incremental_tokens", + 1, + FLOOR_LIMITS.maxIncrementalTokens + ); + + const admission = raw.admission; + assertFloor(admission && typeof admission === "object" && !Array.isArray(admission), "AF_SPEC_INVALID", "admission must be an object"); + const allowedStatuses = boundedStringArray(admission.allowed_statuses, "admission.allowed_statuses", { + minimum: 1, + maximum: 3, + bytes: 20 + }); + assertFloor( + allowedStatuses.every((value) => ["FOUND", "NO_FINDING", "BLOCKED"].includes(value)), + "AF_SPEC_INVALID", + "admission.allowed_statuses contains an unknown status" + ); + const allowedFindingCodes = boundedStringArray( + admission.allowed_finding_codes, + "admission.allowed_finding_codes", + { minimum: 1, maximum: 16, bytes: 80 } + ); + assertFloor( + allowedFindingCodes.every((value) => /^[A-Z][A-Z0-9_]{1,79}$/.test(value)), + "AF_SPEC_INVALID", + "admission.allowed_finding_codes must use uppercase machine identifiers" + ); + const requiredOutputTerms = boundedStringArray( + admission.required_output_terms, + "admission.required_output_terms", + { minimum: 0, maximum: 16, bytes: 120 } + ); + const forbiddenOutputTerms = boundedStringArray( + admission.forbidden_output_terms, + "admission.forbidden_output_terms", + { minimum: 0, maximum: 16, bytes: 120 } + ); + const minimumEvidence = boundedInteger(admission.minimum_evidence, "admission.minimum_evidence", 0, 16); + + return { + version: 1, + root_task_id: identifier(raw.root_task_id, "root_task_id"), + root_task: boundedString(raw.root_task, "root_task", 4096), + repository_boundary: boundary, + repository_boundary_input: boundaryInput, + worker: { + id: identifier(worker.id, "worker.id"), + objective: boundedString(worker.objective, "worker.objective", 2048), + allowed_paths: allowedPaths, + evidence_requirements: evidenceRequirements + }, + admission: { + allowed_statuses: allowedStatuses, + allowed_finding_codes: allowedFindingCodes, + required_output_terms: requiredOutputTerms, + forbidden_output_terms: forbiddenOutputTerms, + minimum_evidence: minimumEvidence + }, + delegation: { + context_mode: "clean", + fork_turns: "none", + model: delegation.model, + reasoning_effort: delegation.reasoning_effort, + max_depth: maxDepth, + max_concurrency: maxConcurrency, + max_cycles: maxCycles, + max_runtime_seconds: maxRuntimeSeconds, + max_output_bytes: maxOutputBytes, + max_incremental_tokens: maxIncrementalTokens, + sandbox: "read-only" + } + }; +} + +export async function loadTaskSpec(specPath) { + const absolute = path.resolve(specPath); + let raw; + try { + raw = JSON.parse(await readFile(absolute, "utf8")); + } catch (error) { + if (error instanceof SyntaxError) { + throw new AgentFloorError("AF_JSON_INVALID", `Invalid JSON in ${absolute}`, { cause: error.message }); + } + throw new AgentFloorError("AF_FILE_READ_FAILED", `Cannot read task spec ${absolute}`, { cause: error.message }, 1); + } + const spec = await validateTaskSpec(raw, { specDir: path.dirname(absolute) }); + return { spec, raw, specPath: absolute }; +} diff --git a/src/adapters/codex-delegation/runner.js b/src/adapters/codex-delegation/runner.js new file mode 100644 index 0000000..51e3bcf --- /dev/null +++ b/src/adapters/codex-delegation/runner.js @@ -0,0 +1,529 @@ +import { spawn } from "node:child_process"; +import { access, copyFile, mkdir, mkdtemp, open, readFile, rm, unlink, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { admitWorkerResult } from "./admission.js"; +import { auditCodexEvents } from "./audit.js"; +import { AgentFloorError } from "../../errors.js"; +import { createChildPacket, renderPacketPrompt, renderWorkerInstructions } from "./packet.js"; +import { resolveInside, sha256, writeJsonAtomic } from "../../util.js"; + +const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); +const OUTPUT_SCHEMA = path.join(PROJECT_ROOT, "schemas", "worker-output.schema.json"); +const TOOL_ITEM_TYPES = new Set([ + "command_execution", + "file_change", + "mcp_tool_call", + "web_search", + "browser_action", + "computer_action" +]); + +const CODEX_PROCESS_ENV_ALLOWLIST = Object.freeze([ + "CODEX_HOME", + "HOME", + "LANG", + "LC_ALL", + "LC_CTYPE", + "LOGNAME", + "PATH", + "SHELL", + "SSL_CERT_DIR", + "SSL_CERT_FILE", + "TEMP", + "TERM", + "TMP", + "TMPDIR", + "USER" +]); + +function tomlString(value) { + return JSON.stringify(String(value)); +} + +export class ExecutionGuard { + constructor(limits) { + this.limits = limits; + this.toolItems = new Set(); + this.messageItems = new Set(); + this.turns = 0; + this.cycles = 0; + this.outputBytes = 0; + this.violations = []; + } + + violate(code, message, details = undefined) { + if (!this.violations.some((item) => item.code === code)) { + this.violations.push({ code, message, ...(details === undefined ? {} : { details }) }); + } + } + + observe(event) { + const item = event?.item; + if (event?.type === "turn.started") { + this.turns += 1; + this.cycles += 1; + if (this.cycles > this.limits.max_cycles) { + this.violate("AF_CYCLE_LIMIT_EXCEEDED", "Worker exceeded the model/tool cycle limit", { + actual: this.cycles, + limit: this.limits.max_cycles + }); + } + } + if ((event?.type === "item.started" || event?.type === "item.completed") && item && TOOL_ITEM_TYPES.has(item.type)) { + const key = item.id ?? `${item.type}:${JSON.stringify(item)}`; + if (!this.toolItems.has(key)) { + this.toolItems.add(key); + this.cycles += 1; + if (this.cycles > this.limits.max_cycles) { + this.violate("AF_CYCLE_LIMIT_EXCEEDED", "Worker exceeded the model/tool cycle limit", { + actual: this.cycles, + limit: this.limits.max_cycles + }); + } + } + } + if (event?.type === "item.completed" && item?.type === "agent_message") { + const key = item.id ?? sha256(String(item.text ?? "")); + if (!this.messageItems.has(key)) { + this.messageItems.add(key); + this.outputBytes += Buffer.byteLength(String(item.text ?? ""), "utf8"); + if (this.outputBytes > this.limits.max_output_bytes) { + this.violate("AF_OUTPUT_LIMIT_EXCEEDED", "Worker exceeded the output byte limit", { + actual: this.outputBytes, + limit: this.limits.max_output_bytes + }); + } + } + } + return this.violations.length === 0; + } +} + +function pidIsAlive(pid) { + if (!Number.isSafeInteger(pid) || pid <= 0) return false; + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +async function acquireConcurrencySlot(spec) { + const lockRoot = path.join(os.tmpdir(), "agent-floor-locks", sha256(spec.repository_boundary)); + await mkdir(lockRoot, { recursive: true }); + for (let slot = 1; slot <= spec.delegation.max_concurrency; slot += 1) { + const slotPath = path.join(lockRoot, `slot-${slot}.lock`); + for (let attempt = 0; attempt < 2; attempt += 1) { + let handle = null; + try { + handle = await open(slotPath, "wx", 0o600); + await handle.writeFile(`${JSON.stringify({ + pid: process.pid, + worker: spec.worker.id, + acquired_at: new Date().toISOString() + })}\n`, "utf8"); + await handle.close(); + return { + slot, + release: async () => { + try { + await unlink(slotPath); + } catch (error) { + if (error.code !== "ENOENT") throw error; + } + } + }; + } catch (error) { + if (handle) await handle.close().catch(() => {}); + if (error.code !== "EEXIST") throw error; + let owner = null; + try { + owner = JSON.parse(await readFile(slotPath, "utf8")); + } catch { + owner = null; + } + if (attempt === 0 && owner && !pidIsAlive(owner.pid)) { + await unlink(slotPath).catch(() => {}); + continue; + } + break; + } + } + } + throw new AgentFloorError( + "AF_CONCURRENCY_LIMIT", + `All ${spec.delegation.max_concurrency} governed worker slots are occupied` + ); +} + +export async function resolveCodexBinary(override = undefined) { + const explicit = override ?? process.env.AGENT_FLOOR_CODEX_BIN; + if (explicit) { + const resolved = path.resolve(explicit); + try { + await access(resolved); + return resolved; + } catch (error) { + throw new AgentFloorError("AF_CODEX_NOT_FOUND", `Codex executable not found: ${resolved}`, { cause: error.message }, 1); + } + } + const appBinary = "/Applications/ChatGPT.app/Contents/Resources/codex"; + try { + await access(appBinary); + return appBinary; + } catch { + return "codex"; + } +} + +export function buildChildProcessEnv(source = process.env) { + const requested = String(source.AGENT_FLOOR_CODEX_ENV_ALLOWLIST ?? "") + .split(",") + .map((value) => value.trim()) + .filter(Boolean); + const names = new Set([...CODEX_PROCESS_ENV_ALLOWLIST, ...requested]); + const env = { NO_COLOR: "1" }; + for (const name of names) { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) { + throw new AgentFloorError("AF_ENV_ALLOWLIST_INVALID", `Invalid environment variable name: ${name}`); + } + if (source[name] !== undefined) env[name] = String(source[name]); + } + return env; +} + +export function buildCodexArgs(spec, runtimePaths) { + return [ + "exec", + "--ephemeral", + "--ignore-user-config", + "--ignore-rules", + "--json", + "--skip-git-repo-check", + "--disable", + "multi_agent", + "--disable", + "enable_fanout", + "--disable", + "hooks", + "--disable", + "apps", + "--disable", + "plugins", + "--disable", + "memories", + "--disable", + "browser_use", + "--disable", + "computer_use", + "--disable", + "image_generation", + "-C", + runtimePaths.workspace, + "-s", + "read-only", + "-m", + spec.delegation.model, + "-c", + `model_reasoning_effort=${tomlString(spec.delegation.reasoning_effort)}`, + "-c", + "agents.max_threads=1", + "-c", + "agents.max_depth=0", + "-c", + "approval_policy=\"never\"", + "-c", + "allow_login_shell=false", + "-c", + "project_doc_max_bytes=0", + "-c", + "project_doc_fallback_filenames=[]", + "-c", + "shell_environment_policy.inherit=\"core\"", + "-c", + "shell_environment_policy.ignore_default_excludes=false", + "-c", + "shell_environment_policy.experimental_use_profile=false", + "-c", + "shell_environment_policy.exclude=[\"*KEY*\",\"*SECRET*\",\"*TOKEN*\",\"*PASSWORD*\",\"*CREDENTIAL*\",\"*AUTH*\"]", + "-c", + `model_instructions_file=${tomlString(runtimePaths.instructions)}`, + "--output-schema", + OUTPUT_SCHEMA, + "-o", + runtimePaths.output, + "-" + ]; +} + +async function createSourceProjection(spec, projectionRoot) { + await mkdir(projectionRoot, { recursive: true }); + for (const relativePath of spec.worker.allowed_paths) { + const source = await resolveInside(spec.repository_boundary, relativePath); + const destination = path.join(projectionRoot, source.relative); + await mkdir(path.dirname(destination), { recursive: true }); + await copyFile(source.absolute, destination); + } +} + +function extractLastAgentMessage(events) { + for (let index = events.length - 1; index >= 0; index -= 1) { + const event = events[index]; + if (event?.type === "item.completed" && event?.item?.type === "agent_message") { + return String(event.item.text ?? ""); + } + } + return null; +} + +async function executeCodex({ command, args, cwd, prompt, guard, timeoutSeconds }) { + return new Promise((resolve, reject) => { + let child; + try { + child = spawn(command, args, { + cwd, + stdio: ["pipe", "pipe", "pipe"], + env: buildChildProcessEnv() + }); + } catch (error) { + reject(new AgentFloorError("AF_CHILD_SPAWN_FAILED", `Could not start Codex: ${error.message}`, undefined, 1)); + return; + } + + const events = []; + const eventLines = []; + let stdoutBuffer = ""; + let stderr = ""; + let eventBytes = 0; + let terminated = false; + let forceKillTimer = null; + + const terminate = () => { + if (terminated) return; + terminated = true; + child.kill("SIGTERM"); + forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 1000); + }; + + const processLine = (rawLine) => { + const line = rawLine.trim(); + if (!line) return; + eventBytes += Buffer.byteLength(line, "utf8") + 1; + if (eventBytes > 16 * 1024 * 1024) { + guard.violate("AF_EVENT_LOG_LIMIT_EXCEEDED", "Codex event stream exceeded 16 MiB"); + terminate(); + return; + } + let event; + try { + event = JSON.parse(line); + } catch (error) { + guard.violate("AF_CHILD_JSONL_INVALID", "Codex emitted malformed JSONL", { cause: error.message }); + terminate(); + return; + } + eventLines.push(line); + events.push(event); + if (!guard.observe(event)) terminate(); + }; + + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (chunk) => { + stdoutBuffer += chunk; + let newline; + while ((newline = stdoutBuffer.indexOf("\n")) >= 0) { + const line = stdoutBuffer.slice(0, newline); + stdoutBuffer = stdoutBuffer.slice(newline + 1); + processLine(line); + } + }); + child.stderr.setEncoding("utf8"); + child.stderr.on("data", (chunk) => { + if (Buffer.byteLength(stderr, "utf8") < 128 * 1024) stderr += chunk; + }); + child.on("error", (error) => { + reject(new AgentFloorError("AF_CHILD_SPAWN_FAILED", `Codex process error: ${error.message}`, undefined, 1)); + }); + + const timeout = setTimeout(() => { + guard.violate("AF_RUNTIME_LIMIT_EXCEEDED", "Worker exceeded the wall-clock runtime limit", { + limit_seconds: timeoutSeconds + }); + terminate(); + }, timeoutSeconds * 1000); + + child.on("close", (exitCode, signal) => { + clearTimeout(timeout); + if (forceKillTimer) clearTimeout(forceKillTimer); + if (stdoutBuffer.trim()) processLine(stdoutBuffer); + resolve({ + exitCode: exitCode ?? 1, + signal: signal ?? null, + events, + eventText: eventLines.length ? `${eventLines.join("\n")}\n` : "", + stderr + }); + }); + + child.stdin.on("error", () => {}); + child.stdin.end(prompt); + }); +} + +export async function runGovernedChild({ spec, outputDir, codexBinary = undefined }) { + const out = path.resolve(outputDir); + await mkdir(out, { recursive: true }); + const packetRecord = await createChildPacket(spec); + const temporary = await mkdtemp(path.join(os.tmpdir(), "agent-floor-run-")); + const instructionsPath = path.join(temporary, "worker-instructions.md"); + const outputPath = path.join(temporary, "worker-output.json"); + const projectionPath = path.join(temporary, "authority-projection"); + await createSourceProjection(spec, projectionPath); + await writeFile(instructionsPath, `${renderWorkerInstructions()}\n`, "utf8"); + const command = await resolveCodexBinary(codexBinary); + const args = buildCodexArgs(spec, { + instructions: instructionsPath, + output: outputPath, + workspace: projectionPath + }); + const guard = new ExecutionGuard(spec.delegation); + const slot = await acquireConcurrencySlot(spec); + const startedAt = new Date().toISOString(); + let execution; + try { + execution = await executeCodex({ + command, + args, + cwd: projectionPath, + prompt: renderPacketPrompt(packetRecord), + guard, + timeoutSeconds: spec.delegation.max_runtime_seconds + }); + + let outputText = null; + try { + outputText = await readFile(outputPath, "utf8"); + } catch { + outputText = extractLastAgentMessage(execution.events); + } + let output = null; + if (outputText) { + try { + output = JSON.parse(outputText); + } catch (error) { + guard.violate("AF_OUTPUT_JSON_INVALID", "Worker final output is not valid JSON", { cause: error.message }); + } + } else { + guard.violate("AF_OUTPUT_MISSING", "Worker did not produce a final output object"); + } + const outputBytes = Buffer.byteLength(outputText ?? "", "utf8"); + if (outputBytes > spec.delegation.max_output_bytes) { + guard.violate("AF_OUTPUT_LIMIT_EXCEEDED", "Worker final output exceeded the byte limit", { + actual: outputBytes, + limit: spec.delegation.max_output_bytes + }); + } + + let audit = null; + try { + audit = auditCodexEvents(execution.events, { contextMode: "clean" }); + } catch (error) { + guard.violate(error.code ?? "AF_USAGE_MISSING", error.message, error.details); + } + const admission = await admitWorkerResult({ + spec, + packetRecord, + audit, + output, + runtime: { + exitCode: execution.exitCode, + outputBytes, + violations: guard.violations + } + }); + const finishedAt = new Date().toISOString(); + const runRecord = { + protocol: "agent-floor/run/1", + started_at: startedAt, + finished_at: finishedAt, + lineage: packetRecord.lineage, + context_enforcement: { + process_boundary: true, + ephemeral: true, + ignore_user_config: true, + ignore_rules: true, + project_docs_bytes: 0, + inherited_turns: 0, + multi_agent_tools: "disabled", + child_spawn_depth: 0, + process_environment: "allowlisted", + shell_environment: "core-with-secret-pattern-exclusions" + }, + authority: { + repository_boundary: spec.repository_boundary, + execution_projection: "run-local allowlisted copy", + sandbox: "read-only", + allowed_paths: spec.worker.allowed_paths + }, + limits: spec.delegation, + concurrency_slot: slot.slot, + command: { + executable: command, + arguments: args.map((argument) => { + if (argument === outputPath) return ""; + if (argument === projectionPath) return ""; + if (argument.includes(instructionsPath)) return "model_instructions_file="; + return argument; + }) + }, + process: { + exit_code: execution.exitCode, + signal: execution.signal, + cycles: guard.cycles, + output_bytes: outputBytes + }, + audit, + admission + }; + + await writeJsonAtomic(path.join(out, "packet.json"), packetRecord); + await writeFile(path.join(out, "events.jsonl"), execution.eventText, "utf8"); + await writeFile(path.join(out, "stderr.log"), execution.stderr, "utf8"); + if (output) await writeJsonAtomic(path.join(out, "worker-output.json"), output); + await writeJsonAtomic(path.join(out, "admission.json"), admission); + await writeJsonAtomic(path.join(out, "run.json"), runRecord); + return runRecord; + } finally { + await slot.release(); + await rm(temporary, { recursive: true, force: true }); + } +} + +export async function inspectCodexSurface(codexBinary = undefined) { + const command = await resolveCodexBinary(codexBinary); + const run = (args) => + new Promise((resolve) => { + const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] }); + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk) => (stdout += chunk)); + child.stderr.on("data", (chunk) => (stderr += chunk)); + child.on("close", (code) => resolve({ code, stdout, stderr })); + child.on("error", (error) => resolve({ code: 1, stdout, stderr: error.message })); + }); + const [version, help] = await Promise.all([run(["--version"]), run(["exec", "--help"])]); + const requiredFlags = ["--ephemeral", "--ignore-user-config", "--ignore-rules", "--json", "--output-schema"]; + const flags = Object.fromEntries(requiredFlags.map((flag) => [flag, help.stdout.includes(flag)])); + return { + executable: command, + version: version.stdout.trim() || version.stderr.trim(), + exec_help_exit_code: help.code, + required_flags: flags, + result: version.code === 0 && help.code === 0 && Object.values(flags).every(Boolean) ? "READY" : "NOT_READY" + }; +} diff --git a/src/admission.js b/src/admission.js index 35054c0..cc681e4 100644 --- a/src/admission.js +++ b/src/admission.js @@ -1,206 +1,2 @@ -import { readFile } from "node:fs/promises"; -import { resolveInside, normalizeLine, sha256 } from "./util.js"; - -function rejection(code, message, details = undefined) { - return { code, message, ...(details === undefined ? {} : { details }) }; -} - -function validateOutputShape(output) { - const reasons = []; - if (!output || typeof output !== "object" || Array.isArray(output)) { - return [rejection("AF_OUTPUT_INVALID", "Worker output must be an object")]; - } - if (typeof output.finding_code !== "string" || !/^[A-Z][A-Z0-9_]{1,79}$/.test(output.finding_code)) { - reasons.push(rejection("AF_OUTPUT_INVALID", "finding_code must be an uppercase machine identifier")); - } - if (!["FOUND", "NO_FINDING", "BLOCKED"].includes(output.status)) { - reasons.push(rejection("AF_OUTPUT_INVALID", "status must be FOUND, NO_FINDING, or BLOCKED")); - } - if (typeof output.finding !== "string" || output.finding.trim().length === 0) { - reasons.push(rejection("AF_OUTPUT_INVALID", "finding must be a non-empty string")); - } - if (typeof output.summary !== "string" || output.summary.trim().length === 0) { - reasons.push(rejection("AF_OUTPUT_INVALID", "summary must be a non-empty string")); - } - if (!Array.isArray(output.evidence)) { - reasons.push(rejection("AF_OUTPUT_INVALID", "evidence must be an array")); - } - if (!["ADMIT", "REJECT"].includes(output.recommendation)) { - reasons.push(rejection("AF_OUTPUT_INVALID", "recommendation must be ADMIT or REJECT")); - } - if (output.status === "FOUND" && Array.isArray(output.evidence) && output.evidence.length === 0) { - reasons.push(rejection("AF_EVIDENCE_MISSING", "FOUND results require at least one evidence reference")); - } - return reasons; -} - -async function verifyEvidence(spec, evidence) { - if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) { - throw rejection("AF_EVIDENCE_INVALID", "Evidence entry must be an object"); - } - if (!spec.worker.allowed_paths.includes(evidence.path)) { - throw rejection("AF_EVIDENCE_OUTSIDE_AUTHORITY", `Evidence path was not granted to the worker: ${evidence.path}`); - } - if (!Number.isSafeInteger(evidence.line) || evidence.line < 1) { - throw rejection("AF_EVIDENCE_LINE_INVALID", "Evidence line must be a positive integer", { line: evidence.line }); - } - if (typeof evidence.claim !== "string" || evidence.claim.trim().length === 0) { - throw rejection("AF_EVIDENCE_CLAIM_INVALID", "Evidence claim must be a non-empty string"); - } - if (typeof evidence.excerpt !== "string" || normalizeLine(evidence.excerpt).length < 4) { - throw rejection("AF_EVIDENCE_EXCERPT_INVALID", "Evidence excerpt must contain at least four normalized characters"); - } - let resolved; - try { - resolved = await resolveInside(spec.repository_boundary, evidence.path); - } catch (error) { - throw rejection(error.code ?? "AF_EVIDENCE_FILE_MISSING", error.message, error.details); - } - const text = await readFile(resolved.absolute, "utf8"); - const lines = text.split(/\r?\n/); - if (evidence.line > lines.length) { - throw rejection("AF_EVIDENCE_LINE_MISSING", `Evidence line ${evidence.line} does not exist in ${resolved.relative}`, { lineCount: lines.length }); - } - const actual = lines[evidence.line - 1]; - const normalizedActual = normalizeLine(actual); - const normalizedExcerpt = normalizeLine(evidence.excerpt); - if (!normalizedActual.includes(normalizedExcerpt)) { - throw rejection( - "AF_EVIDENCE_EXCERPT_MISMATCH", - `Evidence excerpt does not match ${resolved.relative}:${evidence.line}`, - { expectedExcerpt: normalizedExcerpt, actualLine: normalizedActual } - ); - } - return { - reference: `${resolved.relative}:${evidence.line}`, - path: resolved.relative, - line: evidence.line, - claim: evidence.claim.trim(), - excerpt: normalizedExcerpt, - evidence_sha256: sha256(`${resolved.relative}:${evidence.line}:${normalizedActual}`) - }; -} - -export async function admitWorkerResult({ spec, packetRecord, audit, output, runtime = {} }) { - const reasons = validateOutputShape(output); - const evidenceRecords = []; - if (output && typeof output === "object") { - if (!spec.admission.allowed_statuses.includes(output.status)) { - reasons.push( - rejection("AF_ADMISSION_STATUS_FORBIDDEN", `Worker status is outside the parent admission policy: ${output.status}`) - ); - } - if (!spec.admission.allowed_finding_codes.includes(output.finding_code)) { - reasons.push( - rejection("AF_FINDING_CODE_FORBIDDEN", `Finding code is outside the parent admission policy: ${output.finding_code}`) - ); - } - const evidenceClaims = Array.isArray(output.evidence) - ? output.evidence.map((item) => item?.claim ?? "").join(" ") - : ""; - const semanticText = normalizeLine(`${output.finding ?? ""} ${output.summary ?? ""} ${evidenceClaims}`).toLowerCase(); - for (const term of spec.admission.required_output_terms) { - if (!semanticText.includes(normalizeLine(term).toLowerCase())) { - reasons.push(rejection("AF_REQUIRED_TERM_MISSING", `Required admission term is absent: ${term}`)); - } - } - for (const term of spec.admission.forbidden_output_terms) { - if (semanticText.includes(normalizeLine(term).toLowerCase())) { - reasons.push(rejection("AF_FORBIDDEN_TERM_PRESENT", `Forbidden admission term is present: ${term}`)); - } - } - if (Array.isArray(output.evidence) && output.evidence.length < spec.admission.minimum_evidence) { - reasons.push( - rejection("AF_EVIDENCE_MINIMUM", "Worker supplied fewer evidence items than the parent admission policy requires", { - actual: output.evidence.length, - minimum: spec.admission.minimum_evidence - }) - ); - } - } - - for (const source of packetRecord?.packet?.authority?.source_manifest ?? []) { - try { - const resolved = await resolveInside(spec.repository_boundary, source.path); - const bytes = await readFile(resolved.absolute); - const currentHash = sha256(bytes); - if (bytes.byteLength !== source.bytes || currentHash !== source.sha256) { - reasons.push( - rejection("AF_SOURCE_CHANGED", `Authority source changed after packet creation: ${source.path}`, { - packet_sha256: source.sha256, - current_sha256: currentHash, - packet_bytes: source.bytes, - current_bytes: bytes.byteLength - }) - ); - } - } catch (error) { - reasons.push(rejection(error.code ?? "AF_SOURCE_CHANGED", error.message, error.details)); - } - } - - if (runtime.exitCode !== undefined && runtime.exitCode !== 0) { - reasons.push(rejection("AF_CHILD_EXIT_NONZERO", `Child process exited with code ${runtime.exitCode}`)); - } - for (const violation of runtime.violations ?? []) { - reasons.push(rejection(violation.code, violation.message, violation.details)); - } - if (!audit) { - reasons.push(rejection("AF_USAGE_MISSING", "Request-local usage audit is missing")); - } else { - if (audit.context_mode !== "clean" || audit.full_history_detected) { - reasons.push(rejection("AF_CONTEXT_NOT_CLEAN", "A full-history or non-clean run cannot be admitted")); - } - if (audit.incremental_tokens > spec.delegation.max_incremental_tokens) { - reasons.push( - rejection("AF_TOKEN_LIMIT_EXCEEDED", "Incremental token usage exceeded the worker budget", { - actual: audit.incremental_tokens, - limit: spec.delegation.max_incremental_tokens - }) - ); - } - } - if (runtime.outputBytes !== undefined && runtime.outputBytes > spec.delegation.max_output_bytes) { - reasons.push( - rejection("AF_OUTPUT_LIMIT_EXCEEDED", "Worker output exceeded the byte limit", { - actual: runtime.outputBytes, - limit: spec.delegation.max_output_bytes - }) - ); - } - if (output?.status === "BLOCKED") { - reasons.push(rejection("AF_WORKER_BLOCKED", "Blocked worker results are not admissible")); - } - if (Array.isArray(output?.evidence)) { - for (const evidence of output.evidence) { - try { - evidenceRecords.push(await verifyEvidence(spec, evidence)); - } catch (reason) { - reasons.push(reason?.code ? reason : rejection("AF_EVIDENCE_INVALID", String(reason))); - } - } - } - - const result = reasons.length === 0 ? "ADMITTED" : "REJECTED"; - return { - protocol: "agent-floor/admission/1", - worker: spec.worker.id, - parent: spec.root_task_id, - context_mode: audit?.context_mode ?? "unknown", - packet_sha256: packetRecord.packet_sha256, - model: spec.delegation.model, - reasoning_effort: spec.delegation.reasoning_effort, - model_calls: audit?.model_calls ?? 0, - incremental_tokens: audit?.incremental_tokens ?? null, - cached_input_tokens: audit?.cached_input_tokens ?? null, - fresh_input_tokens: audit?.fresh_input_tokens ?? null, - output_tokens: audit?.output_tokens ?? null, - result, - finding: output?.finding ?? null, - finding_code: output?.finding_code ?? null, - target_recommendation: output?.recommendation ?? null, - evidence: evidenceRecords.map((record) => record.reference), - evidence_records: evidenceRecords, - rejection_reasons: reasons - }; -} +// Historical import surface. The implementation is CINT Adapter 01. +export * from "./adapters/codex-delegation/admission.js"; diff --git a/src/audit.js b/src/audit.js index 7ec1fb7..0fe009b 100644 --- a/src/audit.js +++ b/src/audit.js @@ -1,209 +1,2 @@ -import { readFile } from "node:fs/promises"; -import { AgentFloorError, assertFloor } from "./errors.js"; -import { asFiniteInteger, canonicalJson, parseJsonLines, sha256 } from "./util.js"; - -function normalizeUsage(raw, label) { - assertFloor(raw && typeof raw === "object", "AF_USAGE_MALFORMED", `${label} is missing`); - const input = asFiniteInteger(raw.input_tokens ?? 0, `${label}.input_tokens`); - const cached = asFiniteInteger(raw.cached_input_tokens ?? 0, `${label}.cached_input_tokens`); - const output = asFiniteInteger(raw.output_tokens ?? 0, `${label}.output_tokens`); - const reasoning = asFiniteInteger(raw.reasoning_output_tokens ?? 0, `${label}.reasoning_output_tokens`); - assertFloor(cached <= input, "AF_USAGE_MALFORMED", `${label} cached input exceeds total input`); - const computedTotal = input + output; - if (raw.total_tokens !== undefined && raw.total_tokens !== null) { - const suppliedTotal = asFiniteInteger(raw.total_tokens, `${label}.total_tokens`); - assertFloor( - suppliedTotal === computedTotal, - "AF_USAGE_MALFORMED", - `${label} total_tokens does not equal input_tokens + output_tokens`, - { suppliedTotal, computedTotal } - ); - } - return { - input_tokens: input, - cached_input_tokens: cached, - fresh_input_tokens: input - cached, - output_tokens: output, - reasoning_output_tokens: reasoning, - total_tokens: computedTotal - }; -} - -function sumUsage(records) { - return records.reduce( - (sum, record) => ({ - input_tokens: sum.input_tokens + record.input_tokens, - cached_input_tokens: sum.cached_input_tokens + record.cached_input_tokens, - fresh_input_tokens: sum.fresh_input_tokens + record.fresh_input_tokens, - output_tokens: sum.output_tokens + record.output_tokens, - reasoning_output_tokens: sum.reasoning_output_tokens + record.reasoning_output_tokens, - total_tokens: sum.total_tokens + record.total_tokens - }), - { - input_tokens: 0, - cached_input_tokens: 0, - fresh_input_tokens: 0, - output_tokens: 0, - reasoning_output_tokens: 0, - total_tokens: 0 - } - ); -} - -export function auditCodexEvents(events, options = {}) { - assertFloor(Array.isArray(events), "AF_USAGE_MALFORMED", "Codex events must be an array"); - const usages = []; - let failed = false; - let threadId = null; - for (const event of events) { - if (event?.type === "thread.started") threadId = event.thread_id ?? null; - if (event?.type === "turn.failed" || event?.type === "error") failed = true; - if (event?.type === "turn.completed" && event.usage) { - usages.push(normalizeUsage(event.usage, `turn.completed[${usages.length}]`)); - } - } - assertFloor(usages.length > 0, "AF_USAGE_MISSING", "No turn.completed usage events were found"); - const total = sumUsage(usages); - return { - source: "codex-exec-jsonl", - thread_id: threadId, - context_mode: options.contextMode ?? "clean", - full_history_detected: false, - model_calls: usages.length, - input_tokens: total.input_tokens, - cached_input_tokens: total.cached_input_tokens, - fresh_input_tokens: total.fresh_input_tokens, - output_tokens: total.output_tokens, - reasoning_output_tokens: total.reasoning_output_tokens, - incremental_tokens: total.total_tokens, - raw_cumulative_tokens: null, - accounting_status: failed ? "RUN_FAILED_WITH_USAGE" : "REQUEST_LOCAL", - duplicate_usage_events: 0 - }; -} - -function totalTokenValue(event) { - const value = event?.payload?.info?.total_token_usage?.total_tokens; - return Number.isSafeInteger(value) && value >= 0 ? value : null; -} - -function isBoundary(event, worker) { - if (event?.type === "agent_floor.boundary") { - return !worker || event?.payload?.worker === worker; - } - return event?.type === "event_msg" && event?.payload?.type === "agent_floor.boundary" && (!worker || event?.payload?.worker === worker); -} - -function detectForkTurns(events) { - for (const event of events) { - if (event?.type !== "session_meta") continue; - const candidates = [ - event?.payload?.fork_turns, - event?.payload?.source?.fork_turns, - event?.payload?.metadata?.fork_turns - ]; - const found = candidates.find((value) => value !== undefined); - if (found !== undefined) return found; - } - return null; -} - -export function auditRolloutEvents(events, options = {}) { - assertFloor(Array.isArray(events), "AF_USAGE_MALFORMED", "Rollout events must be an array"); - let boundaryIndex = -1; - if (options.after) { - boundaryIndex = events.findIndex((event) => typeof event?.timestamp === "string" && event.timestamp >= options.after); - if (boundaryIndex >= 0) boundaryIndex -= 1; - } else { - boundaryIndex = events.findIndex((event) => isBoundary(event, options.worker)); - } - assertFloor( - boundaryIndex >= 0, - "AF_USAGE_BOUNDARY_MISSING", - "A worker boundary is required; refusing to attribute the entire inherited rollout" - ); - - let baseline = null; - for (let index = 0; index <= boundaryIndex; index += 1) { - const value = totalTokenValue(events[index]); - if (value !== null) baseline = value; - } - - const usageRecords = []; - const fingerprints = new Set(); - let duplicates = 0; - let rawCumulative = baseline; - for (let index = boundaryIndex + 1; index < events.length; index += 1) { - const event = events[index]; - const rawLast = event?.payload?.info?.last_token_usage; - const cumulative = totalTokenValue(event); - if (cumulative !== null && (rawCumulative === null || cumulative > rawCumulative)) { - rawCumulative = cumulative; - } - if (!rawLast) continue; - const fingerprint = sha256( - canonicalJson({ timestamp: event.timestamp ?? null, last: rawLast, cumulative }) - ); - if (fingerprints.has(fingerprint)) { - duplicates += 1; - continue; - } - fingerprints.add(fingerprint); - usageRecords.push(normalizeUsage(rawLast, `last_token_usage[${usageRecords.length}]`)); - } - assertFloor(usageRecords.length > 0, "AF_USAGE_MISSING", "No request-local last_token_usage records were found after the boundary"); - const total = sumUsage(usageRecords); - const cumulativeDelta = baseline === null || rawCumulative === null ? null : rawCumulative - baseline; - const consistent = cumulativeDelta === null ? null : cumulativeDelta === total.total_tokens; - const forkTurns = detectForkTurns(events); - const fullHistory = forkTurns === "all"; - - return { - source: "codex-rollout-token-count", - context_mode: fullHistory ? "full_history" : "unknown", - fork_turns: forkTurns, - full_history_detected: fullHistory, - boundary_index: boundaryIndex, - model_calls: usageRecords.length, - input_tokens: total.input_tokens, - cached_input_tokens: total.cached_input_tokens, - fresh_input_tokens: total.fresh_input_tokens, - output_tokens: total.output_tokens, - reasoning_output_tokens: total.reasoning_output_tokens, - incremental_tokens: total.total_tokens, - baseline_cumulative_tokens: baseline, - raw_cumulative_tokens: rawCumulative, - cumulative_delta_tokens: cumulativeDelta, - cumulative_delta_matches_incremental: consistent, - naive_overstatement_tokens: - rawCumulative === null ? null : rawCumulative - total.total_tokens, - naive_overstatement_factor: - rawCumulative === null ? null : Number((rawCumulative / total.total_tokens).toFixed(3)), - accounting_status: consistent === false ? "CUMULATIVE_MISMATCH" : "INCREMENTAL_VERIFIED", - duplicate_usage_events: duplicates - }; -} - -export async function auditTraceFile(tracePath, options = {}) { - let text; - try { - text = await readFile(tracePath, "utf8"); - } catch (error) { - throw new AgentFloorError("AF_FILE_READ_FAILED", `Cannot read trace ${tracePath}`, { cause: error.message }, 1); - } - const events = parseJsonLines(text, tracePath); - const kind = options.kind ?? inferTraceKind(events); - if (kind === "codex") return auditCodexEvents(events, options); - if (kind === "rollout") return auditRolloutEvents(events, options); - throw new AgentFloorError("AF_TRACE_KIND_INVALID", `Unsupported trace kind: ${kind}`); -} - -export function inferTraceKind(events) { - if (events.some((event) => event?.type === "session_meta" || event?.type === "event_msg" || event?.type === "agent_floor.boundary")) { - return "rollout"; - } - if (events.some((event) => event?.type === "thread.started" || event?.type === "turn.completed")) { - return "codex"; - } - throw new AgentFloorError("AF_TRACE_KIND_UNKNOWN", "Trace does not contain recognized Codex events"); -} +// Historical import surface. The implementation is CINT Adapter 01. +export * from "./adapters/codex-delegation/audit.js"; diff --git a/src/packet.js b/src/packet.js index ccb65eb..c8e833b 100644 --- a/src/packet.js +++ b/src/packet.js @@ -1,115 +1,2 @@ -import { readFile } from "node:fs/promises"; -import path from "node:path"; -import { AgentFloorError } from "./errors.js"; -import { FLOOR_LIMITS } from "./policy.js"; -import { canonicalJson, resolveInside, sha256 } from "./util.js"; - -export async function createChildPacket(spec) { - const sourceManifest = []; - for (const relativePath of spec.worker.allowed_paths) { - const resolved = await resolveInside(spec.repository_boundary, relativePath); - const bytes = await readFile(resolved.absolute); - sourceManifest.push({ - path: resolved.relative, - bytes: bytes.byteLength, - sha256: sha256(bytes) - }); - } - - const packet = { - protocol: "agent-floor/1", - root: { - id: spec.root_task_id, - task: spec.root_task - }, - worker: { - id: spec.worker.id, - objective: spec.worker.objective - }, - lineage: { - parent: spec.root_task_id, - child: spec.worker.id, - depth: 1 - }, - context: { - mode: "clean", - fork_turns: "none", - inherited_turns: 0 - }, - authority: { - repository_boundary: ".", - allowed_paths: spec.worker.allowed_paths, - source_manifest: sourceManifest, - filesystem: "read-only", - network: "denied", - delegation: "denied" - }, - execution: { - model: spec.delegation.model, - reasoning_effort: spec.delegation.reasoning_effort, - max_depth: spec.delegation.max_depth, - max_concurrency: spec.delegation.max_concurrency, - max_cycles: spec.delegation.max_cycles, - max_runtime_seconds: spec.delegation.max_runtime_seconds, - max_output_bytes: spec.delegation.max_output_bytes, - max_incremental_tokens: spec.delegation.max_incremental_tokens - }, - evidence_contract: { - requirements: spec.worker.evidence_requirements, - citation_shape: "repository-relative path + one-based line + exact excerpt + claim", - admission_rule: "Only real in-boundary lines whose normalized text contains the excerpt can be admitted." - }, - admission_policy: spec.admission, - completion_contract: { - format: "worker-output.schema.json", - statuses: ["FOUND", "NO_FINDING", "BLOCKED"], - stop: "Return one schema-valid JSON object, then stop." - } - }; - - const serialized = canonicalJson(packet); - const packetBytes = Buffer.byteLength(serialized, "utf8"); - if (packetBytes > FLOOR_LIMITS.packetBytes) { - throw new AgentFloorError( - "AF_PACKET_TOO_LARGE", - `Child packet is ${packetBytes} bytes; hard limit is ${FLOOR_LIMITS.packetBytes}`, - { packetBytes, limit: FLOOR_LIMITS.packetBytes } - ); - } - const packetSha256 = sha256(serialized); - return { - packet, - packet_bytes: packetBytes, - packet_sha256: packetSha256, - lineage: { - parent: spec.root_task_id, - child: spec.worker.id, - depth: 1, - context_mode: "clean", - packet_sha256: packetSha256 - } - }; -} - -export function renderPacketPrompt(packetRecord) { - return [ - "Execute only the bounded child packet below.", - "Do not infer or request parent conversation history.", - "Return only the JSON object required by the completion contract.", - "", - JSON.stringify(packetRecord.packet, null, 2) - ].join("\n"); -} - -export function renderWorkerInstructions() { - return [ - "# Agent Floor bounded worker", - "", - "You are one read-only child worker operating from a self-contained packet.", - "The packet is the complete task context. No parent transcript exists or may be requested.", - "Do not delegate, spawn agents, use network access, or read outside the declared repository boundary.", - "Use only the allowed paths. Stop when the objective is answered or the evidence is insufficient.", - "Every finding must cite a repository-relative file, a one-based line, an exact excerpt from that line, and a claim.", - "Return exactly one JSON object matching the supplied output schema. Do not wrap it in Markdown." - ].join("\n"); -} +// Historical import surface. The implementation is CINT Adapter 01. +export * from "./adapters/codex-delegation/packet.js"; diff --git a/src/policy.js b/src/policy.js index 8966b99..172d284 100644 --- a/src/policy.js +++ b/src/policy.js @@ -1,259 +1,2 @@ -import { readFile, realpath, stat } from "node:fs/promises"; -import path from "node:path"; -import { AgentFloorError, assertFloor } from "./errors.js"; -import { assertSafeRelativePath, isWithin } from "./util.js"; - -export const FLOOR_LIMITS = Object.freeze({ - packetBytes: 8 * 1024, - maxDepth: 1, - maxConcurrency: 2, - maxCycles: 6, - maxRuntimeSeconds: 600, - maxOutputBytes: 32 * 1024, - maxIncrementalTokens: 2_000_000, - maxAllowedPaths: 32, - maxEvidenceRequirements: 8 -}); - -export const ALLOWED_MODELS = Object.freeze([ - "gpt-5.6", - "gpt-5.6-terra", - "gpt-5.6-sol" -]); - -export const ALLOWED_REASONING_EFFORTS = Object.freeze([ - "minimal", - "low", - "medium", - "high", - "xhigh", - "ultra", - "max" -]); - -function boundedString(value, label, maximum) { - assertFloor(typeof value === "string", "AF_SPEC_INVALID", `${label} must be a string`); - const trimmed = value.trim(); - assertFloor(trimmed.length > 0, "AF_SPEC_INVALID", `${label} cannot be empty`); - assertFloor( - Buffer.byteLength(trimmed, "utf8") <= maximum, - "AF_SPEC_TOO_LARGE", - `${label} exceeds ${maximum} bytes` - ); - return trimmed; -} - -function identifier(value, label) { - const checked = boundedString(value, label, 80); - assertFloor( - /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(checked), - "AF_SPEC_INVALID", - `${label} may contain only letters, numbers, dot, underscore, and hyphen` - ); - return checked; -} - -function boundedInteger(value, label, minimum, maximum) { - assertFloor( - Number.isSafeInteger(value) && value >= minimum && value <= maximum, - "AF_LIMIT_INVALID", - `${label} must be an integer from ${minimum} through ${maximum}`, - { value, minimum, maximum } - ); - return value; -} - -function boundedStringArray(value, label, options = {}) { - const minimum = options.minimum ?? 0; - const maximum = options.maximum ?? 16; - const bytes = options.bytes ?? 160; - assertFloor(Array.isArray(value), "AF_SPEC_INVALID", `${label} must be an array`); - assertFloor( - value.length >= minimum && value.length <= maximum, - "AF_SPEC_INVALID", - `${label} must contain ${minimum}-${maximum} entries` - ); - const checked = value.map((item, index) => boundedString(item, `${label}[${index}]`, bytes)); - assertFloor(new Set(checked).size === checked.length, "AF_SPEC_INVALID", `${label} contains duplicates`); - return checked; -} - -export async function validateTaskSpec(raw, options = {}) { - assertFloor(raw && typeof raw === "object" && !Array.isArray(raw), "AF_SPEC_INVALID", "Task spec must be an object"); - assertFloor(raw.version === 1, "AF_SPEC_VERSION", "Only Agent Floor task spec version 1 is accepted"); - - const specDir = path.resolve(options.specDir ?? process.cwd()); - const boundaryInput = boundedString(raw.repository_boundary, "repository_boundary", 4096); - const boundaryCandidate = path.resolve(specDir, boundaryInput); - let boundary; - try { - boundary = await realpath(boundaryCandidate); - } catch (error) { - throw new AgentFloorError( - "AF_REPOSITORY_MISSING", - `Repository boundary does not exist: ${boundaryCandidate}`, - { cause: error.message } - ); - } - const boundaryStat = await stat(boundary); - assertFloor(boundaryStat.isDirectory(), "AF_REPOSITORY_INVALID", "Repository boundary must be a directory"); - - const worker = raw.worker; - assertFloor(worker && typeof worker === "object" && !Array.isArray(worker), "AF_SPEC_INVALID", "worker must be an object"); - assertFloor(!("children" in worker), "AF_DEPTH_EXCEEDED", "Nested child declarations are forbidden at AF-G0"); - - const allowedPathsInput = worker.allowed_paths; - assertFloor(Array.isArray(allowedPathsInput), "AF_SPEC_INVALID", "worker.allowed_paths must be an array"); - assertFloor( - allowedPathsInput.length > 0 && allowedPathsInput.length <= FLOOR_LIMITS.maxAllowedPaths, - "AF_SPEC_INVALID", - `worker.allowed_paths must contain 1-${FLOOR_LIMITS.maxAllowedPaths} paths` - ); - const allowedPaths = []; - for (const candidate of allowedPathsInput) { - const relative = assertSafeRelativePath(candidate); - const absoluteCandidate = path.resolve(boundary, relative); - let resolved; - try { - resolved = await realpath(absoluteCandidate); - } catch (error) { - throw new AgentFloorError( - "AF_AUTHORITY_FILE_MISSING", - `Allowed path does not exist: ${relative}`, - { cause: error.message } - ); - } - assertFloor(isWithin(boundary, resolved), "AF_PATH_ESCAPE", `Allowed path escapes repository boundary: ${relative}`); - const fileStat = await stat(resolved); - assertFloor(fileStat.isFile(), "AF_PATH_NOT_FILE", `Allowed path is not a regular file: ${relative}`); - allowedPaths.push(relative); - } - assertFloor(new Set(allowedPaths).size === allowedPaths.length, "AF_SPEC_INVALID", "worker.allowed_paths contains duplicates"); - - const requirementsInput = worker.evidence_requirements; - assertFloor(Array.isArray(requirementsInput), "AF_SPEC_INVALID", "worker.evidence_requirements must be an array"); - assertFloor( - requirementsInput.length > 0 && requirementsInput.length <= FLOOR_LIMITS.maxEvidenceRequirements, - "AF_SPEC_INVALID", - `worker.evidence_requirements must contain 1-${FLOOR_LIMITS.maxEvidenceRequirements} entries` - ); - const evidenceRequirements = requirementsInput.map((value, index) => - boundedString(value, `worker.evidence_requirements[${index}]`, 512) - ); - - const delegation = raw.delegation; - assertFloor(delegation && typeof delegation === "object" && !Array.isArray(delegation), "AF_SPEC_INVALID", "delegation must be an object"); - if (delegation.fork_turns === "all") { - throw new AgentFloorError( - "AF_CONTEXT_FULL_HISTORY_FORBIDDEN", - "fork_turns=all is mechanically forbidden; use a clean self-contained packet" - ); - } - assertFloor( - delegation.context_mode === "clean" && delegation.fork_turns === "none", - "AF_CONTEXT_NOT_CLEAN", - "context_mode must be clean and fork_turns must be none" - ); - assertFloor(delegation.sandbox === "read-only", "AF_AUTHORITY_TOO_BROAD", "AF-G0 workers must use the read-only sandbox"); - assertFloor(ALLOWED_MODELS.includes(delegation.model), "AF_MODEL_FORBIDDEN", `Model is outside the GPT-5.6 allowlist: ${delegation.model}`); - assertFloor( - ALLOWED_REASONING_EFFORTS.includes(delegation.reasoning_effort), - "AF_REASONING_FORBIDDEN", - `Unsupported reasoning effort: ${delegation.reasoning_effort}` - ); - - const maxDepth = boundedInteger(delegation.max_depth, "delegation.max_depth", 1, FLOOR_LIMITS.maxDepth); - const maxConcurrency = boundedInteger(delegation.max_concurrency, "delegation.max_concurrency", 1, FLOOR_LIMITS.maxConcurrency); - const maxCycles = boundedInteger(delegation.max_cycles, "delegation.max_cycles", 1, FLOOR_LIMITS.maxCycles); - const maxRuntimeSeconds = boundedInteger(delegation.max_runtime_seconds, "delegation.max_runtime_seconds", 1, FLOOR_LIMITS.maxRuntimeSeconds); - const maxOutputBytes = boundedInteger(delegation.max_output_bytes, "delegation.max_output_bytes", 256, FLOOR_LIMITS.maxOutputBytes); - const maxIncrementalTokens = boundedInteger( - delegation.max_incremental_tokens, - "delegation.max_incremental_tokens", - 1, - FLOOR_LIMITS.maxIncrementalTokens - ); - - const admission = raw.admission; - assertFloor(admission && typeof admission === "object" && !Array.isArray(admission), "AF_SPEC_INVALID", "admission must be an object"); - const allowedStatuses = boundedStringArray(admission.allowed_statuses, "admission.allowed_statuses", { - minimum: 1, - maximum: 3, - bytes: 20 - }); - assertFloor( - allowedStatuses.every((value) => ["FOUND", "NO_FINDING", "BLOCKED"].includes(value)), - "AF_SPEC_INVALID", - "admission.allowed_statuses contains an unknown status" - ); - const allowedFindingCodes = boundedStringArray( - admission.allowed_finding_codes, - "admission.allowed_finding_codes", - { minimum: 1, maximum: 16, bytes: 80 } - ); - assertFloor( - allowedFindingCodes.every((value) => /^[A-Z][A-Z0-9_]{1,79}$/.test(value)), - "AF_SPEC_INVALID", - "admission.allowed_finding_codes must use uppercase machine identifiers" - ); - const requiredOutputTerms = boundedStringArray( - admission.required_output_terms, - "admission.required_output_terms", - { minimum: 0, maximum: 16, bytes: 120 } - ); - const forbiddenOutputTerms = boundedStringArray( - admission.forbidden_output_terms, - "admission.forbidden_output_terms", - { minimum: 0, maximum: 16, bytes: 120 } - ); - const minimumEvidence = boundedInteger(admission.minimum_evidence, "admission.minimum_evidence", 0, 16); - - return { - version: 1, - root_task_id: identifier(raw.root_task_id, "root_task_id"), - root_task: boundedString(raw.root_task, "root_task", 4096), - repository_boundary: boundary, - repository_boundary_input: boundaryInput, - worker: { - id: identifier(worker.id, "worker.id"), - objective: boundedString(worker.objective, "worker.objective", 2048), - allowed_paths: allowedPaths, - evidence_requirements: evidenceRequirements - }, - admission: { - allowed_statuses: allowedStatuses, - allowed_finding_codes: allowedFindingCodes, - required_output_terms: requiredOutputTerms, - forbidden_output_terms: forbiddenOutputTerms, - minimum_evidence: minimumEvidence - }, - delegation: { - context_mode: "clean", - fork_turns: "none", - model: delegation.model, - reasoning_effort: delegation.reasoning_effort, - max_depth: maxDepth, - max_concurrency: maxConcurrency, - max_cycles: maxCycles, - max_runtime_seconds: maxRuntimeSeconds, - max_output_bytes: maxOutputBytes, - max_incremental_tokens: maxIncrementalTokens, - sandbox: "read-only" - } - }; -} - -export async function loadTaskSpec(specPath) { - const absolute = path.resolve(specPath); - let raw; - try { - raw = JSON.parse(await readFile(absolute, "utf8")); - } catch (error) { - if (error instanceof SyntaxError) { - throw new AgentFloorError("AF_JSON_INVALID", `Invalid JSON in ${absolute}`, { cause: error.message }); - } - throw new AgentFloorError("AF_FILE_READ_FAILED", `Cannot read task spec ${absolute}`, { cause: error.message }, 1); - } - const spec = await validateTaskSpec(raw, { specDir: path.dirname(absolute) }); - return { spec, raw, specPath: absolute }; -} +// Historical import surface. The implementation is CINT Adapter 01. +export * from "./adapters/codex-delegation/policy.js"; diff --git a/src/runner.js b/src/runner.js index 2655340..566ddfb 100644 --- a/src/runner.js +++ b/src/runner.js @@ -1,529 +1,2 @@ -import { spawn } from "node:child_process"; -import { access, copyFile, mkdir, mkdtemp, open, readFile, rm, unlink, writeFile } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { admitWorkerResult } from "./admission.js"; -import { auditCodexEvents } from "./audit.js"; -import { AgentFloorError } from "./errors.js"; -import { createChildPacket, renderPacketPrompt, renderWorkerInstructions } from "./packet.js"; -import { resolveInside, sha256, writeJsonAtomic } from "./util.js"; - -const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const OUTPUT_SCHEMA = path.join(PROJECT_ROOT, "schemas", "worker-output.schema.json"); -const TOOL_ITEM_TYPES = new Set([ - "command_execution", - "file_change", - "mcp_tool_call", - "web_search", - "browser_action", - "computer_action" -]); - -const CODEX_PROCESS_ENV_ALLOWLIST = Object.freeze([ - "CODEX_HOME", - "HOME", - "LANG", - "LC_ALL", - "LC_CTYPE", - "LOGNAME", - "PATH", - "SHELL", - "SSL_CERT_DIR", - "SSL_CERT_FILE", - "TEMP", - "TERM", - "TMP", - "TMPDIR", - "USER" -]); - -function tomlString(value) { - return JSON.stringify(String(value)); -} - -export class ExecutionGuard { - constructor(limits) { - this.limits = limits; - this.toolItems = new Set(); - this.messageItems = new Set(); - this.turns = 0; - this.cycles = 0; - this.outputBytes = 0; - this.violations = []; - } - - violate(code, message, details = undefined) { - if (!this.violations.some((item) => item.code === code)) { - this.violations.push({ code, message, ...(details === undefined ? {} : { details }) }); - } - } - - observe(event) { - const item = event?.item; - if (event?.type === "turn.started") { - this.turns += 1; - this.cycles += 1; - if (this.cycles > this.limits.max_cycles) { - this.violate("AF_CYCLE_LIMIT_EXCEEDED", "Worker exceeded the model/tool cycle limit", { - actual: this.cycles, - limit: this.limits.max_cycles - }); - } - } - if ((event?.type === "item.started" || event?.type === "item.completed") && item && TOOL_ITEM_TYPES.has(item.type)) { - const key = item.id ?? `${item.type}:${JSON.stringify(item)}`; - if (!this.toolItems.has(key)) { - this.toolItems.add(key); - this.cycles += 1; - if (this.cycles > this.limits.max_cycles) { - this.violate("AF_CYCLE_LIMIT_EXCEEDED", "Worker exceeded the model/tool cycle limit", { - actual: this.cycles, - limit: this.limits.max_cycles - }); - } - } - } - if (event?.type === "item.completed" && item?.type === "agent_message") { - const key = item.id ?? sha256(String(item.text ?? "")); - if (!this.messageItems.has(key)) { - this.messageItems.add(key); - this.outputBytes += Buffer.byteLength(String(item.text ?? ""), "utf8"); - if (this.outputBytes > this.limits.max_output_bytes) { - this.violate("AF_OUTPUT_LIMIT_EXCEEDED", "Worker exceeded the output byte limit", { - actual: this.outputBytes, - limit: this.limits.max_output_bytes - }); - } - } - } - return this.violations.length === 0; - } -} - -function pidIsAlive(pid) { - if (!Number.isSafeInteger(pid) || pid <= 0) return false; - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -async function acquireConcurrencySlot(spec) { - const lockRoot = path.join(os.tmpdir(), "agent-floor-locks", sha256(spec.repository_boundary)); - await mkdir(lockRoot, { recursive: true }); - for (let slot = 1; slot <= spec.delegation.max_concurrency; slot += 1) { - const slotPath = path.join(lockRoot, `slot-${slot}.lock`); - for (let attempt = 0; attempt < 2; attempt += 1) { - let handle = null; - try { - handle = await open(slotPath, "wx", 0o600); - await handle.writeFile(`${JSON.stringify({ - pid: process.pid, - worker: spec.worker.id, - acquired_at: new Date().toISOString() - })}\n`, "utf8"); - await handle.close(); - return { - slot, - release: async () => { - try { - await unlink(slotPath); - } catch (error) { - if (error.code !== "ENOENT") throw error; - } - } - }; - } catch (error) { - if (handle) await handle.close().catch(() => {}); - if (error.code !== "EEXIST") throw error; - let owner = null; - try { - owner = JSON.parse(await readFile(slotPath, "utf8")); - } catch { - owner = null; - } - if (attempt === 0 && owner && !pidIsAlive(owner.pid)) { - await unlink(slotPath).catch(() => {}); - continue; - } - break; - } - } - } - throw new AgentFloorError( - "AF_CONCURRENCY_LIMIT", - `All ${spec.delegation.max_concurrency} governed worker slots are occupied` - ); -} - -export async function resolveCodexBinary(override = undefined) { - const explicit = override ?? process.env.AGENT_FLOOR_CODEX_BIN; - if (explicit) { - const resolved = path.resolve(explicit); - try { - await access(resolved); - return resolved; - } catch (error) { - throw new AgentFloorError("AF_CODEX_NOT_FOUND", `Codex executable not found: ${resolved}`, { cause: error.message }, 1); - } - } - const appBinary = "/Applications/ChatGPT.app/Contents/Resources/codex"; - try { - await access(appBinary); - return appBinary; - } catch { - return "codex"; - } -} - -export function buildChildProcessEnv(source = process.env) { - const requested = String(source.AGENT_FLOOR_CODEX_ENV_ALLOWLIST ?? "") - .split(",") - .map((value) => value.trim()) - .filter(Boolean); - const names = new Set([...CODEX_PROCESS_ENV_ALLOWLIST, ...requested]); - const env = { NO_COLOR: "1" }; - for (const name of names) { - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) { - throw new AgentFloorError("AF_ENV_ALLOWLIST_INVALID", `Invalid environment variable name: ${name}`); - } - if (source[name] !== undefined) env[name] = String(source[name]); - } - return env; -} - -export function buildCodexArgs(spec, runtimePaths) { - return [ - "exec", - "--ephemeral", - "--ignore-user-config", - "--ignore-rules", - "--json", - "--skip-git-repo-check", - "--disable", - "multi_agent", - "--disable", - "enable_fanout", - "--disable", - "hooks", - "--disable", - "apps", - "--disable", - "plugins", - "--disable", - "memories", - "--disable", - "browser_use", - "--disable", - "computer_use", - "--disable", - "image_generation", - "-C", - runtimePaths.workspace, - "-s", - "read-only", - "-m", - spec.delegation.model, - "-c", - `model_reasoning_effort=${tomlString(spec.delegation.reasoning_effort)}`, - "-c", - "agents.max_threads=1", - "-c", - "agents.max_depth=0", - "-c", - "approval_policy=\"never\"", - "-c", - "allow_login_shell=false", - "-c", - "project_doc_max_bytes=0", - "-c", - "project_doc_fallback_filenames=[]", - "-c", - "shell_environment_policy.inherit=\"core\"", - "-c", - "shell_environment_policy.ignore_default_excludes=false", - "-c", - "shell_environment_policy.experimental_use_profile=false", - "-c", - "shell_environment_policy.exclude=[\"*KEY*\",\"*SECRET*\",\"*TOKEN*\",\"*PASSWORD*\",\"*CREDENTIAL*\",\"*AUTH*\"]", - "-c", - `model_instructions_file=${tomlString(runtimePaths.instructions)}`, - "--output-schema", - OUTPUT_SCHEMA, - "-o", - runtimePaths.output, - "-" - ]; -} - -async function createSourceProjection(spec, projectionRoot) { - await mkdir(projectionRoot, { recursive: true }); - for (const relativePath of spec.worker.allowed_paths) { - const source = await resolveInside(spec.repository_boundary, relativePath); - const destination = path.join(projectionRoot, source.relative); - await mkdir(path.dirname(destination), { recursive: true }); - await copyFile(source.absolute, destination); - } -} - -function extractLastAgentMessage(events) { - for (let index = events.length - 1; index >= 0; index -= 1) { - const event = events[index]; - if (event?.type === "item.completed" && event?.item?.type === "agent_message") { - return String(event.item.text ?? ""); - } - } - return null; -} - -async function executeCodex({ command, args, cwd, prompt, guard, timeoutSeconds }) { - return new Promise((resolve, reject) => { - let child; - try { - child = spawn(command, args, { - cwd, - stdio: ["pipe", "pipe", "pipe"], - env: buildChildProcessEnv() - }); - } catch (error) { - reject(new AgentFloorError("AF_CHILD_SPAWN_FAILED", `Could not start Codex: ${error.message}`, undefined, 1)); - return; - } - - const events = []; - const eventLines = []; - let stdoutBuffer = ""; - let stderr = ""; - let eventBytes = 0; - let terminated = false; - let forceKillTimer = null; - - const terminate = () => { - if (terminated) return; - terminated = true; - child.kill("SIGTERM"); - forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 1000); - }; - - const processLine = (rawLine) => { - const line = rawLine.trim(); - if (!line) return; - eventBytes += Buffer.byteLength(line, "utf8") + 1; - if (eventBytes > 16 * 1024 * 1024) { - guard.violate("AF_EVENT_LOG_LIMIT_EXCEEDED", "Codex event stream exceeded 16 MiB"); - terminate(); - return; - } - let event; - try { - event = JSON.parse(line); - } catch (error) { - guard.violate("AF_CHILD_JSONL_INVALID", "Codex emitted malformed JSONL", { cause: error.message }); - terminate(); - return; - } - eventLines.push(line); - events.push(event); - if (!guard.observe(event)) terminate(); - }; - - child.stdout.setEncoding("utf8"); - child.stdout.on("data", (chunk) => { - stdoutBuffer += chunk; - let newline; - while ((newline = stdoutBuffer.indexOf("\n")) >= 0) { - const line = stdoutBuffer.slice(0, newline); - stdoutBuffer = stdoutBuffer.slice(newline + 1); - processLine(line); - } - }); - child.stderr.setEncoding("utf8"); - child.stderr.on("data", (chunk) => { - if (Buffer.byteLength(stderr, "utf8") < 128 * 1024) stderr += chunk; - }); - child.on("error", (error) => { - reject(new AgentFloorError("AF_CHILD_SPAWN_FAILED", `Codex process error: ${error.message}`, undefined, 1)); - }); - - const timeout = setTimeout(() => { - guard.violate("AF_RUNTIME_LIMIT_EXCEEDED", "Worker exceeded the wall-clock runtime limit", { - limit_seconds: timeoutSeconds - }); - terminate(); - }, timeoutSeconds * 1000); - - child.on("close", (exitCode, signal) => { - clearTimeout(timeout); - if (forceKillTimer) clearTimeout(forceKillTimer); - if (stdoutBuffer.trim()) processLine(stdoutBuffer); - resolve({ - exitCode: exitCode ?? 1, - signal: signal ?? null, - events, - eventText: eventLines.length ? `${eventLines.join("\n")}\n` : "", - stderr - }); - }); - - child.stdin.on("error", () => {}); - child.stdin.end(prompt); - }); -} - -export async function runGovernedChild({ spec, outputDir, codexBinary = undefined }) { - const out = path.resolve(outputDir); - await mkdir(out, { recursive: true }); - const packetRecord = await createChildPacket(spec); - const temporary = await mkdtemp(path.join(os.tmpdir(), "agent-floor-run-")); - const instructionsPath = path.join(temporary, "worker-instructions.md"); - const outputPath = path.join(temporary, "worker-output.json"); - const projectionPath = path.join(temporary, "authority-projection"); - await createSourceProjection(spec, projectionPath); - await writeFile(instructionsPath, `${renderWorkerInstructions()}\n`, "utf8"); - const command = await resolveCodexBinary(codexBinary); - const args = buildCodexArgs(spec, { - instructions: instructionsPath, - output: outputPath, - workspace: projectionPath - }); - const guard = new ExecutionGuard(spec.delegation); - const slot = await acquireConcurrencySlot(spec); - const startedAt = new Date().toISOString(); - let execution; - try { - execution = await executeCodex({ - command, - args, - cwd: projectionPath, - prompt: renderPacketPrompt(packetRecord), - guard, - timeoutSeconds: spec.delegation.max_runtime_seconds - }); - - let outputText = null; - try { - outputText = await readFile(outputPath, "utf8"); - } catch { - outputText = extractLastAgentMessage(execution.events); - } - let output = null; - if (outputText) { - try { - output = JSON.parse(outputText); - } catch (error) { - guard.violate("AF_OUTPUT_JSON_INVALID", "Worker final output is not valid JSON", { cause: error.message }); - } - } else { - guard.violate("AF_OUTPUT_MISSING", "Worker did not produce a final output object"); - } - const outputBytes = Buffer.byteLength(outputText ?? "", "utf8"); - if (outputBytes > spec.delegation.max_output_bytes) { - guard.violate("AF_OUTPUT_LIMIT_EXCEEDED", "Worker final output exceeded the byte limit", { - actual: outputBytes, - limit: spec.delegation.max_output_bytes - }); - } - - let audit = null; - try { - audit = auditCodexEvents(execution.events, { contextMode: "clean" }); - } catch (error) { - guard.violate(error.code ?? "AF_USAGE_MISSING", error.message, error.details); - } - const admission = await admitWorkerResult({ - spec, - packetRecord, - audit, - output, - runtime: { - exitCode: execution.exitCode, - outputBytes, - violations: guard.violations - } - }); - const finishedAt = new Date().toISOString(); - const runRecord = { - protocol: "agent-floor/run/1", - started_at: startedAt, - finished_at: finishedAt, - lineage: packetRecord.lineage, - context_enforcement: { - process_boundary: true, - ephemeral: true, - ignore_user_config: true, - ignore_rules: true, - project_docs_bytes: 0, - inherited_turns: 0, - multi_agent_tools: "disabled", - child_spawn_depth: 0, - process_environment: "allowlisted", - shell_environment: "core-with-secret-pattern-exclusions" - }, - authority: { - repository_boundary: spec.repository_boundary, - execution_projection: "run-local allowlisted copy", - sandbox: "read-only", - allowed_paths: spec.worker.allowed_paths - }, - limits: spec.delegation, - concurrency_slot: slot.slot, - command: { - executable: command, - arguments: args.map((argument) => { - if (argument === outputPath) return ""; - if (argument === projectionPath) return ""; - if (argument.includes(instructionsPath)) return "model_instructions_file="; - return argument; - }) - }, - process: { - exit_code: execution.exitCode, - signal: execution.signal, - cycles: guard.cycles, - output_bytes: outputBytes - }, - audit, - admission - }; - - await writeJsonAtomic(path.join(out, "packet.json"), packetRecord); - await writeFile(path.join(out, "events.jsonl"), execution.eventText, "utf8"); - await writeFile(path.join(out, "stderr.log"), execution.stderr, "utf8"); - if (output) await writeJsonAtomic(path.join(out, "worker-output.json"), output); - await writeJsonAtomic(path.join(out, "admission.json"), admission); - await writeJsonAtomic(path.join(out, "run.json"), runRecord); - return runRecord; - } finally { - await slot.release(); - await rm(temporary, { recursive: true, force: true }); - } -} - -export async function inspectCodexSurface(codexBinary = undefined) { - const command = await resolveCodexBinary(codexBinary); - const run = (args) => - new Promise((resolve) => { - const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] }); - let stdout = ""; - let stderr = ""; - child.stdout.setEncoding("utf8"); - child.stderr.setEncoding("utf8"); - child.stdout.on("data", (chunk) => (stdout += chunk)); - child.stderr.on("data", (chunk) => (stderr += chunk)); - child.on("close", (code) => resolve({ code, stdout, stderr })); - child.on("error", (error) => resolve({ code: 1, stdout, stderr: error.message })); - }); - const [version, help] = await Promise.all([run(["--version"]), run(["exec", "--help"])]); - const requiredFlags = ["--ephemeral", "--ignore-user-config", "--ignore-rules", "--json", "--output-schema"]; - const flags = Object.fromEntries(requiredFlags.map((flag) => [flag, help.stdout.includes(flag)])); - return { - executable: command, - version: version.stdout.trim() || version.stderr.trim(), - exec_help_exit_code: help.code, - required_flags: flags, - result: version.code === 0 && help.code === 0 && Object.values(flags).every(Boolean) ? "READY" : "NOT_READY" - }; -} +// Historical import surface. The implementation is CINT Adapter 01. +export * from "./adapters/codex-delegation/runner.js"; diff --git a/tests/legacy-adapter.test.js b/tests/legacy-adapter.test.js new file mode 100644 index 0000000..37d1d08 --- /dev/null +++ b/tests/legacy-adapter.test.js @@ -0,0 +1,30 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import * as adapter from "../src/adapters/codex-delegation/index.js"; +import * as admission from "../src/admission.js"; +import * as audit from "../src/audit.js"; +import * as packet from "../src/packet.js"; +import * as policy from "../src/policy.js"; +import * as runner from "../src/runner.js"; + +test("historical root exports are exact Adapter 01 compatibility aliases", () => { + assert.equal(admission.admitWorkerResult, adapter.admitWorkerResult); + assert.equal(audit.auditTraceFile, adapter.auditTraceFile); + assert.equal(packet.createChildPacket, adapter.createChildPacket); + assert.equal(policy.validateTaskSpec, adapter.validateTaskSpec); + assert.equal(runner.runGovernedChild, adapter.runGovernedChild); +}); + +test("Adapter 01 declares a zero-authority CINT boundary", () => { + assert.equal(adapter.CODEX_DELEGATION_ADAPTER.id, "cint.adapter.codex-delegation"); + assert.deepEqual(adapter.CODEX_DELEGATION_ADAPTER.authority_boundary, { + may_mint_decision: false, + may_issue_receipt: false, + may_consume_receipt: false, + may_self_admit: false, + may_bypass_revalidation: false, + may_seal_outcome: false + }); +}); + From 3e9bda9f60645561744049112a688331df02b015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Thu, 27 Aug 2026 20:26:29 +0300 Subject: [PATCH 03/16] cint(g3): add canonical decision contracts --- artifacts/cint-r0/gate-ledger.json | 6 + artifacts/cint-r0/gates/CINT-R0.G3.json | 16 ++ docs/cint-r0/02_LEGACY_EXTRACTION.md | 1 - docs/cint-r0/03_DECISION_CONTRACTS.md | 57 +++++++ schemas/cint/authority.schema.json | 60 +++++++ schemas/cint/challenge.schema.json | 49 ++++++ schemas/cint/decision.schema.json | 78 +++++++++ schemas/cint/intent.schema.json | 52 ++++++ schemas/cint/policy.schema.json | 43 +++++ schemas/cint/principal.schema.json | 22 +++ src/cint/authority.js | 110 +++++++++++++ src/cint/canonical.js | 146 ++++++++++++++++ src/cint/challenge.js | 127 ++++++++++++++ src/cint/decision.js | 67 ++++++++ src/cint/index.js | 9 + src/cint/intent.js | 71 ++++++++ src/cint/machine-state.js | 24 +++ src/cint/policy.js | 97 +++++++++++ src/cint/principal.js | 37 +++++ src/cint/state-machine.js | 82 +++++++++ tests/cint-decision.test.js | 210 ++++++++++++++++++++++++ tests/legacy-adapter.test.js | 1 - 22 files changed, 1363 insertions(+), 2 deletions(-) create mode 100644 artifacts/cint-r0/gates/CINT-R0.G3.json create mode 100644 docs/cint-r0/03_DECISION_CONTRACTS.md create mode 100644 schemas/cint/authority.schema.json create mode 100644 schemas/cint/challenge.schema.json create mode 100644 schemas/cint/decision.schema.json create mode 100644 schemas/cint/intent.schema.json create mode 100644 schemas/cint/policy.schema.json create mode 100644 schemas/cint/principal.schema.json create mode 100644 src/cint/authority.js create mode 100644 src/cint/canonical.js create mode 100644 src/cint/challenge.js create mode 100644 src/cint/decision.js create mode 100644 src/cint/index.js create mode 100644 src/cint/intent.js create mode 100644 src/cint/machine-state.js create mode 100644 src/cint/policy.js create mode 100644 src/cint/principal.js create mode 100644 src/cint/state-machine.js create mode 100644 tests/cint-decision.test.js diff --git a/artifacts/cint-r0/gate-ledger.json b/artifacts/cint-r0/gate-ledger.json index d89c541..8536f18 100644 --- a/artifacts/cint-r0/gate-ledger.json +++ b/artifacts/cint-r0/gate-ledger.json @@ -22,6 +22,12 @@ "name": "Module extraction with behavioral equivalence", "status": "PASS", "evidence": "docs/cint-r0/02_LEGACY_EXTRACTION.md" + }, + { + "gate": "CINT-R0.G3", + "name": "CINT contracts and decision engine", + "status": "PASS", + "evidence": "docs/cint-r0/03_DECISION_CONTRACTS.md" } ], "terminal_verdict": null diff --git a/artifacts/cint-r0/gates/CINT-R0.G3.json b/artifacts/cint-r0/gates/CINT-R0.G3.json new file mode 100644 index 0000000..5254b03 --- /dev/null +++ b/artifacts/cint-r0/gates/CINT-R0.G3.json @@ -0,0 +1,16 @@ +{ + "gate": "CINT-R0.G3", + "status": "PASS", + "cint_contract_tests": { + "passed": 12, + "failed": 0 + }, + "legacy_and_adapter_tests": { + "passed": 16, + "failed": 0 + }, + "strict_schemas": 6, + "decision_states": ["ADMIT", "DENY", "REVIEW"], + "decision_execution_authority": "NONE", + "evidence": "docs/cint-r0/03_DECISION_CONTRACTS.md" +} diff --git a/docs/cint-r0/02_LEGACY_EXTRACTION.md b/docs/cint-r0/02_LEGACY_EXTRACTION.md index 2f8fa2f..e4cb999 100644 --- a/docs/cint-r0/02_LEGACY_EXTRACTION.md +++ b/docs/cint-r0/02_LEGACY_EXTRACTION.md @@ -30,4 +30,3 @@ self-admit, bypass revalidation, or seal an outcome. entries from the historical Git object. No CINT decision or execution authority is introduced at this gate. - diff --git a/docs/cint-r0/03_DECISION_CONTRACTS.md b/docs/cint-r0/03_DECISION_CONTRACTS.md new file mode 100644 index 0000000..6a6eddd --- /dev/null +++ b/docs/cint-r0/03_DECISION_CONTRACTS.md @@ -0,0 +1,57 @@ +# CINT-R0 canonical contracts and decision engine + +SI1 CINT now has strict, digest-bound objects for intent, principal, authority, +policy, adapter capability, machine state, counter-intent challenge, decision, +and lifecycle state. Unknown fields, non-JSON values, invalid canonical times, +and tampered record digests fail before adjudication. + +## Decision chain + +```text +explicit request + -> canonical intent and exact action/target/context digests + -> authenticated principal and authority chain + -> active authority, exact grant, time window, and policy binding + -> policy and adapter capability challenge + -> current machine-state binding + -> ADMIT / DENY / REVIEW +``` + +An `ADMIT` decision is only receipt-eligible. Every decision states +`execution_authority: NONE`; no decision object can itself cross the execution +boundary. + +## Counter-intent matrix + +| Condition | Decision | +|---|---| +| Exact current principal, authority, policy, target, action, adapter, and machine-state bindings | `ADMIT` | +| Missing explicit request | `DENY / CINT_SILENT_REQUEST` | +| Undeclared effects | `DENY / CINT_EFFECT_UNDECLARED` | +| Target outside the exact grant | `DENY / CINT_AUTHORITY_ACTION_DENIED` | +| CINT state unavailable | `DENY / CINT_UNAVAILABLE` | +| Consequential adapter lacks required rollback | `DENY / CINT_ROLLBACK_REQUIRED` | +| Material uncertainty remains | `REVIEW / CINT_COUNTER_INTENT_UNRESOLVED` | +| Any sealed input was changed | hard rejection before decision | + +## Strict schemas + +The schema set under `schemas/cint/` uses JSON Schema 2020-12, required fields, +and `additionalProperties: false` for every authority-bearing envelope and +nested binding object. Canonical JSON parsing additionally requires the exact +sorted, whitespace-free byte representation, eliminating duplicate-key and +noncanonical encodings from signed or hashed surfaces. + +## Gate proof + +- 12 CINT contract and decision tests pass; +- 16 preserved legacy and adapter-boundary tests pass; +- unauthorized target, silent request, undeclared effect, unavailable state, + missing rollback, tampered record, and overlong decision lifetime controls + are exercised; +- the state machine rejects undeclared lifecycle transitions; +- historical demo, publication audit, dependency audit, and immutable release + manifest verification remain green. + +Receipt issuance, receipt consumption, and consequential execution remain +absent until later gates. diff --git a/schemas/cint/authority.schema.json b/schemas/cint/authority.schema.json new file mode 100644 index 0000000..5f7453e --- /dev/null +++ b/schemas/cint/authority.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/authority-1.json", + "title": "CINT authority", + "type": "object", + "additionalProperties": false, + "required": [ + "protocol", + "id", + "principal_id", + "issuer_id", + "status", + "epoch", + "grants", + "policy_ids", + "require_rollback", + "issued_at", + "not_before", + "expires_at", + "revoked_at", + "revocation_reason", + "digest" + ], + "properties": { + "protocol": { "const": "cint/authority/1" }, + "id": { "$ref": "#/$defs/id" }, + "principal_id": { "$ref": "#/$defs/id" }, + "issuer_id": { "$ref": "#/$defs/id" }, + "status": { "enum": ["ACTIVE", "REVOKED"] }, + "epoch": { "type": "integer", "minimum": 1 }, + "grants": { + "type": "array", + "minItems": 1, + "maxItems": 32, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["adapter", "type", "target_digest"], + "properties": { + "adapter": { "$ref": "#/$defs/id" }, + "type": { "$ref": "#/$defs/id" }, + "target_digest": { "$ref": "#/$defs/sha256" } + } + } + }, + "policy_ids": { "type": "array", "maxItems": 16, "uniqueItems": true, "items": { "$ref": "#/$defs/id" } }, + "require_rollback": { "type": "boolean" }, + "issued_at": { "type": "string", "format": "date-time" }, + "not_before": { "type": "string", "format": "date-time" }, + "expires_at": { "type": "string", "format": "date-time" }, + "revoked_at": { "type": ["string", "null"], "format": "date-time" }, + "revocation_reason": { "type": ["string", "null"] }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/schemas/cint/challenge.schema.json b/schemas/cint/challenge.schema.json new file mode 100644 index 0000000..f4ac9ba --- /dev/null +++ b/schemas/cint/challenge.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/challenge-1.json", + "title": "CINT counter-intent challenge", + "type": "object", + "additionalProperties": false, + "required": [ + "protocol", + "status", + "checked_at", + "intent_digest", + "principal_digest", + "authority_digest", + "policy_digest", + "adapter_capability_digest", + "machine_state_digest", + "reasons", + "digest" + ], + "properties": { + "protocol": { "const": "cint/challenge/1" }, + "status": { "enum": ["CLEAR", "DENIED", "REVIEW"] }, + "checked_at": { "type": "string", "format": "date-time" }, + "intent_digest": { "$ref": "#/$defs/sha256" }, + "principal_digest": { "$ref": "#/$defs/sha256" }, + "authority_digest": { "$ref": "#/$defs/sha256" }, + "policy_digest": { "$ref": "#/$defs/sha256" }, + "adapter_capability_digest": { "$ref": "#/$defs/sha256" }, + "machine_state_digest": { "$ref": "#/$defs/sha256" }, + "reasons": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["code", "disposition", "message"], + "properties": { + "code": { "$ref": "#/$defs/id" }, + "disposition": { "enum": ["DENY", "REVIEW"] }, + "message": { "type": "string", "minLength": 1 } + } + } + }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/schemas/cint/decision.schema.json b/schemas/cint/decision.schema.json new file mode 100644 index 0000000..a02604c --- /dev/null +++ b/schemas/cint/decision.schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/decision-1.json", + "title": "CINT decision", + "type": "object", + "additionalProperties": false, + "required": [ + "protocol", + "id", + "status", + "issued_at", + "expires_at", + "binding", + "binding_digest", + "challenge_digest", + "reason_codes", + "receipt_eligible", + "execution_authority", + "digest" + ], + "properties": { + "protocol": { "const": "cint/decision/1" }, + "id": { "$ref": "#/$defs/id" }, + "status": { "enum": ["ADMIT", "DENY", "REVIEW"] }, + "issued_at": { "type": "string", "format": "date-time" }, + "expires_at": { "type": "string", "format": "date-time" }, + "binding": { + "type": "object", + "additionalProperties": false, + "required": [ + "intent_digest", + "action_digest", + "target_digest", + "context_digest", + "principal_digest", + "authority_id", + "authority_digest", + "authority_epoch", + "policy_id", + "policy_digest", + "policy_epoch", + "adapter_id", + "adapter_capability_digest", + "machine_state_id", + "machine_state_epoch", + "machine_state_digest" + ], + "properties": { + "intent_digest": { "$ref": "#/$defs/sha256" }, + "action_digest": { "$ref": "#/$defs/sha256" }, + "target_digest": { "$ref": "#/$defs/sha256" }, + "context_digest": { "$ref": "#/$defs/sha256" }, + "principal_digest": { "$ref": "#/$defs/sha256" }, + "authority_id": { "$ref": "#/$defs/id" }, + "authority_digest": { "$ref": "#/$defs/sha256" }, + "authority_epoch": { "type": "integer", "minimum": 1 }, + "policy_id": { "$ref": "#/$defs/id" }, + "policy_digest": { "$ref": "#/$defs/sha256" }, + "policy_epoch": { "type": "integer", "minimum": 1 }, + "adapter_id": { "$ref": "#/$defs/id" }, + "adapter_capability_digest": { "$ref": "#/$defs/sha256" }, + "machine_state_id": { "$ref": "#/$defs/id" }, + "machine_state_epoch": { "type": "integer", "minimum": 1 }, + "machine_state_digest": { "$ref": "#/$defs/sha256" } + } + }, + "binding_digest": { "$ref": "#/$defs/sha256" }, + "challenge_digest": { "$ref": "#/$defs/sha256" }, + "reason_codes": { "type": "array", "items": { "$ref": "#/$defs/id" } }, + "receipt_eligible": { "type": "boolean" }, + "execution_authority": { "const": "NONE" }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/schemas/cint/intent.schema.json b/schemas/cint/intent.schema.json new file mode 100644 index 0000000..42abf16 --- /dev/null +++ b/schemas/cint/intent.schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/intent-1.json", + "title": "CINT intent", + "type": "object", + "additionalProperties": false, + "required": [ + "protocol", + "id", + "principal_id", + "request", + "action", + "declared_effects", + "context", + "uncertainties", + "created_at", + "action_digest", + "target_digest", + "context_digest", + "digest" + ], + "properties": { + "protocol": { "const": "cint/intent/1" }, + "id": { "$ref": "#/$defs/id" }, + "principal_id": { "$ref": "#/$defs/id" }, + "request": { "type": ["string", "null"], "minLength": 1 }, + "action": { + "type": "object", + "additionalProperties": false, + "required": ["adapter", "type", "target", "parameters", "consequence"], + "properties": { + "adapter": { "$ref": "#/$defs/id" }, + "type": { "$ref": "#/$defs/id" }, + "target": {}, + "parameters": {}, + "consequence": { "enum": ["READ_ONLY", "CONSEQUENTIAL"] } + } + }, + "declared_effects": { "type": "array", "maxItems": 32, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }, + "context": {}, + "uncertainties": { "type": "array", "maxItems": 16, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }, + "created_at": { "type": "string", "format": "date-time" }, + "action_digest": { "$ref": "#/$defs/sha256" }, + "target_digest": { "$ref": "#/$defs/sha256" }, + "context_digest": { "$ref": "#/$defs/sha256" }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/schemas/cint/policy.schema.json b/schemas/cint/policy.schema.json new file mode 100644 index 0000000..139f5fc --- /dev/null +++ b/schemas/cint/policy.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/policy-1.json", + "title": "CINT policy snapshot", + "type": "object", + "additionalProperties": false, + "required": [ + "protocol", + "id", + "version", + "status", + "epoch", + "allowed_adapters", + "allowed_action_types", + "denied_action_types", + "require_explicit_request", + "require_declared_effects", + "require_rollback_for_consequential", + "review_on_uncertainty", + "issued_at", + "digest" + ], + "properties": { + "protocol": { "const": "cint/policy/1" }, + "id": { "$ref": "#/$defs/id" }, + "version": { "$ref": "#/$defs/id" }, + "status": { "const": "ACTIVE" }, + "epoch": { "type": "integer", "minimum": 1 }, + "allowed_adapters": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/id" } }, + "allowed_action_types": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/id" } }, + "denied_action_types": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/id" } }, + "require_explicit_request": { "type": "boolean" }, + "require_declared_effects": { "type": "boolean" }, + "require_rollback_for_consequential": { "type": "boolean" }, + "review_on_uncertainty": { "type": "boolean" }, + "issued_at": { "type": "string", "format": "date-time" }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/schemas/cint/principal.schema.json b/schemas/cint/principal.schema.json new file mode 100644 index 0000000..d1f9212 --- /dev/null +++ b/schemas/cint/principal.schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/principal-1.json", + "title": "CINT principal", + "type": "object", + "additionalProperties": false, + "required": ["protocol", "id", "type", "authenticated", "authority_chain", "attributes", "resolved_at", "digest"], + "properties": { + "protocol": { "const": "cint/principal/1" }, + "id": { "$ref": "#/$defs/id" }, + "type": { "enum": ["HUMAN", "SERVICE", "AGENT"] }, + "authenticated": { "type": "boolean" }, + "authority_chain": { "type": "array", "maxItems": 16, "uniqueItems": true, "items": { "$ref": "#/$defs/id" } }, + "attributes": {}, + "resolved_at": { "type": "string", "format": "date-time" }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/src/cint/authority.js b/src/cint/authority.js new file mode 100644 index 0000000..b54b528 --- /dev/null +++ b/src/cint/authority.js @@ -0,0 +1,110 @@ +import { + assertCint, + assertExactKeys, + assertJsonValue, + boundedString, + canonicalDigest, + identifier, + integer, + isoInstant, + sealRecord, + stringArray, + verifySealedRecord +} 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) { + verifySealedRecord(authority, "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 }) { + verifySealedRecord(authority, "authority"); + verifySealedRecord(intent, "intent"); + if (policy) verifySealedRecord(policy, "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/canonical.js b/src/cint/canonical.js new file mode 100644 index 0000000..fb17d7a --- /dev/null +++ b/src/cint/canonical.js @@ -0,0 +1,146 @@ +import { canonicalJson, canonicalize, sha256 } from "../util.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); + return immutableRecord({ ...canonical, digest: canonicalDigest(canonical) }); +} + +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` + ); + return value; +} diff --git a/src/cint/challenge.js b/src/cint/challenge.js new file mode 100644 index 0000000..d016b45 --- /dev/null +++ b/src/cint/challenge.js @@ -0,0 +1,127 @@ +import { + assertCint, + assertExactKeys, + identifier, + isoInstant, + sealRecord, + stringArray, + verifySealedRecord +} 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", "rollback", "interrupt", "outcome_verification"], + [], + "adapter capability" + ); + for (const field of ["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, + rollback: input.rollback, + interrupt: input.interrupt, + outcome_verification: input.outcome_verification + }); +} + +export function runCounterIntentChallenge({ + intent, + principal, + authority, + policy, + adapter_capability, + machine_state, + now +}) { + for (const [label, record] of Object.entries({ + intent, + principal, + authority, + policy, + adapter_capability, + machine_state + })) { + verifySealedRecord(record, label); + } + 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 (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/decision.js b/src/cint/decision.js new file mode 100644 index 0000000..0681bc3 --- /dev/null +++ b/src/cint/decision.js @@ -0,0 +1,67 @@ +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/index.js b/src/cint/index.js new file mode 100644 index 0000000..b2f98c3 --- /dev/null +++ b/src/cint/index.js @@ -0,0 +1,9 @@ +export * from "./authority.js"; +export * from "./canonical.js"; +export * from "./challenge.js"; +export * from "./decision.js"; +export * from "./intent.js"; +export * from "./machine-state.js"; +export * from "./policy.js"; +export * from "./principal.js"; +export * from "./state-machine.js"; diff --git a/src/cint/intent.js b/src/cint/intent.js new file mode 100644 index 0000000..a6ad576 --- /dev/null +++ b/src/cint/intent.js @@ -0,0 +1,71 @@ +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/machine-state.js b/src/cint/machine-state.js new file mode 100644 index 0000000..d164f4a --- /dev/null +++ b/src/cint/machine-state.js @@ -0,0 +1,24 @@ +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/policy.js b/src/cint/policy.js new file mode 100644 index 0000000..43a7197 --- /dev/null +++ b/src/cint/policy.js @@ -0,0 +1,97 @@ +import { + assertCint, + assertExactKeys, + identifier, + integer, + isoInstant, + sealRecord, + stringArray, + verifySealedRecord +} 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 }) { + verifySealedRecord(policy, "policy"); + verifySealedRecord(intent, "intent"); + verifySealedRecord(adapter_capability, "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/principal.js b/src/cint/principal.js new file mode 100644 index 0000000..2ab9275 --- /dev/null +++ b/src/cint/principal.js @@ -0,0 +1,37 @@ +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/state-machine.js b/src/cint/state-machine.js new file mode 100644 index 0000000..eaffc55 --- /dev/null +++ b/src/cint/state-machine.js @@ -0,0 +1,82 @@ +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/tests/cint-decision.test.js b/tests/cint-decision.test.js new file mode 100644 index 0000000..8d300d0 --- /dev/null +++ b/tests/cint-decision.test.js @@ -0,0 +1,210 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + createAdapterCapability, + createAuthorityGrant, + createIntent, + createMachineStateSnapshot, + createPolicySnapshot, + createStateMachine, + decide, + parseCanonicalJson, + resolvePrincipal, + transitionState +} from "../src/cint/index.js"; +import { canonicalJson } from "../src/util.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 TARGET = Object.freeze({ path: "sandbox/target.txt" }); + +function records(overrides = {}) { + const intent = 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, + ...overrides.intent + }); + const principal = resolvePrincipal({ + id: "principal.operator", + type: "HUMAN", + authenticated: true, + authority_chain: ["authority.demo.1"], + attributes: { role: "operator" }, + resolved_at: T0, + ...overrides.principal + }); + const authority = 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, + ...overrides.authority + }); + const policy = 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, + ...overrides.policy + }); + const adapter_capability = createAdapterCapability({ + id: "cint.adapter.synthetic-file-patch", + action_types: ["SYNTHETIC_FILE_PATCH"], + consequence_classes: ["CONSEQUENTIAL"], + rollback: true, + interrupt: true, + outcome_verification: true, + ...overrides.adapter_capability + }); + const machine_state = createMachineStateSnapshot({ + id: "machine.synthetic", + epoch: 1, + available: true, + state: { mode: "READY", target_locked: false }, + observed_at: T0, + ...overrides.machine_state + }); + return { intent, principal, authority, policy, adapter_capability, machine_state }; +} + +function decideRecords(values, overrides = {}) { + return decide({ + id: overrides.id ?? "decision.demo.1", + ...values, + now: overrides.now ?? T1, + expires_at: overrides.expires_at ?? T2 + }); +} + +test("canonical parser admits only the canonical byte representation", () => { + const value = { z: 1, a: [true, null] }; + 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" + ); +}); + +test("strict intent construction rejects unknown fields", () => { + assert.throws( + () => records({ intent: { undeclared_field: true } }), + (error) => error.code === "CINT_UNKNOWN_FIELD" + ); +}); + +test("exact current bindings produce ADMIT without executable authority", () => { + const decision = decideRecords(records()); + assert.equal(decision.status, "ADMIT"); + assert.equal(decision.receipt_eligible, true); + assert.equal(decision.execution_authority, "NONE"); + assert.equal(decision.reason_codes.length, 0); +}); + +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")); +}); + +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")); +}); + +test("target outside the exact authority binding is denied", () => { + const values = records({ + intent: { + action: { + adapter: "cint.adapter.synthetic-file-patch", + type: "SYNTHETIC_FILE_PATCH", + target: { path: "sandbox/other.txt" }, + parameters: { content: "after", expected_before_sha256: "0".repeat(64) }, + consequence: "CONSEQUENTIAL" + } + } + }); + const decision = decideRecords(values); + assert.equal(decision.status, "DENY"); + assert(decision.reason_codes.includes("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")); +}); + +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")); +}); + +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")); +}); + +test("tampered sealed input cannot reach a decision", () => { + const values = records(); + const tamperedIntent = { ...values.intent, request: "Different request" }; + assert.throws( + () => decideRecords({ ...values, intent: tamperedIntent }), + (error) => error.code === "CINT_RECORD_TAMPERED" + ); +}); + +test("decision cannot outlive its authority", () => { + assert.throws( + () => decideRecords(records(), { expires_at: "2026-08-27T02:00:00.000Z" }), + (error) => error.code === "CINT_DECISION_TIME" + ); +}); + +test("state machine permits only declared transitions", () => { + const requested = createStateMachine({ id: "state.demo.1", subject_id: "intent.demo.1", created_at: T0 }); + const challenged = transitionState(requested, { state: "CHALLENGED", at: T1, evidence_digest: null }); + const admitted = transitionState(challenged, { state: "ADMITTED", at: T2, evidence_digest: null }); + assert.equal(admitted.state, "ADMITTED"); + assert.throws( + () => transitionState(admitted, { state: "EXECUTING", at: T2, evidence_digest: null }), + (error) => error.code === "CINT_STATE_TRANSITION" + ); +}); diff --git a/tests/legacy-adapter.test.js b/tests/legacy-adapter.test.js index 37d1d08..fafa219 100644 --- a/tests/legacy-adapter.test.js +++ b/tests/legacy-adapter.test.js @@ -27,4 +27,3 @@ test("Adapter 01 declares a zero-authority CINT boundary", () => { may_seal_outcome: false }); }); - From c41db4cb3dd7a9cf5878461e5bd0f4f243c8957c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Thu, 27 Aug 2026 20:31:51 +0300 Subject: [PATCH 04/16] cint(g4): enforce one-shot decision receipts --- artifacts/cint-r0/gate-ledger.json | 6 + artifacts/cint-r0/gates/CINT-R0.G4.json | 18 ++ docs/cint-r0/04_DECISION_RECEIPTS.md | 55 +++++ schemas/cint/receipt.schema.json | 43 ++++ src/cint/index.js | 2 + src/cint/receipt.js | 119 ++++++++++ src/cint/store.js | 190 ++++++++++++++++ tests/cint-receipt.test.js | 277 ++++++++++++++++++++++++ 8 files changed, 710 insertions(+) create mode 100644 artifacts/cint-r0/gates/CINT-R0.G4.json create mode 100644 docs/cint-r0/04_DECISION_RECEIPTS.md create mode 100644 schemas/cint/receipt.schema.json create mode 100644 src/cint/receipt.js create mode 100644 src/cint/store.js create mode 100644 tests/cint-receipt.test.js diff --git a/artifacts/cint-r0/gate-ledger.json b/artifacts/cint-r0/gate-ledger.json index 8536f18..5bbbc87 100644 --- a/artifacts/cint-r0/gate-ledger.json +++ b/artifacts/cint-r0/gate-ledger.json @@ -28,6 +28,12 @@ "name": "CINT contracts and decision engine", "status": "PASS", "evidence": "docs/cint-r0/03_DECISION_CONTRACTS.md" + }, + { + "gate": "CINT-R0.G4", + "name": "Receipt and atomic one-shot consumption", + "status": "PASS", + "evidence": "docs/cint-r0/04_DECISION_RECEIPTS.md" } ], "terminal_verdict": null diff --git a/artifacts/cint-r0/gates/CINT-R0.G4.json b/artifacts/cint-r0/gates/CINT-R0.G4.json new file mode 100644 index 0000000..4eaad1b --- /dev/null +++ b/artifacts/cint-r0/gates/CINT-R0.G4.json @@ -0,0 +1,18 @@ +{ + "gate": "CINT-R0.G4", + "status": "PASS", + "receipt_tests": { + "passed": 10, + "failed": 0 + }, + "prior_tests": { + "passed": 28, + "failed": 0 + }, + "receipt_authentication": "HMAC-SHA256", + "one_shot_store": "file-exclusive-lock-and-terminal-state", + "parallel_consumers_admitted": 1, + "ambiguous_lock": "FAIL_CLOSED", + "adapter_receipt_authority": "NONE", + "evidence": "docs/cint-r0/04_DECISION_RECEIPTS.md" +} diff --git a/docs/cint-r0/04_DECISION_RECEIPTS.md b/docs/cint-r0/04_DECISION_RECEIPTS.md new file mode 100644 index 0000000..3b01d57 --- /dev/null +++ b/docs/cint-r0/04_DECISION_RECEIPTS.md @@ -0,0 +1,55 @@ +# CINT-R0 decision receipts and one-shot store + +An `ADMIT` decision can now be transformed into an executable-authority +candidate only by the CINT receipt authority. `DENY` and `REVIEW` decisions are +cryptographically ineligible. + +## Receipt binding + +Each receipt is HMAC-SHA256 authenticated and carries the exact decision digest, +decision lifetime, nonce, and the admitted binding: + +- intent, action, target, and context digests; +- principal digest; +- authority identifier, digest, and epoch; +- policy identifier, digest, and epoch; +- adapter identifier and capability digest; +- machine-state identifier, digest, and epoch. + +Changing any field, recomputing the public record digest, or substituting a +different issuer fails verification. The signing key never enters the receipt. + +## Atomic consumption protocol + +The file-backed receipt store uses a hashed receipt identifier and four isolated +states: `pending`, `locks`, `consumed`, and `rejected`. + +```text +register with exclusive create + -> acquire exclusive receipt lock + -> compare presented and registered receipt digests + -> run immediate revalidation while holding the lock + -> write terminal consumed or rejected record with exclusive create + -> remove pending record + -> release lock +``` + +Only one concurrent consumer can acquire the lock. A consumed, rejected, or +already locked receipt returns `CINT_RECEIPT_REPLAY_REJECTED`. Crash residue is +not guessed away; an ambiguous lock remains `LOCKED` and therefore fail closed. +Expired or revalidation-revoked receipts move to the terminal rejected state. + +## Gate proof + +- receipt issuance from non-`ADMIT` decisions is rejected; +- exact binding and signature verification passes; +- rehashed binding forgery and expiry are rejected; +- registered receipts consume once and reject replay; +- two concurrent consumers produce exactly one success; +- a substituted receipt with the same identifier is rejected; +- failed revalidation and expiry become terminal rejection; +- crash-lock ambiguity remains fail closed; +- all 28 earlier CINT and legacy tests remain green. + +No adapter is given the receipt key or store authority. Consequential execution +remains absent until the next gate. diff --git a/schemas/cint/receipt.schema.json b/schemas/cint/receipt.schema.json new file mode 100644 index 0000000..09533a0 --- /dev/null +++ b/schemas/cint/receipt.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/receipt-1.json", + "title": "CINT one-shot decision receipt", + "type": "object", + "additionalProperties": false, + "required": [ + "protocol", + "id", + "issuer_id", + "decision_id", + "decision_digest", + "status", + "issued_at", + "expires_at", + "nonce", + "binding", + "binding_digest", + "signature_algorithm", + "signature", + "digest" + ], + "properties": { + "protocol": { "const": "cint/decision-receipt/1" }, + "id": { "$ref": "#/$defs/id" }, + "issuer_id": { "$ref": "#/$defs/id" }, + "decision_id": { "$ref": "#/$defs/id" }, + "decision_digest": { "$ref": "#/$defs/sha256" }, + "status": { "const": "ISSUED" }, + "issued_at": { "type": "string", "format": "date-time" }, + "expires_at": { "type": "string", "format": "date-time" }, + "nonce": { "type": "string", "minLength": 16, "maxLength": 128 }, + "binding": { "$ref": "decision.schema.json#/properties/binding" }, + "binding_digest": { "$ref": "#/$defs/sha256" }, + "signature_algorithm": { "const": "HMAC-SHA256" }, + "signature": { "$ref": "#/$defs/sha256" }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/src/cint/index.js b/src/cint/index.js index b2f98c3..cab77f2 100644 --- a/src/cint/index.js +++ b/src/cint/index.js @@ -6,4 +6,6 @@ export * from "./intent.js"; export * from "./machine-state.js"; export * from "./policy.js"; export * from "./principal.js"; +export * from "./receipt.js"; export * from "./state-machine.js"; +export * from "./store.js"; diff --git a/src/cint/receipt.js b/src/cint/receipt.js new file mode 100644 index 0000000..cd29a83 --- /dev/null +++ b/src/cint/receipt.js @@ -0,0 +1,119 @@ +import { createHmac, randomUUID, timingSafeEqual } from "node:crypto"; + +import { canonicalJson } from "../util.js"; +import { + assertCint, + assertExactKeys, + boundedString, + canonicalDigest, + identifier, + isoInstant, + sealRecord, + sha256Digest, + verifySealedRecord +} from "./canonical.js"; + +function unsignedReceipt(receipt) { + return Object.fromEntries( + Object.entries(receipt).filter(([key]) => key !== "digest" && key !== "signature") + ); +} + +export class DecisionReceiptAuthority { + #key; + + 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"); + 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"); + } + + issue(input) { + assertExactKeys(input, ["decision", "issued_at"], ["id", "nonce"], "receipt issuance"); + verifySealedRecord(input.decision, "decision"); + assertCint( + input.decision.protocol === "cint/decision/1" && input.decision.status === "ADMIT" && input.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 unsigned = { + protocol: "cint/decision-receipt/1", + 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", + 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" + }; + return sealRecord({ ...unsigned, signature: this.#sign(unsigned) }); + } + + verify(receipt, options = {}) { + verifySealedRecord(receipt, "receipt"); + assertExactKeys( + receipt, + [ + "protocol", + "id", + "issuer_id", + "decision_id", + "decision_digest", + "status", + "issued_at", + "expires_at", + "nonce", + "binding", + "binding_digest", + "signature_algorithm", + "signature", + "digest" + ], + [], + "receipt" + ); + assertCint(receipt.protocol === "cint/decision-receipt/1", "CINT_RECEIPT_PROTOCOL", "Unsupported receipt protocol"); + identifier(receipt.id, "receipt.id"); + identifier(receipt.decision_id, "receipt.decision_id"); + assertCint(receipt.issuer_id === this.issuer_id, "CINT_RECEIPT_ISSUER_MISMATCH", "Receipt issuer does not match this authority"); + assertCint(receipt.status === "ISSUED", "CINT_RECEIPT_STATUS", "Receipt is not in ISSUED state"); + isoInstant(receipt.issued_at, "receipt.issued_at"); + isoInstant(receipt.expires_at, "receipt.expires_at"); + boundedString(receipt.nonce, "receipt.nonce", { minimum: 16, maximum: 128 }); + sha256Digest(receipt.decision_digest, "receipt.decision_digest"); + sha256Digest(receipt.binding_digest, "receipt.binding_digest"); + assertCint( + canonicalDigest(receipt.binding) === receipt.binding_digest, + "CINT_RECEIPT_BINDING_INVALID", + "Receipt binding digest is invalid" + ); + assertCint(receipt.signature_algorithm === "HMAC-SHA256", "CINT_RECEIPT_SIGNATURE_INVALID", "Unsupported receipt signature algorithm"); + sha256Digest(receipt.signature, "receipt.signature"); + const expected = Buffer.from(this.#sign(unsignedReceipt(receipt)), "hex"); + const actual = Buffer.from(receipt.signature, "hex"); + assertCint( + expected.byteLength === actual.byteLength && timingSafeEqual(expected, actual), + "CINT_RECEIPT_SIGNATURE_INVALID", + "Receipt signature is invalid" + ); + if (options.now !== undefined) { + const now = isoInstant(options.now, "receipt verification time"); + assertCint(Date.parse(now) < Date.parse(receipt.expires_at), "CINT_RECEIPT_EXPIRED", "Receipt has expired"); + } + return receipt; + } +} diff --git a/src/cint/store.js b/src/cint/store.js new file mode 100644 index 0000000..29f28c2 --- /dev/null +++ b/src/cint/store.js @@ -0,0 +1,190 @@ +import { access, mkdir, open, readFile, unlink } from "node:fs/promises"; +import path from "node:path"; + +import { canonicalJson, sha256 } from "../util.js"; +import { + CintError, + assertCint, + assertExactKeys, + identifier, + immutableRecord, + isoInstant, + parseCanonicalJson, + sealRecord, + sha256Digest, + verifySealedRecord +} from "./canonical.js"; + +async function exists(filePath) { + try { + await access(filePath); + return true; + } catch (error) { + if (error.code === "ENOENT") return false; + throw error; + } +} + +async function writeExclusive(filePath, value) { + let handle; + try { + handle = await open(filePath, "wx", 0o600); + await handle.writeFile(canonicalJson(value), "utf8"); + await handle.sync(); + } finally { + if (handle) await handle.close(); + } +} + +export class FileReceiptStore { + constructor(root) { + this.root = path.resolve(root); + this.pending = path.join(this.root, "pending"); + this.consumed = path.join(this.root, "consumed"); + this.rejected = path.join(this.root, "rejected"); + this.locks = path.join(this.root, "locks"); + } + + async initialize() { + 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"); + const name = `${sha256(receiptId)}.json`; + return { + pending: path.join(this.pending, name), + consumed: path.join(this.consumed, name), + rejected: path.join(this.rejected, name), + lock: path.join(this.locks, `${name}.lock`) + }; + } + + async register(receipt, input) { + verifySealedRecord(receipt, "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"); + 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", + receipt_id: receipt.id, + receipt_digest: receipt.digest, + registered_at: isoInstant(input.registered_at, "receipt registered_at") + }); + try { + await writeExclusive(paths.pending, entry); + } catch (error) { + if (error.code === "EEXIST") { + throw new CintError("CINT_RECEIPT_ALREADY_REGISTERED", "Receipt is already registered"); + } + throw error; + } + return entry; + } + + async inspect(receiptId) { + await this.initialize(); + const paths = this.#paths(receiptId); + if (await exists(paths.consumed)) return immutableRecord({ state: "CONSUMED", path: "consumed" }); + if (await exists(paths.rejected)) return immutableRecord({ state: "REJECTED", path: "rejected" }); + if (await exists(paths.lock)) return immutableRecord({ state: "LOCKED", path: "locks" }); + if (await exists(paths.pending)) return immutableRecord({ state: "PENDING", path: "pending" }); + return immutableRecord({ state: "ABSENT", path: null }); + } + + async consume(receipt, input) { + verifySealedRecord(receipt, "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"); + await this.initialize(); + const consumedAt = isoInstant(input.consumed_at, "receipt consumed_at"); + const paths = this.#paths(receipt.id); + let lock; + 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") { + throw new CintError("CINT_RECEIPT_REPLAY_REJECTED", "Receipt is already in flight or consumed"); + } + throw error; + } + + try { + if ((await exists(paths.consumed)) || (await exists(paths.rejected))) { + 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; + throw error; + }); + assertCint(pendingText !== null, "CINT_RECEIPT_UNREGISTERED", "Receipt is not registered"); + const pendingEntry = parseCanonicalJson(pendingText, "pending receipt entry"); + verifySealedRecord(pendingEntry, "pending receipt entry"); + assertCint( + pendingEntry.receipt_id === receipt.id && pendingEntry.receipt_digest === receipt.digest, + "CINT_RECEIPT_STORE_MISMATCH", + "Registered receipt digest does not match the presented receipt" + ); + + let revalidation; + if (Date.parse(consumedAt) >= Date.parse(receipt.expires_at)) { + revalidation = { status: "REVOKED", reason_codes: ["CINT_RECEIPT_EXPIRED"], digest: receipt.digest }; + } else { + revalidation = await input.revalidate(receipt); + } + assertCint(revalidation && typeof revalidation === "object", "CINT_REVALIDATION_INVALID", "Revalidation returned no result"); + const revalidationDigest = revalidation.digest ?? receipt.digest; + sha256Digest(revalidationDigest, "revalidation digest"); + if (revalidation.status !== "VALID") { + const rejectedEntry = sealRecord({ + protocol: "cint/receipt-store-entry/1", + state: "REJECTED", + 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"] + }); + await writeExclusive(paths.rejected, rejectedEntry); + await unlink(paths.pending); + throw new CintError("CINT_RECEIPT_REVOKED", "Receipt failed immediate revalidation", { + reason_codes: rejectedEntry.reason_codes + }); + } + const consumedEntry = sealRecord({ + protocol: "cint/receipt-store-entry/1", + state: "CONSUMED", + receipt_id: receipt.id, + receipt_digest: receipt.digest, + consumed_at: consumedAt, + revalidation_digest: revalidationDigest + }); + await writeExclusive(paths.consumed, consumedEntry); + await unlink(paths.pending); + return consumedEntry; + } finally { + await lock.close().catch(() => {}); + await unlink(paths.lock).catch(() => {}); + } + } +} diff --git a/tests/cint-receipt.test.js b/tests/cint-receipt.test.js new file mode 100644 index 0000000..edb8ec4 --- /dev/null +++ b/tests/cint-receipt.test.js @@ -0,0 +1,277 @@ +import assert from "node:assert/strict"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { + DecisionReceiptAuthority, + FileReceiptStore, + canonicalDigest, + createAdapterCapability, + createAuthorityGrant, + createIntent, + createMachineStateSnapshot, + createPolicySnapshot, + decide, + resolvePrincipal +} from "../src/cint/index.js"; +import { sha256 } from "../src/util.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 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 = {}) { + const intent = createIntent({ + id: "intent.receipt.1", + principal_id: "principal.operator", + request: "Apply the declared disposable synthetic patch.", + 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 the declared disposable target bytes"], + context: { workspace: "synthetic-proof" }, + uncertainties: [], + created_at: T0, + ...overrides.intent + }); + const principal = resolvePrincipal({ + id: "principal.operator", + type: "HUMAN", + authenticated: true, + authority_chain: ["authority.receipt.1"], + attributes: {}, + resolved_at: T0 + }); + const authority = createAuthorityGrant({ + id: "authority.receipt.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.receipt"], + require_rollback: true, + issued_at: T0, + not_before: T0, + expires_at: EXPIRY + }); + const policy = createPolicySnapshot({ + id: "policy.receipt", + 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 = createAdapterCapability({ + id: "cint.adapter.synthetic-file-patch", + action_types: ["SYNTHETIC_FILE_PATCH"], + consequence_classes: ["CONSEQUENTIAL"], + rollback: true, + interrupt: true, + outcome_verification: true + }); + const machine_state = createMachineStateSnapshot({ + id: "machine.synthetic", + epoch: 1, + available: true, + state: { mode: "READY" }, + observed_at: T0 + }); + return decide({ + id: overrides.id ?? "decision.receipt.1", + intent, + principal, + authority, + policy, + adapter_capability, + machine_state, + now: T1, + expires_at: T2 + }); +} + +function receiptAuthority() { + return new DecisionReceiptAuthority({ + issuer_id: "cint.receipt-authority.r0", + key: Buffer.alloc(32, 7) + }); +} + +function receiptFor(decision = decisionFor()) { + return receiptAuthority().issue({ + id: "receipt.demo.1", + nonce: "receipt-nonce-0000000001", + decision, + issued_at: T1 + }); +} + +function reforge(record, changes) { + const unsigned = { ...record, ...changes }; + delete unsigned.digest; + return { ...unsigned, digest: canonicalDigest(unsigned) }; +} + +async function withStore(run) { + const root = await mkdtemp(path.join(os.tmpdir(), "cint-receipt-test-")); + try { + return await run(new FileReceiptStore(root)); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +const validRevalidation = async () => ({ + status: "VALID", + reason_codes: [], + digest: "a".repeat(64) +}); + +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" + ); +}); + +test("receipt signature covers every decision binding", () => { + const authority = receiptAuthority(); + const receipt = receiptFor(); + assert.equal(authority.verify(receipt, { now: T1 }), receipt); + assert.equal(receipt.binding.action_digest.length, 64); + assert.equal(receipt.binding.target_digest.length, 64); + assert.equal(receipt.binding.context_digest.length, 64); +}); + +test("rehashed binding forgery fails the receipt signature", () => { + const authority = receiptAuthority(); + const receipt = receiptFor(); + const binding = { ...receipt.binding, action_digest: "f".repeat(64) }; + const forged = reforge(receipt, { + binding, + binding_digest: canonicalDigest(binding) + }); + assert.throws( + () => authority.verify(forged, { now: T1 }), + (error) => error.code === "CINT_RECEIPT_SIGNATURE_INVALID" + ); +}); + +test("expired receipt verification is rejected", () => { + assert.throws( + () => receiptAuthority().verify(receiptFor(), { now: T2 }), + (error) => error.code === "CINT_RECEIPT_EXPIRED" + ); +}); + +test("registered receipt is consumed exactly once", async () => { + await withStore(async (store) => { + const receipt = receiptFor(); + await store.register(receipt, { registered_at: T1 }); + const consumed = await store.consume(receipt, { consumed_at: T1, revalidate: validRevalidation }); + assert.equal(consumed.state, "CONSUMED"); + 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" + ); + }); +}); + +test("parallel replay produces one atomic consumer", async () => { + await withStore(async (store) => { + const receipt = receiptFor(); + await store.register(receipt, { registered_at: T1 }); + const attempts = await Promise.allSettled([ + store.consume(receipt, { consumed_at: T1, revalidate: validRevalidation }), + store.consume(receipt, { consumed_at: T1, revalidate: validRevalidation }) + ]); + 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"); + }); +}); + +test("presented receipt must match the registered digest", async () => { + await withStore(async (store) => { + const receipt = receiptFor(); + await store.register(receipt, { registered_at: T1 }); + const other = receiptAuthority().issue({ + id: receipt.id, + nonce: "receipt-nonce-0000000002", + decision: decisionFor(), + issued_at: T1 + }); + await assert.rejects( + store.consume(other, { consumed_at: T1, revalidate: validRevalidation }), + (error) => error.code === "CINT_RECEIPT_STORE_MISMATCH" + ); + }); +}); + +test("failed immediate revalidation terminally rejects the receipt", async () => { + await withStore(async (store) => { + const receipt = receiptFor(); + await store.register(receipt, { registered_at: T1 }); + await assert.rejects( + store.consume(receipt, { + consumed_at: T1, + revalidate: async () => ({ + status: "REVOKED", + reason_codes: ["CINT_POLICY_CHANGED"], + digest: "b".repeat(64) + }) + }), + (error) => error.code === "CINT_RECEIPT_REVOKED" + ); + assert.equal((await store.inspect(receipt.id)).state, "REJECTED"); + }); +}); + +test("receipt expiry at consumption is terminal and fail closed", async () => { + await withStore(async (store) => { + const receipt = receiptFor(); + await store.register(receipt, { registered_at: T1 }); + await assert.rejects( + store.consume(receipt, { consumed_at: T3, revalidate: validRevalidation }), + (error) => error.code === "CINT_RECEIPT_REVOKED" + ); + assert.equal((await store.inspect(receipt.id)).state, "REJECTED"); + }); +}); + +test("ambiguous crash lock remains fail closed", async () => { + await withStore(async (store) => { + const receipt = receiptFor(); + await store.register(receipt, { registered_at: T1 }); + const lock = path.join(store.root, "locks", `${sha256(receipt.id)}.json.lock`); + await writeFile(lock, "crash-residue", "utf8"); + await assert.rejects( + store.consume(receipt, { consumed_at: T1, revalidate: validRevalidation }), + (error) => error.code === "CINT_RECEIPT_REPLAY_REJECTED" + ); + assert.equal((await store.inspect(receipt.id)).state, "LOCKED"); + }); +}); From 220a00bc1c0b0ae81c73809207e3c0812566a079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Thu, 27 Aug 2026 20:40:33 +0300 Subject: [PATCH 05/16] cint(g5): gate synthetic execution and rollback --- artifacts/cint-r0/gate-ledger.json | 6 + artifacts/cint-r0/gates/CINT-R0.G5.json | 27 ++ docs/cint-r0/05_SYNTHETIC_ACTION_PROOF.md | 52 ++++ schemas/cint/execution-result.schema.json | 39 +++ schemas/cint/outcome.schema.json | 41 +++ schemas/cint/revalidation.schema.json | 54 ++++ schemas/cint/seal.schema.json | 45 +++ src/cint/adapters/synthetic-file-patch.js | 125 ++++++++ src/cint/evidence.js | 85 ++++++ src/cint/execution.js | 283 ++++++++++++++++++ src/cint/index.js | 7 + src/cint/outcome.js | 31 ++ src/cint/revalidation.js | 127 ++++++++ src/cint/rollback.js | 16 + src/cint/seal.js | 110 +++++++ tests/cint-execution.test.js | 346 ++++++++++++++++++++++ 16 files changed, 1394 insertions(+) create mode 100644 artifacts/cint-r0/gates/CINT-R0.G5.json create mode 100644 docs/cint-r0/05_SYNTHETIC_ACTION_PROOF.md create mode 100644 schemas/cint/execution-result.schema.json create mode 100644 schemas/cint/outcome.schema.json create mode 100644 schemas/cint/revalidation.schema.json create mode 100644 schemas/cint/seal.schema.json create mode 100644 src/cint/adapters/synthetic-file-patch.js create mode 100644 src/cint/evidence.js create mode 100644 src/cint/execution.js create mode 100644 src/cint/outcome.js create mode 100644 src/cint/revalidation.js create mode 100644 src/cint/rollback.js create mode 100644 src/cint/seal.js create mode 100644 tests/cint-execution.test.js diff --git a/artifacts/cint-r0/gate-ledger.json b/artifacts/cint-r0/gate-ledger.json index 5bbbc87..3373783 100644 --- a/artifacts/cint-r0/gate-ledger.json +++ b/artifacts/cint-r0/gate-ledger.json @@ -34,6 +34,12 @@ "name": "Receipt and atomic one-shot consumption", "status": "PASS", "evidence": "docs/cint-r0/04_DECISION_RECEIPTS.md" + }, + { + "gate": "CINT-R0.G5", + "name": "Synthetic gated execution and rollback", + "status": "PASS", + "evidence": "docs/cint-r0/05_SYNTHETIC_ACTION_PROOF.md" } ], "terminal_verdict": null diff --git a/artifacts/cint-r0/gates/CINT-R0.G5.json b/artifacts/cint-r0/gates/CINT-R0.G5.json new file mode 100644 index 0000000..da83158 --- /dev/null +++ b/artifacts/cint-r0/gates/CINT-R0.G5.json @@ -0,0 +1,27 @@ +{ + "gate": "CINT-R0.G5", + "status": "PASS", + "synthetic_execution_tests": { + "passed": 10, + "failed": 0 + }, + "prior_tests": { + "passed": 38, + "failed": 0 + }, + "proof": { + "bounded_patch": "SEALED", + "silent_request": "DENY", + "outside_authority": "DENY", + "missing_rollback": "DENY", + "changed_action": "REJECTED", + "stale_policy": "REVOKED", + "replay": "REPLAY_REJECTED", + "divergence": "ROLLED_BACK", + "interrupt": "ROLLED_BACK", + "unavailable": "FAIL_CLOSED", + "rollback_hash_restored": true + }, + "external_mutations": 0, + "evidence": "docs/cint-r0/05_SYNTHETIC_ACTION_PROOF.md" +} diff --git a/docs/cint-r0/05_SYNTHETIC_ACTION_PROOF.md b/docs/cint-r0/05_SYNTHETIC_ACTION_PROOF.md new file mode 100644 index 0000000..7e2e365 --- /dev/null +++ b/docs/cint-r0/05_SYNTHETIC_ACTION_PROOF.md @@ -0,0 +1,52 @@ +# CINT-R0 synthetic consequential-action proof + +The first CINT action adapter operates only on a disposable file boundary. Its +single action type, `SYNTHETIC_FILE_PATCH`, binds a relative target, expected +pre-action hash, exact replacement bytes, and consequential classification into +the admitted intent and one-shot receipt. + +## Enforced sequence + +```text +verify receipt signature and lifetime + -> lock receipt + -> resolve current intent, principal, authority, policy, adapter, and state + -> revalidate every receipt binding + -> consume receipt once + -> resolve and revalidate current state again + -> verify target pre-action hash + -> execute atomic synthetic patch + -> verify resulting bytes + -> seal verified outcome OR restore original bytes and seal rollback +``` + +The execution ledger is hash-chained. The evidence seal authenticates the +receipt, consumption record, immediate revalidation, verified or restored +outcome, and terminal ledger head. The adapter receives neither receipt nor seal +keys and cannot admit or seal itself. + +## Proof matrix + +| Vector | Terminal state | Action started | Material result | +|---|---|---:|---| +| Explicit bounded patch | `SEALED` | yes | Declared bytes verified and evidence sealed | +| Silent injected request | `DENY` | no | Receipt ineligible | +| Target outside authority | `DENY` | no | Receipt ineligible | +| Mutation without rollback | `DENY` | no | Receipt ineligible | +| Changed action digest | `REJECTED` | no | Receipt terminally rejected | +| Stale policy before consumption | `REVOKED` | no | Receipt terminally rejected | +| Policy change after consumption | `REVOKED` | no | Consumed receipt cannot act | +| Receipt replay | `REPLAY_REJECTED` | no | No second action | +| Outcome divergence | `ROLLED_BACK` | yes | Original hash restored and sealed | +| In-flight interrupt | `ROLLED_BACK` | yes | Original hash restored and sealed | +| CINT dependency unavailable | `FAIL_CLOSED` | no | Receipt remains pending; target unchanged | +| Target state changed after decision | `FAIL_CLOSED` | no | External bytes preserved; no overwrite | + +## Gate proof + +- 10 end-to-end synthetic execution tests pass; +- all 38 prior CINT and legacy tests remain green; +- successful, divergent, and interrupted paths produce verifiable evidence + seals; +- rollback restoration is asserted by the exact original SHA-256 digest; +- the synthetic adapter never reaches another repository or live machine state. diff --git a/schemas/cint/execution-result.schema.json b/schemas/cint/execution-result.schema.json new file mode 100644 index 0000000..9bc190b --- /dev/null +++ b/schemas/cint/execution-result.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/execution-result-1.json", + "title": "CINT execution result", + "type": "object", + "additionalProperties": false, + "required": [ + "protocol", + "status", + "receipt_id", + "receipt_digest", + "consumption_digest", + "revalidation_digest", + "action_started", + "outcome", + "evidence_seal", + "error_code", + "completed_at", + "digest" + ], + "properties": { + "protocol": { "const": "cint/execution-result/1" }, + "status": { "enum": ["SEALED", "ROLLED_BACK", "REPLAY_REJECTED", "REJECTED", "REVOKED", "FAIL_CLOSED"] }, + "receipt_id": { "type": ["string", "null"] }, + "receipt_digest": { "$ref": "#/$defs/nullableSha256" }, + "consumption_digest": { "$ref": "#/$defs/nullableSha256" }, + "revalidation_digest": { "$ref": "#/$defs/nullableSha256" }, + "action_started": { "type": "boolean" }, + "outcome": { "oneOf": [{ "$ref": "outcome.schema.json" }, { "type": "null" }] }, + "evidence_seal": { "oneOf": [{ "$ref": "seal.schema.json" }, { "type": "null" }] }, + "error_code": { "type": ["string", "null"] }, + "completed_at": { "type": "string", "format": "date-time" }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "nullableSha256": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/schemas/cint/outcome.schema.json b/schemas/cint/outcome.schema.json new file mode 100644 index 0000000..e5bf037 --- /dev/null +++ b/schemas/cint/outcome.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/outcome-1.json", + "title": "CINT verified outcome", + "type": "object", + "additionalProperties": false, + "required": [ + "protocol", + "status", + "effect_status", + "receipt_id", + "receipt_digest", + "action_digest", + "target_digest", + "execution_digest", + "verification_digest", + "rollback_digest", + "final_state_digest", + "completed_at", + "digest" + ], + "properties": { + "protocol": { "const": "cint/outcome/1" }, + "status": { "enum": ["VERIFIED", "ROLLED_BACK"] }, + "effect_status": { "enum": ["APPLIED", "RESTORED"] }, + "receipt_id": { "$ref": "#/$defs/id" }, + "receipt_digest": { "$ref": "#/$defs/sha256" }, + "action_digest": { "$ref": "#/$defs/sha256" }, + "target_digest": { "$ref": "#/$defs/sha256" }, + "execution_digest": { "$ref": "#/$defs/sha256" }, + "verification_digest": { "$ref": "#/$defs/sha256" }, + "rollback_digest": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$" }, + "final_state_digest": { "$ref": "#/$defs/sha256" }, + "completed_at": { "type": "string", "format": "date-time" }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/schemas/cint/revalidation.schema.json b/schemas/cint/revalidation.schema.json new file mode 100644 index 0000000..bfcf500 --- /dev/null +++ b/schemas/cint/revalidation.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/revalidation-1.json", + "title": "CINT immediate revalidation", + "type": "object", + "additionalProperties": false, + "required": ["protocol", "receipt_id", "status", "checked_at", "binding_digest", "current", "reason_codes", "digest"], + "properties": { + "protocol": { "const": "cint/revalidation/1" }, + "receipt_id": { "$ref": "#/$defs/id" }, + "status": { "enum": ["VALID", "REVOKED", "REJECTED", "FAIL_CLOSED"] }, + "checked_at": { "type": "string", "format": "date-time" }, + "binding_digest": { "$ref": "#/$defs/sha256" }, + "current": { + "type": "object", + "additionalProperties": false, + "required": [ + "intent_digest", + "action_digest", + "target_digest", + "context_digest", + "principal_digest", + "authority_digest", + "authority_epoch", + "policy_digest", + "policy_epoch", + "adapter_capability_digest", + "machine_state_digest", + "machine_state_epoch" + ], + "properties": { + "intent_digest": { "$ref": "#/$defs/nullableSha256" }, + "action_digest": { "$ref": "#/$defs/nullableSha256" }, + "target_digest": { "$ref": "#/$defs/nullableSha256" }, + "context_digest": { "$ref": "#/$defs/nullableSha256" }, + "principal_digest": { "$ref": "#/$defs/nullableSha256" }, + "authority_digest": { "$ref": "#/$defs/nullableSha256" }, + "authority_epoch": { "type": ["integer", "null"], "minimum": 1 }, + "policy_digest": { "$ref": "#/$defs/nullableSha256" }, + "policy_epoch": { "type": ["integer", "null"], "minimum": 1 }, + "adapter_capability_digest": { "$ref": "#/$defs/nullableSha256" }, + "machine_state_digest": { "$ref": "#/$defs/nullableSha256" }, + "machine_state_epoch": { "type": ["integer", "null"], "minimum": 1 } + } + }, + "reason_codes": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/id" } }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "nullableSha256": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/schemas/cint/seal.schema.json b/schemas/cint/seal.schema.json new file mode 100644 index 0000000..4463df4 --- /dev/null +++ b/schemas/cint/seal.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/seal-1.json", + "title": "CINT evidence seal", + "type": "object", + "additionalProperties": false, + "required": [ + "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" + ], + "properties": { + "protocol": { "const": "cint/evidence-seal/1" }, + "id": { "$ref": "#/$defs/id" }, + "issuer_id": { "$ref": "#/$defs/id" }, + "status": { "const": "SEALED" }, + "outcome_status": { "enum": ["VERIFIED", "ROLLED_BACK"] }, + "receipt_id": { "$ref": "#/$defs/id" }, + "receipt_digest": { "$ref": "#/$defs/sha256" }, + "consumption_digest": { "$ref": "#/$defs/sha256" }, + "revalidation_digest": { "$ref": "#/$defs/sha256" }, + "outcome_digest": { "$ref": "#/$defs/sha256" }, + "ledger_head_digest": { "$ref": "#/$defs/sha256" }, + "issued_at": { "type": "string", "format": "date-time" }, + "signature_algorithm": { "const": "HMAC-SHA256" }, + "signature": { "$ref": "#/$defs/sha256" }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/src/cint/adapters/synthetic-file-patch.js b/src/cint/adapters/synthetic-file-patch.js new file mode 100644 index 0000000..02cca90 --- /dev/null +++ b/src/cint/adapters/synthetic-file-patch.js @@ -0,0 +1,125 @@ +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, + 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"], + rollback: true, + interrupt: true, + outcome_verification: true + }); + } + + async prepare(intent, options) { + verifySealedRecord(intent, "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/evidence.js b/src/cint/evidence.js new file mode 100644 index 0000000..52f247b --- /dev/null +++ b/src/cint/evidence.js @@ -0,0 +1,85 @@ +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/execution.js b/src/cint/execution.js new file mode 100644 index 0000000..8d898a0 --- /dev/null +++ b/src/cint/execution.js @@ -0,0 +1,283 @@ +import { randomUUID } from "node:crypto"; + +import { + assertCint, + assertExactKeys, + isoInstant, + sealRecord, + 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 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, + errorCode = null, + actionStarted = true +}) { + const outcomeEvent = await record(input.ledger, `OUTCOME_${outcome.status}`, input.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: input.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: input.at + }); +} + +export async function executeWithReceipt(input) { + assertExactKeys( + input, + [ + "receipt", + "receipt_authority", + "store", + "snapshot_provider", + "adapter", + "seal_authority", + "ledger", + "at" + ], + ["signal"], + "execution request" + ); + const at = isoInstant(input.at, "execution time"); + const available = + typeof input.snapshot_provider === "function" && + input.receipt_authority?.verify instanceof Function && + input.store?.consume instanceof Function && + input.adapter?.prepare instanceof Function && + input.adapter?.execute instanceof Function && + input.seal_authority?.issue instanceof Function && + input.ledger?.record instanceof Function; + if (!available) { + return failureResult({ + receipt: input.receipt, + status: "FAIL_CLOSED", + code: "CINT_UNAVAILABLE", + at, + actionStarted: false + }); + } + try { + verifySealedRecord(input.receipt, "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, "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; + try { + prepared = await input.adapter.prepare(currentSnapshot.intent, { at, signal: input.signal }); + await record(input.ledger, "EXECUTION_STARTED", at, { + receipt_digest: input.receipt.digest, + action_digest: currentSnapshot.intent.action_digest, + target_digest: currentSnapshot.intent.target_digest + }); + actionStarted = true; + execution = await input.adapter.execute(prepared, { at, signal: input.signal }); + verifySealedRecord(execution, "adapter execution"); + const verification = await input.adapter.verify(prepared, execution, { at, signal: input.signal }); + verifySealedRecord(verification, "outcome verification"); + if (verification.status === "VERIFIED") { + const outcome = createOutcome({ + receipt: input.receipt, + execution, + verification, + rollback: null, + completed_at: at + }); + return await sealedResult({ input, consumption, revalidation, outcome, status: "SEALED" }); + } + const rollback = await performRollback(input.adapter, prepared, { at, 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: at + }); + return await sealedResult({ input, consumption, revalidation, outcome, status: "ROLLED_BACK" }); + } 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: at + }); + const verification = await input.adapter.verify(prepared, interruptedExecution, { at, signal: input.signal }).catch(() => + sealRecord({ + protocol: "cint/outcome-verification/1", + status: "DIVERGED", + target: "unknown", + expected_sha256: prepared.after_sha256, + actual_sha256: "0".repeat(64), + checked_at: at + }) + ); + const rollback = await performRollback(input.adapter, prepared, { at, signal: input.signal }); + if (rollback.status === "RESTORED") { + const outcome = createOutcome({ + receipt: input.receipt, + execution: interruptedExecution, + verification, + rollback, + completed_at: at + }); + return await sealedResult({ + input, + consumption, + revalidation, + outcome, + status: "ROLLED_BACK", + errorCode: error.code ?? "CINT_EXECUTION_FAILED" + }); + } + } + return failureResult({ + receipt: input.receipt, + status: "FAIL_CLOSED", + code: error.code ?? "CINT_FAIL_CLOSED", + at, + consumption, + revalidation, + actionStarted + }); + } +} diff --git a/src/cint/index.js b/src/cint/index.js index cab77f2..5a2e00b 100644 --- a/src/cint/index.js +++ b/src/cint/index.js @@ -1,11 +1,18 @@ export * from "./authority.js"; +export * from "./adapters/synthetic-file-patch.js"; export * from "./canonical.js"; export * from "./challenge.js"; export * from "./decision.js"; +export * from "./evidence.js"; +export * from "./execution.js"; export * from "./intent.js"; export * from "./machine-state.js"; +export * from "./outcome.js"; export * from "./policy.js"; export * from "./principal.js"; export * from "./receipt.js"; +export * from "./revalidation.js"; +export * from "./rollback.js"; +export * from "./seal.js"; export * from "./state-machine.js"; export * from "./store.js"; diff --git a/src/cint/outcome.js b/src/cint/outcome.js new file mode 100644 index 0000000..60477c4 --- /dev/null +++ b/src/cint/outcome.js @@ -0,0 +1,31 @@ +import { assertCint, assertExactKeys, isoInstant, sealRecord, verifySealedRecord } from "./canonical.js"; + +export function createOutcome(input) { + assertExactKeys( + input, + ["receipt", "execution", "verification", "rollback", "completed_at"], + [], + "outcome" + ); + verifySealedRecord(input.receipt, "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/revalidation.js b/src/cint/revalidation.js new file mode 100644 index 0000000..568306a --- /dev/null +++ b/src/cint/revalidation.js @@ -0,0 +1,127 @@ +import { isoInstant, sealRecord, verifySealedRecord } 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 signatureValid = false; + try { + input.receipt_authority.verify(input.receipt, { now: checkedAt }); + signatureValid = true; + } catch (error) { + reasonCodes.push(error.code ?? "CINT_RECEIPT_INVALID"); + } + + for (const [label, record] of Object.entries({ + intent: input.intent, + principal: input.principal, + authority: input.authority, + policy: input.policy, + adapter_capability: input.adapter_capability, + machine_state: input.machine_state + })) { + try { + verifySealedRecord(record, 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 (signatureValid && reasonCodes.length === 0) { + 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_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/rollback.js b/src/cint/rollback.js new file mode 100644 index 0000000..e0ed371 --- /dev/null +++ b/src/cint/rollback.js @@ -0,0 +1,16 @@ +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/seal.js b/src/cint/seal.js new file mode 100644 index 0000000..fa351c2 --- /dev/null +++ b/src/cint/seal.js @@ -0,0 +1,110 @@ +import { createHmac, randomUUID, timingSafeEqual } from "node:crypto"; + +import { canonicalJson } from "../util.js"; +import { + assertCint, + assertExactKeys, + identifier, + isoInstant, + sealRecord, + sha256Digest, + 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" + ); + for (const [label, record] of Object.entries({ + receipt: input.receipt, + consumption: input.consumption, + revalidation: input.revalidation, + outcome: input.outcome, + ledger_head: input.ledger_head + })) { + verifySealedRecord(record, label); + } + 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) { + verifySealedRecord(seal, "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/tests/cint-execution.test.js b/tests/cint-execution.test.js new file mode 100644 index 0000000..95b1452 --- /dev/null +++ b/tests/cint-execution.test.js @@ -0,0 +1,346 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { + CintError, + DecisionReceiptAuthority, + ExecutionLedger, + FileReceiptStore, + OutcomeSealAuthority, + SyntheticFilePatchAdapter, + createAdapterCapability, + createAuthorityGrant, + createIntent, + createMachineStateSnapshot, + createPolicySnapshot, + decide, + executeWithReceipt, + resolvePrincipal +} from "../src/cint/index.js"; +import { sha256 } from "../src/util.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"; + +async function withRuntime(run) { + 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"); + await writeFile(target, initial); + const targetObject = Object.freeze({ path: "target.txt" }); + const beforeSha256 = sha256(initial); + + const makeIntent = (overrides = {}) => + createIntent({ + id: overrides.id ?? "intent.execution.1", + principal_id: "principal.operator", + request: Object.hasOwn(overrides, "request") ? overrides.request : "Apply the bounded synthetic file patch.", + action: + overrides.action ?? + { + adapter: "cint.adapter.synthetic-file-patch", + type: "SYNTHETIC_FILE_PATCH", + target: targetObject, + parameters: { content: overrides.content ?? "after\n", expected_before_sha256: beforeSha256 }, + consequence: "CONSEQUENTIAL" + }, + declared_effects: overrides.declared_effects ?? ["Replace the declared disposable target bytes"], + context: overrides.context ?? { workspace: "synthetic-proof" }, + uncertainties: overrides.uncertainties ?? [], + created_at: T0 + }); + const principal = resolvePrincipal({ + id: "principal.operator", + type: "HUMAN", + authenticated: true, + authority_chain: ["authority.execution.1"], + attributes: {}, + resolved_at: T0 + }); + const authority = 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: targetObject + } + ], + policy_ids: ["policy.execution"], + require_rollback: true, + issued_at: T0, + not_before: T0, + expires_at: EXPIRY + }); + const makePolicy = (overrides = {}) => + 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 adapter = new SyntheticFilePatchAdapter(root); + const machine_state = createMachineStateSnapshot({ + id: "machine.synthetic", + epoch: 1, + available: true, + state: { mode: "READY", target_sha256: beforeSha256 }, + observed_at: T0 + }); + const intent = makeIntent(); + const snapshot = { + intent, + principal, + authority, + policy, + adapter_capability: adapter.capability, + machine_state + }; + const decision = decide({ + id: "decision.execution.1", + ...snapshot, + now: T1, + expires_at: T2 + }); + const receipt_authority = new 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 FileReceiptStore(path.join(root, "receipt-store")); + await store.register(receipt, { registered_at: T1 }); + const seal_authority = new OutcomeSealAuthority({ + issuer_id: "cint.seal-authority.r0", + key: Buffer.alloc(32, 19) + }); + const ledger = new ExecutionLedger(path.join(root, "evidence", "execution.jsonl")); + + const runtime = { + root, + target, + initial, + beforeSha256, + targetObject, + makeIntent, + makePolicy, + principal, + authority, + policy, + adapter, + machine_state, + snapshot, + decision, + receipt, + receipt_authority, + store, + seal_authority, + ledger, + execute: (overrides = {}) => + executeWithReceipt({ + receipt, + receipt_authority, + store, + snapshot_provider: async () => snapshot, + adapter, + seal_authority, + ledger, + at: T1, + ...overrides + }) + }; + try { + return await run(runtime); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +test("bounded synthetic mutation is admitted, verified, and sealed", async () => { + await withRuntime(async (runtime) => { + const result = await runtime.execute(); + assert.equal(result.status, "SEALED"); + assert.equal(result.outcome.status, "VERIFIED"); + assert.equal(result.outcome.effect_status, "APPLIED"); + 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); + }); +}); + +test("silent, out-of-authority, and rollback-free actions cannot obtain a receipt", async () => { + await withRuntime(async (runtime) => { + const silent = decide({ + id: "decision.synthetic.silent", + ...runtime.snapshot, + intent: runtime.makeIntent({ id: "intent.synthetic.silent", request: null }), + now: T1, + expires_at: T2 + }); + const outside = decide({ + id: "decision.synthetic.outside", + ...runtime.snapshot, + intent: runtime.makeIntent({ + id: "intent.synthetic.outside", + action: { + adapter: "cint.adapter.synthetic-file-patch", + type: "SYNTHETIC_FILE_PATCH", + target: { path: "other.txt" }, + parameters: { content: "after\n", expected_before_sha256: runtime.beforeSha256 }, + consequence: "CONSEQUENTIAL" + } + }), + now: T1, + expires_at: T2 + }); + const noRollbackCapability = createAdapterCapability({ + id: "cint.adapter.synthetic-file-patch", + action_types: ["SYNTHETIC_FILE_PATCH"], + consequence_classes: ["CONSEQUENTIAL"], + rollback: false, + interrupt: true, + outcome_verification: true + }); + const rollbackFree = decide({ + id: "decision.synthetic.no-rollback", + ...runtime.snapshot, + adapter_capability: noRollbackCapability, + now: T1, + expires_at: T2 + }); + assert.equal(silent.status, "DENY"); + assert.equal(outside.status, "DENY"); + 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" + ); + } + }); +}); + +test("changed action digest is rejected before execution", async () => { + await withRuntime(async (runtime) => { + const changed = runtime.makeIntent({ id: "intent.execution.changed", content: "different\n" }); + const result = await runtime.execute({ + snapshot_provider: async () => ({ ...runtime.snapshot, intent: changed }) + }); + assert.equal(result.status, "REJECTED"); + assert.equal(result.action_started, false); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("stale policy revokes the receipt before execution", async () => { + await withRuntime(async (runtime) => { + const changedPolicy = runtime.makePolicy({ version: "r0.2", epoch: 2, issued_at: T1 }); + const result = await runtime.execute({ + snapshot_provider: async () => ({ ...runtime.snapshot, policy: changedPolicy }) + }); + assert.equal(result.status, "REVOKED"); + assert.equal(result.action_started, false); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("policy change after consumption still blocks the action", async () => { + await withRuntime(async (runtime) => { + const changedPolicy = runtime.makePolicy({ version: "r0.2", epoch: 2, issued_at: T1 }); + let calls = 0; + const result = await runtime.execute({ + snapshot_provider: async () => { + calls += 1; + return calls === 1 ? runtime.snapshot : { ...runtime.snapshot, policy: changedPolicy }; + } + }); + assert.equal(result.status, "REVOKED"); + assert.equal(result.action_started, false); + assert.equal((await runtime.store.inspect(runtime.receipt.id)).state, "CONSUMED"); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("receipt replay is rejected after a sealed action", async () => { + await withRuntime(async (runtime) => { + assert.equal((await runtime.execute()).status, "SEALED"); + const replay = await runtime.execute(); + assert.equal(replay.status, "REPLAY_REJECTED"); + assert.equal(replay.action_started, false); + }); +}); + +test("outcome divergence triggers rollback and restores the original hash", async () => { + await withRuntime(async (runtime) => { + const execute = runtime.adapter.execute.bind(runtime.adapter); + runtime.adapter.execute = async (...args) => { + const record = await execute(...args); + await writeFile(runtime.target, "diverged\n", "utf8"); + return record; + }; + const result = await runtime.execute(); + assert.equal(result.status, "ROLLED_BACK"); + assert.equal(result.outcome.status, "ROLLED_BACK"); + assert.equal(result.outcome.final_state_digest, runtime.beforeSha256); + assert.equal(sha256(await readFile(runtime.target)), runtime.beforeSha256); + runtime.seal_authority.verify(result.evidence_seal); + }); +}); + +test("in-flight interrupt triggers rollback and a sealed restored outcome", async () => { + await withRuntime(async (runtime) => { + const execute = runtime.adapter.execute.bind(runtime.adapter); + runtime.adapter.execute = async (...args) => { + await execute(...args); + throw new CintError("CINT_EXECUTION_INTERRUPTED", "Synthetic interrupt"); + }; + const result = await runtime.execute(); + assert.equal(result.status, "ROLLED_BACK"); + assert.equal(result.error_code, "CINT_EXECUTION_INTERRUPTED"); + assert.equal(sha256(await readFile(runtime.target)), runtime.beforeSha256); + runtime.seal_authority.verify(result.evidence_seal); + }); +}); + +test("unavailable CINT dependency fails closed without consuming or acting", async () => { + await withRuntime(async (runtime) => { + const result = await runtime.execute({ receipt_authority: null }); + assert.equal(result.status, "FAIL_CLOSED"); + assert.equal(result.error_code, "CINT_UNAVAILABLE"); + assert.equal(result.action_started, false); + assert.equal((await runtime.store.inspect(runtime.receipt.id)).state, "PENDING"); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("changed target bytes after decision fail closed before action", async () => { + await withRuntime(async (runtime) => { + await writeFile(runtime.target, "external-change\n", "utf8"); + const result = await runtime.execute(); + assert.equal(result.status, "FAIL_CLOSED"); + assert.equal(result.error_code, "CINT_TARGET_STATE_CHANGED"); + assert.equal(result.action_started, false); + assert.equal(await readFile(runtime.target, "utf8"), "external-change\n"); + }); +}); From 464cd0d4564b31deeafca09c94b0b7c1920ffd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Thu, 27 Aug 2026 20:43:54 +0300 Subject: [PATCH 06/16] cint(g6): admit legacy Codex adapter --- artifacts/cint-r0/gate-ledger.json | 6 + artifacts/cint-r0/gates/CINT-R0.G6.json | 19 +++ docs/cint-r0/06_CODEX_ADAPTER.md | 47 ++++++ src/cint/adapters/codex/index.js | 128 ++++++++++++++++ src/cint/index.js | 1 + tests/cint-codex-adapter.test.js | 192 ++++++++++++++++++++++++ 6 files changed, 393 insertions(+) create mode 100644 artifacts/cint-r0/gates/CINT-R0.G6.json create mode 100644 docs/cint-r0/06_CODEX_ADAPTER.md create mode 100644 src/cint/adapters/codex/index.js create mode 100644 tests/cint-codex-adapter.test.js diff --git a/artifacts/cint-r0/gate-ledger.json b/artifacts/cint-r0/gate-ledger.json index 3373783..d61df46 100644 --- a/artifacts/cint-r0/gate-ledger.json +++ b/artifacts/cint-r0/gate-ledger.json @@ -40,6 +40,12 @@ "name": "Synthetic gated execution and rollback", "status": "PASS", "evidence": "docs/cint-r0/05_SYNTHETIC_ACTION_PROOF.md" + }, + { + "gate": "CINT-R0.G6", + "name": "Legacy Codex adapter admission", + "status": "PASS", + "evidence": "docs/cint-r0/06_CODEX_ADAPTER.md" } ], "terminal_verdict": null diff --git a/artifacts/cint-r0/gates/CINT-R0.G6.json b/artifacts/cint-r0/gates/CINT-R0.G6.json new file mode 100644 index 0000000..3cd07a2 --- /dev/null +++ b/artifacts/cint-r0/gates/CINT-R0.G6.json @@ -0,0 +1,19 @@ +{ + "gate": "CINT-R0.G6", + "status": "PASS", + "codex_adapter_tests": { + "passed": 3, + "failed": 0 + }, + "prior_tests": { + "passed": 48, + "failed": 0 + }, + "legacy_adapter": "CINT Adapter 01", + "legacy_result_authority": "EVIDENCE_ONLY", + "cint_authority_methods_exposed": 0, + "packet_drift": "REJECTED_BEFORE_EXECUTION", + "mock_backed_cint_outcome": "SEALED", + "external_mutations": 0, + "evidence": "docs/cint-r0/06_CODEX_ADAPTER.md" +} diff --git a/docs/cint-r0/06_CODEX_ADAPTER.md b/docs/cint-r0/06_CODEX_ADAPTER.md new file mode 100644 index 0000000..75b8a1c --- /dev/null +++ b/docs/cint-r0/06_CODEX_ADAPTER.md @@ -0,0 +1,47 @@ +# CINT Adapter 01 — delegated coding-agent execution + +The frozen Agent Floor kernel is now integrated beneath CINT as a read-only +action adapter. Its clean packet, source manifest, ephemeral process, +request-local telemetry, and evidence checks remain intact. + +## Authority separation + +The adapter exposes only: + +- an exact `CODEX_DELEGATED_REVIEW` action constructor; +- a packet/task binding check; +- bounded legacy execution; +- an outcome verifier. + +It exposes no CINT decision, receipt issuance, receipt consumption, outcome +seal, or self-admission operation. Every declared authority-boundary flag is +false. + +The historical `agent-floor/admission/1` record remains unchanged for +compatibility. Its `ADMITTED` or `REJECTED` value is now adapter evidence only. +It cannot authorize execution and cannot become a CINT terminal verdict. + +## Binding + +The CINT intent binds both: + +- the canonical legacy packet SHA-256 digest; and +- a safe task digest covering the worker, admission policy, delegation limits, + and a digest of the repository boundary. + +The adapter recomputes both after receipt consumption and before launching the +legacy runner. Drift stops before Codex execution. + +## Gate proof + +- the wrapper exposes no CINT authority methods; +- one mock-backed clean delegated review runs through decision, receipt, + revalidation, one-shot consumption, legacy execution, outcome verification, + ledger, and evidence seal; +- the preserved legacy result remains `ADMITTED`, while CINT independently + returns a sealed verified outcome; +- changed legacy packet/task material is rejected before the child process; +- all 48 prior CINT and legacy tests remain green. + +Agent delegation is therefore one governed action type. It no longer defines +the product or controls its own authority. diff --git a/src/cint/adapters/codex/index.js b/src/cint/adapters/codex/index.js new file mode 100644 index 0000000..eb0c35c --- /dev/null +++ b/src/cint/adapters/codex/index.js @@ -0,0 +1,128 @@ +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, + 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_binary"], "Codex adapter configuration"); + 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.capability = createAdapterCapability({ + id: this.id, + action_types: ["CODEX_DELEGATED_REVIEW"], + consequence_classes: ["READ_ONLY"], + rollback: false, + interrupt: false, + outcome_verification: true + }); + this.authority_boundary = CODEX_DELEGATION_ADAPTER.authority_boundary; + } + + async prepare(intent, options) { + verifySealedRecord(intent, "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 + }); + 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/index.js b/src/cint/index.js index 5a2e00b..0045cb3 100644 --- a/src/cint/index.js +++ b/src/cint/index.js @@ -1,5 +1,6 @@ export * from "./authority.js"; export * from "./adapters/synthetic-file-patch.js"; +export * from "./adapters/codex/index.js"; export * from "./canonical.js"; export * from "./challenge.js"; export * from "./decision.js"; diff --git a/tests/cint-codex-adapter.test.js b/tests/cint-codex-adapter.test.js new file mode 100644 index 0000000..8299e10 --- /dev/null +++ b/tests/cint-codex-adapter.test.js @@ -0,0 +1,192 @@ +import assert from "node:assert/strict"; +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 { + CodexDelegationCintAdapter, + DecisionReceiptAuthority, + ExecutionLedger, + FileReceiptStore, + OutcomeSealAuthority, + createAuthorityGrant, + createCodexDelegationAction, + createIntent, + createMachineStateSnapshot, + createPolicySnapshot, + decide, + executeWithReceipt, + resolvePrincipal +} from "../src/cint/index.js"; +import { DEMO_SPEC, PROJECT_ROOT } from "../src/demo.js"; +import { loadTaskSpec } from "../src/policy.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"; + +async function withAdapterRuntime(run) { + 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 spec = { + ...loadedSpec, + delegation: { ...loadedSpec.delegation, max_concurrency: 2 } + }; + const action = await createCodexDelegationAction(spec); + const intent = createIntent({ + id: "intent.codex-adapter.1", + principal_id: "principal.operator", + request: "Run the bounded read-only delegated review and return its verified evidence.", + 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 = resolvePrincipal({ + id: "principal.operator", + type: "HUMAN", + authenticated: true, + authority_chain: ["authority.codex-adapter.1"], + attributes: {}, + resolved_at: T0 + }); + const authority = createAuthorityGrant({ + id: "authority.codex-adapter.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-adapter"], + require_rollback: false, + issued_at: T0, + not_before: T0, + expires_at: EXPIRY + }); + const policy = createPolicySnapshot({ + id: "policy.codex-adapter", + 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 CodexDelegationCintAdapter({ spec, output_dir: output, codex_binary: mock }); + const machine_state = createMachineStateSnapshot({ + id: "machine.codex-adapter", + 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 = decide({ + id: "decision.codex-adapter.1", + ...snapshot, + now: T1, + expires_at: T2 + }); + const receipt_authority = new DecisionReceiptAuthority({ + issuer_id: "cint.receipt-authority.r0", + key: Buffer.alloc(32, 23) + }); + const receipt = receipt_authority.issue({ + id: "receipt.codex-adapter.1", + nonce: "codex-adapter-receipt-0001", + decision, + issued_at: T1 + }); + const store = new FileReceiptStore(path.join(temporary, "receipt-store")); + await store.register(receipt, { registered_at: T1 }); + const seal_authority = new OutcomeSealAuthority({ + issuer_id: "cint.seal-authority.r0", + key: Buffer.alloc(32, 29) + }); + const ledger = new ExecutionLedger(path.join(temporary, "evidence", "execution.jsonl")); + try { + return await run({ + temporary, + output, + mock, + spec, + action, + intent, + snapshot, + decision, + adapter, + receipt, + receipt_authority, + store, + seal_authority, + ledger + }); + } finally { + await rm(temporary, { recursive: true, force: true }); + } +} + +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(Object.values(adapter.authority_boundary).every((value) => value === false)); + }); +}); + +test("legacy bounded review runs only under a consumed CINT receipt", async () => { + await withAdapterRuntime(async (runtime) => { + const result = await executeWithReceipt({ + receipt: runtime.receipt, + receipt_authority: runtime.receipt_authority, + store: runtime.store, + snapshot_provider: async () => runtime.snapshot, + adapter: runtime.adapter, + seal_authority: runtime.seal_authority, + ledger: runtime.ledger, + at: T1 + }); + assert.equal(result.status, "SEALED"); + 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); + assert.equal((await runtime.store.inspect(runtime.receipt.id)).state, "CONSUMED"); + }); +}); + +test("legacy packet drift is rejected before Codex execution", async () => { + await withAdapterRuntime(async (runtime) => { + const changedSpec = { ...runtime.spec, root_task: `${runtime.spec.root_task} changed` }; + const changedAdapter = new CodexDelegationCintAdapter({ + spec: changedSpec, + output_dir: runtime.output, + codex_binary: runtime.mock + }); + await assert.rejects( + changedAdapter.prepare(runtime.intent, { at: T1 }), + (error) => error.code === "CINT_CODEX_PACKET_CHANGED" + ); + }); +}); From 7c31ba85907fe811a3edfd3b12156cfdd560ced9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Thu, 27 Aug 2026 20:52:01 +0300 Subject: [PATCH 07/16] cint(g7): reassemble product identity and documentation --- CHANGELOG.md | 18 ++ CINT_EVALUATION.md | 27 +++ CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 24 +- NOTICE | 12 +- README.md | 223 ++++++------------ SECURITY.md | 38 ++- artifacts/cint-r0/gate-ledger.json | 6 + artifacts/cint-r0/gates/CINT-R0.G7.json | 19 ++ bin/cint.js | 11 + docs/ARCHITECTURE.md | 192 +++++++-------- docs/CODEX-ADAPTER.md | 38 +++ docs/LIMITATIONS.md | 60 ++--- docs/PRIVACY.md | 42 +++- docs/RELEASE.md | 60 ++--- docs/SECURITY-MODEL.md | 94 ++++---- docs/THREAT-MODEL.md | 165 ++++++++++--- docs/archive/README.md | 17 ++ docs/archive/agent-floor-v0.1/ARCHITECTURE.md | 117 +++++++++ .../agent-floor-v0.1}/CODEX-ADOPTION.md | 2 +- docs/archive/agent-floor-v0.1/LIMITATIONS.md | 29 +++ docs/archive/agent-floor-v0.1/PRIVACY.md | 21 ++ docs/archive/agent-floor-v0.1/README.md | 173 ++++++++++++++ docs/archive/agent-floor-v0.1/RELEASE.md | 27 +++ .../agent-floor-v0.1/SECURITY-MODEL.md | 53 +++++ docs/archive/agent-floor-v0.1/SECURITY.md | 25 ++ docs/archive/agent-floor-v0.1/THREAT-MODEL.md | 39 +++ .../agent-floor-v0.1/media}/architecture.svg | 2 +- .../media}/screenshots/af-g0-proof.svg | 2 +- .../build-week-2026/CODEX_EVALUATION.md | 2 +- .../build-week-2026}/JUDGE-GUIDE.md | 2 +- docs/cint-r0/07_DOCUMENTATION_REASSEMBLY.md | 51 ++++ package-lock.json | 11 +- package.json | 12 +- scripts/publication-audit.mjs | 4 +- src/cint/cli.js | 49 ++++ src/cint/index.js | 1 + tests/cint-cli.test.js | 19 ++ 38 files changed, 1252 insertions(+), 437 deletions(-) create mode 100644 CINT_EVALUATION.md create mode 100644 artifacts/cint-r0/gates/CINT-R0.G7.json create mode 100755 bin/cint.js create mode 100644 docs/CODEX-ADAPTER.md create mode 100644 docs/archive/README.md create mode 100644 docs/archive/agent-floor-v0.1/ARCHITECTURE.md rename docs/{ => archive/agent-floor-v0.1}/CODEX-ADOPTION.md (99%) create mode 100644 docs/archive/agent-floor-v0.1/LIMITATIONS.md create mode 100644 docs/archive/agent-floor-v0.1/PRIVACY.md create mode 100644 docs/archive/agent-floor-v0.1/README.md create mode 100644 docs/archive/agent-floor-v0.1/RELEASE.md create mode 100644 docs/archive/agent-floor-v0.1/SECURITY-MODEL.md create mode 100644 docs/archive/agent-floor-v0.1/SECURITY.md create mode 100644 docs/archive/agent-floor-v0.1/THREAT-MODEL.md rename {media => docs/archive/agent-floor-v0.1/media}/architecture.svg (98%) rename {media => docs/archive/agent-floor-v0.1/media}/screenshots/af-g0-proof.svg (96%) rename CODEX_EVALUATION.md => docs/archive/build-week-2026/CODEX_EVALUATION.md (97%) rename docs/{ => archive/build-week-2026}/JUDGE-GUIDE.md (98%) create mode 100644 docs/cint-r0/07_DOCUMENTATION_REASSEMBLY.md create mode 100644 src/cint/cli.js create mode 100644 tests/cint-cli.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 406478f..6aba3ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## CINT-R0 — local review candidate + +- Reassembled the active product as SI1 CINT, Machine Counterintelligence + Runtime. +- Added canonical intent, principal, authority, policy, challenge, decision, + machine-state, receipt, revalidation, outcome, rollback, ledger, and seal + protocols. +- Added signed action-bound receipts with atomic one-shot consumption, replay + rejection, expiry, revocation, and fail-closed locks. +- Added the disposable synthetic action proof with immediate revalidation, + exact outcome verification, interrupt, rollback, and hash restoration. +- Isolated Agent Floor as CINT Adapter 01 while preserving historical APIs, + protocols, tests, fixtures, and evidence. +- Archived competition and historical Agent Floor narrative outside the active + product documentation. +- Kept the candidate local with no push, publication, release, package + publication, repository rename, or external announcement. + ## v0.1.0-af-g0 — 2026-07-18 - Added clean, self-contained child packets with explicit lineage and authority. diff --git a/CINT_EVALUATION.md b/CINT_EVALUATION.md new file mode 100644 index 0000000..3a4f4bf --- /dev/null +++ b/CINT_EVALUATION.md @@ -0,0 +1,27 @@ +# Independent CINT-R0 evaluation + +Use a clean local checkout of the candidate branch. Do not push, publish, +release, rename, or operate external services. + +```sh +npm ci +npm test +npm run cint:identity +npm run verify +``` + +Require all of the following: + +- every test passes with zero failures; +- the historical AF-G0 demo remains reproducible; +- dependency, publication, and immutable historical evidence checks pass; +- `cint:identity` reports `LOCAL_R0_CANDIDATE` and publication authority + `NONE`; +- `artifacts/cint-r0/gate-ledger.json` contains one ordered pass per completed + gate; +- Git status is clean after verification; +- the branch has no remote publication side effect. + +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 +model before accepting the local candidate for operator review. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 953cc18..dc51bbb 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,6 +1,6 @@ # Code of conduct -Participation in Agent Floor must remain technically rigorous and professionally respectful. +Participation in SI1 CINT must remain technically rigorous and professionally respectful. Contributors must: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93d26cd..d6e4ef7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,36 +2,48 @@ ## Development setup -Agent Floor requires Node.js 20 or newer and has no runtime package dependencies. +SI1 CINT requires Node.js 20 or newer and has no runtime package dependencies. ```sh npm ci npm test +npm run cint:identity npm run demo npm run public:audit ``` -`npm run demo` is deterministic and makes no model call. A native Codex run is optional and must be explicitly intended by the operator. +`npm run demo` is the deterministic historical Adapter 01 regression and makes +no model call. A native Codex adapter run is optional and must be explicitly +authorized by the operator. ## Change discipline -- Keep worker packets self-contained and bounded. -- Never introduce full-history delegation or implicit resume behavior. +- Preserve the invariant: no consequential action without current + decision-bound authority. +- Keep decisions non-executable and receipts exact, expiring, one-shot, and + immediately revalidated. +- Keep adapters unable to decide, issue, consume, self-admit, or seal. +- Keep legacy worker packets self-contained and bounded. +- Never introduce full-history delegation or implicit resume behavior into the + Codex adapter. - Preserve the operator-selected root model and reasoning setting. - Classify each control as policy, mechanical enforcement, telemetry interpretation, or semantic admission. - Attribute usage from the worker's own boundary; never assign inherited cumulative totals to the worker. - Add a regression test for every accounting or admission defect. -- Keep AF-G0 read-only; mutation requires a separate authority design. +- Preserve every AF-G0 observable as historical Adapter 01 compatibility. +- Add negative controls for replay, drift, fail-open, verification, and rollback + changes. ## Public evidence rules Use synthetic or explicitly sanitized fixtures. Do not commit credentials, email addresses, session identifiers, local absolute paths, raw native logs, private source text, or private project names. Do not make billing or provider-quota claims from local telemetry. -Run the publication audit before opening a pull request: +Run the complete local verification before requesting review: ```sh npm run public:audit npm audit +npm run evidence:verify ``` ## Pull requests diff --git a/NOTICE b/NOTICE index 2932d9b..462a65e 100644 --- a/NOTICE +++ b/NOTICE @@ -1,10 +1,10 @@ -Agent Floor -Copyright 2026 Agent Floor contributors +SI1 CINT +Copyright 2026 SI1 CINT contributors -Agent Floor is an open-source developer tool for governed Codex -delegation, bounded worker execution, request-local accounting, -and evidence-based result admission. +SI1 CINT is an open-source Machine Counterintelligence Runtime for +decision-bound consequential action. Agent Floor v0.1 is preserved as +the historical Codex delegation adapter and regression foundation. -"Agent Floor" and "Sentelligence One" may be trademarks or product +"SI1 CINT", "Agent Floor", and "Sentelligence One" may be trademarks or product identifiers of their respective owner. The Apache License 2.0 grants copyright and patent permissions but does not grant trademark rights. diff --git a/README.md b/README.md index faf7ad4..5abca2f 100644 --- a/README.md +++ b/README.md @@ -1,173 +1,102 @@ -# Agent Floor +# SI1 CINT -A Codex worker appeared to consume **555.3M tokens**. +**Machine Counterintelligence Runtime** -Its verified request-local usage was **1,492,621 tokens**. +> No consequential action without current authority. -The **372× discrepancy** came from inherited cumulative history after `fork_turns="all"`. Agent Floor prevents this class of error before delegation and audits every worker afterward. +CINT prevents silent, stale, replayed, or unauthorized intent from becoming +machine action. It reconstructs the requested intent, resolves the principal +and authority, challenges current policy and machine state, and emits one of +`ADMIT`, `DENY`, or `REVIEW`. Only an `ADMIT` decision can receive a signed, +action-bound, expiring, one-shot receipt. A decision alone carries no executable +authority. -**Release gate:** AF-G0 PASS · **Version:** `v0.1.0-af-g0` · **Runtime:** Node.js 20+ - -![Agent Floor architecture](media/architecture.svg) - -## What Agent Floor is - -Agent Floor is a zero-dependency Node.js control layer for governed Codex delegation. It turns a root task into a self-contained child packet, starts a fresh bounded Codex process, attributes only request-local usage, and admits a result only when the parent can verify its evidence and semantic contract. - -Four layers remain separate: - -| Layer | Responsibility | -|---|---| -| Policy controls | Declare allowed models, reasoning settings, context mode, authority, depth, concurrency, cycles, runtime, output, and token ceilings. | -| Mechanically enforced controls | Reject full-history inheritance, create a clean ephemeral process, disable child fan-out, project only allowlisted files, and terminate limit violations. | -| Telemetry interpretation | Deduplicate usage events and calculate post-boundary input, cached input, fresh input, output, and incremental totals. | -| Semantic evidence admission | Verify file, line, excerpt, source hash, finding code, status, required terms, and forbidden terms before admitting a result. | - -## The failure it prevents - -A cumulative counter can include work that existed before a child request. Treating that counter as the child total creates false attribution. - -| Quantity | Tokens | Interpretation | -|---|---:|---| -| Raw cumulative counter | 555,300,000 | Inherited baseline plus child work | -| Request-local increment | 1,492,621 | Sixteen unique post-boundary request records | -| Overstatement removed | 553,807,379 | Raw counter minus verified child increment | -| Correction | 372.03× | Raw counter divided by verified child increment | - -The sanitized regression preserves the measured request-local split: 1,380,352 cached input tokens, 103,132 fresh input tokens, and 9,137 output tokens. It adds a declared synthetic baseline solely to reproduce the 555.3M failure shape. +```text +request + -> intent + -> principal + authority + -> policy + machine-state challenge + -> ADMIT / DENY / REVIEW + -> signed one-shot receipt + -> immediate revalidation + -> atomic receipt consumption + -> action adapter + -> outcome verification + -> evidence seal or rollback +``` -## How governed delegation works +## R0 state -1. Validate a root-owned task specification. -2. Reject any context mode other than `clean` and any `fork_turns` value other than `none`. -3. Build a canonical packet no larger than 8 KiB with lineage, authority, limits, source hashes, and output schema. -4. Copy only allowlisted regular files into a temporary read-only authority projection. -5. Start a new ephemeral Codex process with inherited rules, configuration, apps, plugins, memories, browser tools, computer tools, image tools, and multi-agent fan-out disabled. Pass only an explicit runtime environment allowlist and exclude common secret-bearing variable names from worker shells. -6. Monitor model/tool cycles, runtime, output bytes, and process status. -7. Audit only request-local JSONL usage or verified post-boundary legacy deltas. -8. Reopen the original sources and admit the result only if every deterministic evidence and semantic check passes. +`CINT-R0` is a local successor candidate. Publication, push, release, package +publication, repository rename, and external announcement authority are all +absent. -The worker may recommend accepting or rejecting the target under review. That recommendation never controls whether the worker's own result is admitted; only the parent gate does. +The R0 proof establishes: -## Architecture +- strict canonical objects and JSON schemas; +- explicit principal, authority, policy, machine-state, and counter-intent + contracts; +- `ADMIT / DENY / REVIEW` decisions with zero executable authority; +- HMAC-authenticated receipts bound to the exact decision, action, target, + context, authority epoch, policy epoch, adapter, and machine state; +- file-backed atomic one-shot consumption, replay rejection, expiry, and + fail-closed crash locks; +- event-bound revalidation before and after receipt consumption; +- synthetic consequential action, outcome verification, interrupt, rollback, + hash restoration, execution ledger, and evidence seal; +- Agent Floor imported as **CINT Adapter 01 — Delegated Coding-Agent + Execution**. -The root-to-child boundary is one-way: the child receives a bounded packet and an allowlisted source projection, then returns schema-constrained output and request-local telemetry. It cannot resume the parent task or spawn another worker. +## Adapters -See [Architecture](docs/ARCHITECTURE.md), [Security model](docs/SECURITY-MODEL.md), [Threat model](docs/THREAT-MODEL.md), and [Privacy model](docs/PRIVACY.md). +| Adapter | Action | Consequence | Authority | +|---|---|---|---| +| Synthetic file patch | `SYNTHETIC_FILE_PATCH` | Disposable consequential proof | Executes only with a consumed CINT receipt; verifies or restores exact bytes | +| Codex delegation | `CODEX_DELEGATED_REVIEW` | Read-only delegated review | Reports legacy evidence only; cannot decide, issue, consume, admit itself, or seal | -## Quick start +## Verify locally ```sh -git clone https://github.com/sntllgnc/agent-floor.git -cd agent-floor npm ci npm test -npm run demo +npm run cint:identity +npm run verify ``` -No package dependency is installed; `npm ci` verifies the frozen package metadata. - -## Deterministic demonstration - -`npm run demo` makes no model call. It evaluates the public sanitized fixture and writes reproducible detail records to the ignored `artifacts/generated/` directory. +The historical `npm run demo` remains available as the AF-G0 Adapter 01 +regression. It is not the CINT product definition. -Expected proof: +## Repository map ```text -14 tests passed -fork_turns="all" -> REJECTED -raw cumulative attribution -> 555,300,000 -verified incremental usage -> 1,492,621 -overstatement removed -> 553,807,379 -correction -> 372.03x -evidence result -> ADMITTED -contradictory semantic control -> REJECTED +src/cint/ CINT protocols and execution boundary +src/cint/adapters/ R0 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 +docs/cint-r0/ gate evidence and local review packet +docs/archive/ historical Agent Floor and competition material +artifacts/cint-r0/ gate ledger and sanitized gate receipts ``` -The frozen release record is [artifacts/af-g0.json](artifacts/af-g0.json). - -## Native Codex execution - -Validate the installed Codex surface without a model call: - -```sh -npm run doctor -``` - -Run a governed child only when authenticated local Codex execution is intended: - -```sh -node ./bin/agent-floor.js run ./examples/live-smoke-task.json --out ./artifacts/runs/native -``` - -The release's redacted native smoke used a clean GPT-5.6 Terra child at medium reasoning: one model call, two monitored cycles, 19,417 incremental tokens, and an admitted evidence record. No raw native event stream is published. See [artifacts/native-smoke-summary.json](artifacts/native-smoke-summary.json). - -## Evidence admission - -An evidence item is admitted only when: +## Design and review -- its repository-relative path was granted to the child; -- its one-based line exists; -- the exact excerpt occurs on that normalized line; -- the source still matches the packet-time SHA-256; -- the result satisfies parent-declared status, finding-code, required-term, forbidden-term, and evidence-count rules; -- execution succeeded within the declared bounds; -- measured incremental usage remains within the admission ceiling. - -The fixture also includes a negative control that cites two real lines but reaches the opposite conclusion. Agent Floor rejects it. Real citations are necessary, not sufficient. - -## AF-G0 verification - -AF-G0 passes only when one command proves all of the following: - -- clean context with zero inherited turns; -- mechanical rejection of full-history delegation; -- bounded model, reasoning, depth, concurrency, cycle, runtime, and output policy; -- request-local usage accounting with duplicate removal; -- reproduction of the 555.3M versus 1.49M attribution defect; -- deterministic evidence admission and semantic rejection; -- no dependency on a private repository. - -Run `npm test`, `npm run demo`, `npm run public:audit`, and `npm run evidence:verify`. The release evidence inventory is [artifacts/evidence-manifest.json](artifacts/evidence-manifest.json). - -## Security and privacy - -Agent Floor executes locally. It does not upload raw logs, modify Codex credentials, or publish operator task history. The release contains only synthetic source files, sanitized telemetry-shaped fixtures, and a redacted native summary. - -The child sees a temporary read-only projection instead of the original repository. The Codex process receives a small runtime environment allowlist instead of the complete parent environment; worker shells retain default filtering and add common key, secret, token, password, credential, and authentication name exclusions. The public package is scanned for local absolute paths, common credential formats, email addresses, session UUIDs, hidden metadata, raw logs, and private project names. - -See [SECURITY.md](SECURITY.md) for vulnerability reporting and [Security model](docs/SECURITY-MODEL.md) for trust boundaries. - -## Limitations and non-claims - -Agent Floor verifies request-local telemetry attribution. It does **not** establish an account billing total, explain a provider quota decision, or assert that cached input is free. A token ceiling is enforced at admission after the completed request reports usage; it is not a pre-request provider-side spending cap. - -AF-G0 is read-only. A later mutation phase must consume an admitted record under a separate explicit authority grant. See [Limitations](docs/LIMITATIONS.md). - -## How Codex and GPT-5.6 were used - -Codex implemented and verified the runner, accounting logic, fixtures, tests, evidence gate, documentation, and publication audit. GPT-5.6 Terra executed the redacted native smoke. The public deterministic demo and test suite do not require model access. - -Agent Floor preserves the operator-selected root reasoning setting. Its policy accepts `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra`, and `max`; the selected value is recorded rather than silently reduced. Model identifiers and reasoning tiers remain environment-dependent capabilities, not repository promises. - -The runner is built around Codex non-interactive JSONL execution, ephemeral sessions, structured output, and configuration controls described in the official [non-interactive mode](https://learn.chatgpt.com/docs/non-interactive-mode), [subagent configuration](https://learn.chatgpt.com/docs/agent-configuration/subagents), and [configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference). - -## Codex adoption - -Another Codex task can apply the policy without inheriting this repository's development history. Use the bounded instructions in [Codex adoption](docs/CODEX-ADOPTION.md). - -## Build Week judge instructions - -```sh -git clone https://github.com/sntllgnc/agent-floor.git -cd agent-floor -npm ci -npm test -npm run demo -``` +- [Architecture](docs/ARCHITECTURE.md) +- [Security model](docs/SECURITY-MODEL.md) +- [Threat model](docs/THREAT-MODEL.md) +- [Privacy](docs/PRIVACY.md) +- [Limitations](docs/LIMITATIONS.md) +- [Codex Adapter 01](docs/CODEX-ADAPTER.md) +- [CINT-R0 gate ledger](artifacts/cint-r0/gate-ledger.json) +- [Local release-candidate state](docs/RELEASE.md) -The complete judge path, expected fields, and under-three-minute narration are in [Judge guide](docs/JUDGE-GUIDE.md). A separate fresh-task packet is available in [Independent Codex evaluation](CODEX_EVALUATION.md). +The immutable Agent Floor baseline remains the tagged release +`v0.1.0-af-g0`. Its narrative, evaluation material, media, fixtures, protocols, +and evidence remain available as historical lineage and regression proof. -## Contributing and licence +## Licence and contribution -Contributions are governed by [CONTRIBUTING.md](CONTRIBUTING.md), [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md), and repository instructions in [AGENTS.md](AGENTS.md). Code, documentation, and sanitized fixtures are licensed under [Apache-2.0](LICENSE); [NOTICE](NOTICE) preserves attribution and the separate trademark boundary. Release evidence is summarized in [Release v0.1.0-af-g0](docs/RELEASE.md) and [CHANGELOG.md](CHANGELOG.md). +Code, documentation, schemas, and sanitized fixtures are licensed under +[Apache-2.0](LICENSE). Contribution and conduct requirements are in +[CONTRIBUTING.md](CONTRIBUTING.md), [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md), +and [AGENTS.md](AGENTS.md). Security reports follow [SECURITY.md](SECURITY.md). diff --git a/SECURITY.md b/SECURITY.md index 407a37f..677ee73 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,25 +1,41 @@ # Security policy -## Supported release +## Supported line -Security fixes are applied to the latest `0.1.x` release line. +Security corrections for the local CINT-R0 candidate are developed on the +successor branch. The immutable Agent Floor release remains historical evidence. ## Report a vulnerability -Use GitHub's private vulnerability reporting feature for this repository. Include the affected version, control boundary, reproduction steps, impact, and the smallest sanitized evidence needed to verify the report. +Use the repository's private vulnerability reporting channel. Include the +affected revision, violated CINT invariant, realistic attacker starting +capability, action or evidence boundary, reproduction steps, impact, and the +smallest sanitized proof required to verify the report. -Do not place secrets, credentials, private source material, raw Codex logs, or operator conversation history in a public issue. If private reporting is unavailable, open a minimal public issue requesting a private coordination channel without including vulnerability details. +Do not place credentials, receipt or seal keys, private source, raw Codex logs, +operator requests, local paths, or vulnerability-triggering payloads in a public +issue. If private reporting is unavailable, request a private coordination +channel without vulnerability details. -## Data and credential boundary +## Reportable boundaries -Agent Floor executes locally. It does not require raw log upload, does not modify Codex credentials, and does not publish authentication material. The runner uses the already authenticated local Codex command surface when a user explicitly invokes a native run. +- unauthorized decision or receipt issuance; +- receipt forgery, replay, expiry, revocation, or atomic-consumption bypass; +- action, target, context, principal, authority, policy, adapter, or + machine-state binding bypass; +- fail-open execution when a required CINT component is unavailable; +- synthetic containment, verification, interrupt, or rollback failure; +- evidence-ledger or seal integrity bypass; +- Codex Adapter 01 escape from its read-only source or zero-authority boundary; +- disclosure of signing keys, credentials, private requests, or raw evidence. -The public repository contains sanitized fixtures only. Native proof is reduced to aggregate usage, control state, evidence references, and content hashes. +## Non-security accounting boundary -## Billing and quota boundary - -Agent Floor reports request-local telemetry. Security reports and project documentation must not convert that telemetry into an unsupported billing total, subscription debit, or provider quota-causation claim. +Legacy Adapter 01 telemetry is request-local execution evidence. It is not an +account billing total, subscription debit, or provider quota-causation formula. ## Disclosure handling -Reports will be acknowledged, reproduced against the smallest safe fixture, classified by affected boundary, and corrected with regression evidence. Coordinated disclosure should occur only after a fix or documented mitigation is available. +Reports are reproduced against the smallest safe local fixture, classified by +the affected trust boundary, corrected with negative and positive conformance +evidence, and coordinated before any public disclosure. diff --git a/artifacts/cint-r0/gate-ledger.json b/artifacts/cint-r0/gate-ledger.json index d61df46..777bbc0 100644 --- a/artifacts/cint-r0/gate-ledger.json +++ b/artifacts/cint-r0/gate-ledger.json @@ -46,6 +46,12 @@ "name": "Legacy Codex adapter admission", "status": "PASS", "evidence": "docs/cint-r0/06_CODEX_ADAPTER.md" + }, + { + "gate": "CINT-R0.G7", + "name": "Documentation and technology reassembly", + "status": "PASS", + "evidence": "docs/cint-r0/07_DOCUMENTATION_REASSEMBLY.md" } ], "terminal_verdict": null diff --git a/artifacts/cint-r0/gates/CINT-R0.G7.json b/artifacts/cint-r0/gates/CINT-R0.G7.json new file mode 100644 index 0000000..c1a064d --- /dev/null +++ b/artifacts/cint-r0/gates/CINT-R0.G7.json @@ -0,0 +1,19 @@ +{ + "gate": "CINT-R0.G7", + "status": "PASS", + "cint_cli_tests": { + "passed": 2, + "failed": 0 + }, + "prior_tests": { + "passed": 51, + "failed": 0 + }, + "active_product": "SI1 CINT", + "legacy_product": "CINT Adapter 01", + "historical_material": "docs/archive", + "package": "si1-cint@0.1.0-cint-r0", + "publication_authority": "NONE", + "remote_mutations": 0, + "evidence": "docs/cint-r0/07_DOCUMENTATION_REASSEMBLY.md" +} diff --git a/bin/cint.js b/bin/cint.js new file mode 100755 index 0000000..389c5f7 --- /dev/null +++ b/bin/cint.js @@ -0,0 +1,11 @@ +#!/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/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d6240de..09989d9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,117 +1,87 @@ -# Architecture - -Agent Floor separates intent, enforcement, measurement, and adjudication. A policy declaration is not reported as a mechanical guarantee; telemetry is not reported as billing; a valid citation is not treated as a valid conclusion. - -```mermaid -flowchart LR - A["Root task specification"] --> B["Policy gate"] - B -->|invalid context or limits| X["REJECTED"] - B --> C["Canonical clean packet and lineage hash"] - C --> D["Allowlisted source projection"] - D --> E["Ephemeral bounded Codex child"] - E --> F["Request-local JSONL usage"] - E --> G["Schema-constrained result"] - F --> H["Incremental auditor"] - G --> I["Evidence and semantic verifier"] - H --> J["Parent admission gate"] - I --> J - J -->|all checks pass| K["ADMITTED"] - J -->|any check fails| X -``` - -## Control classification - -| Class | Examples | Authority | -|---|---|---| -| Policy controls | Allowlisted model, selected reasoning, maximum depth, concurrency, cycles, runtime, output, and incremental-token admission ceiling | Root task specification validated before execution | -| Mechanical controls | New ephemeral process, no resume path, clean packet, ignored inherited rules/configuration, read-only projection, disabled child fan-out, process lock, event monitor, timer | Runner and operating-system process boundary | -| Telemetry interpretation | Unique request usage, cached/fresh split, cumulative baseline, boundary delta, duplicate removal | Auditor over JSONL records | -| Semantic evidence admission | Allowed status and finding codes, required/forbidden terms, minimum evidence, exact file-line-excerpt verification, source hash | Parent gate after execution | - -## Context boundary - -The runner never forks or resumes a parent task. It starts `codex exec` as a new ephemeral process and sends one canonical packet through standard input. User configuration and project rules are ignored for the child. Project documentation is capped at zero bytes and replaced with a generated bounded instruction file. - -The packet records: - -- `context.mode: clean` and `inherited_turns: 0`; -- parent and child lineage; -- repository boundary and allowlisted authority; -- model and reasoning selection; -- depth, concurrency, cycle, runtime, output, and token-admission limits; -- source hashes and evidence requirements; -- final output schema; -- canonical packet SHA-256. - -The packet is limited to 8 KiB. Full conversation history has no packet field and `fork_turns="all"` fails validation before a child process starts. - -## Authority boundary - -Only task-declared regular files whose resolved paths remain inside the repository boundary are accepted. Before execution, those files are copied into a temporary projection. Codex runs from that projection in a read-only sandbox. The original repository is not the child working directory. - -Before admission, Agent Floor re-hashes the original sources and rejects drift. Each evidence item must: - -1. use an allowlisted repository-relative path; -2. resolve inside the original boundary; -3. cite an existing one-based line; -4. supply an excerpt contained in that normalized line; -5. carry a non-empty claim. - -The admitted evidence record hashes `path:line:normalized-line`, binding the decision to observed content. - -## Delegation and resource boundary - -The root-to-child depth is one. Inside the child process, multi-agent tools and fan-out are disabled and child `agents.max_depth` is zero. A repository-scoped cross-process lock enforces the declared concurrency ceiling. The event monitor counts model/tool cycles and terminates the process when the hard cycle, runtime, or output boundary is crossed. - -The token limit is different: completed request usage is measured from the returned telemetry and checked before admission. It is not a provider-side preflight spending cap. - -## Usage equations - -For current `codex exec --json` traces: - -```text -incremental_tokens = sum(turn.completed.usage.input_tokens - + turn.completed.usage.output_tokens) -fresh_input_tokens = input_tokens - cached_input_tokens -``` - -`reasoning_output_tokens` is reported separately but is not added again because it is a subset of output tokens. - -For legacy rollout traces: - -```text -incremental_tokens = sum(unique post-boundary last_token_usage.total_tokens) -cumulative_delta = last cumulative total - pre-boundary cumulative total -verified = incremental_tokens == cumulative_delta -``` - -A rollout without an explicit worker boundary is rejected. Exact duplicate token events are fingerprinted and removed. Cumulative totals remain diagnostic telemetry; they are never presented as request-local cost or account billing. +# CINT architecture -## Admission state machine +## Governing invariant -```mermaid -stateDiagram-v2 - [*] --> Candidate - Candidate --> Rejected: process or policy violation - Candidate --> Rejected: context is not clean - Candidate --> Rejected: incremental token ceiling exceeded - Candidate --> Rejected: schema, semantic, or evidence failure - Candidate --> Rejected: source drift - Candidate --> Admitted: every deterministic check passes - Admitted --> [*] - Rejected --> [*] -``` +No consequential action occurs without current decision-bound authority. -A worker's `recommendation` concerns the inspected target. It cannot admit or reject its own result. Parent-declared execution, usage, source-integrity, evidence, and semantic rules control admission. +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. -## Artifact flow +## Control planes -| Artifact | Created by | Consumed by | +| Plane | Responsibility | Cannot do | |---|---|---| -| Task specification | Root operator | Policy gate | -| Child packet | Packet builder | Runner and admission gate | -| Source projection | Runner | Child process | -| JSONL event stream | Codex child | Usage auditor | -| Worker output | Codex child | Admission gate | -| Admission record | Parent gate | Root task or later authorized phase | -| Evidence manifest | Release process | Public verifier | +| Canonical | Strict objects, exact fields, canonical JSON, digests, immutable records | Decide or execute | +| Intent | Reconstruct request, action, target, context, effects, uncertainties | Grant authority | +| Principal and authority | Resolve actor, delegation chain, exact grants, epochs, validity, revocation | Execute an action | +| Policy and challenge | Compare current policy, adapter capability, machine state, and counter-intent | Mint a receipt | +| Decision | Emit `ADMIT`, `DENY`, or `REVIEW`; bind every current digest | Cross the execution boundary | +| Receipt authority | Authenticate an eligible `ADMIT` binding and lifetime | Reconstruct or widen the decision | +| Receipt store | Register, lock, revalidate, consume once, reject replay | Decide policy | +| Execution gate | Require services, revalidate, consume, revalidate again, invoke exact adapter | Mint or reinterpret authority | +| Outcome and rollback | Verify final state or restore the pre-action state | Seal itself | +| Ledger and seal | Hash-chain events and authenticate terminal evidence | Authorize another action | + +## Decision and execution sequence + +1. `createIntent` canonicalizes the explicit request and calculates independent + intent, action, target, and context digests. +2. Principal resolution and an authority grant establish identity, delegation + chain, exact adapter/action/target scope, epoch, and lifetime. +3. A policy snapshot and adapter capability establish current allowed actions, + rollback requirements, and review conditions. +4. The counter-intent challenge denies silent or undeclared effects, mismatched + principals, unavailable state, stale authority, disallowed targets, and + incapable adapters. Material uncertainty routes to `REVIEW`. +5. The decision binds every digest and explicitly records + `execution_authority: NONE`. +6. The receipt authority signs only a receipt-eligible `ADMIT` decision. +7. The store obtains an exclusive receipt lock, invokes immediate revalidation, + and writes exactly one terminal consumed or rejected record. +8. The execution gate obtains a fresh snapshot and revalidates again. Drift + after consumption still blocks action. +9. The selected adapter verifies its current target state and performs only the + receipt-bound action. +10. Outcome verification either confirms the intended final digest or invokes + rollback. Only verified or demonstrably restored outcomes can be sealed. + +## Atomicity and failure posture + +The receipt store uses exclusive file creation for registration, lock, and +terminal records. A concurrent caller, existing terminal record, or ambiguous +lock is rejected. A crash does not trigger guessed lock recovery. The receipt +remains unusable until an explicit higher-authority recovery protocol exists. + +Execution fails closed when the receipt authority, store, snapshot provider, +adapter, ledger, or seal authority is unavailable. If failure occurs after a +synthetic action begins, the gate invokes rollback and verifies the restored +hash before sealing the restored outcome. + +## Adapter boundary + +Adapters implement preparation, execution, outcome verification, and optional +rollback. They receive no receipt or seal key. They cannot resolve principals, +change authority, issue decisions, consume receipts, or seal outcomes. + +The Codex delegation adapter binds the preserved Agent Floor packet and task +digests, then executes the existing clean, bounded, read-only runner. Its +historical `ADMITTED/REJECTED` field is adapter evidence, not CINT authority. + +## Persistence + +- CINT records are canonical immutable JSON objects with content digests. +- Receipt-store filenames are hashes of receipt identifiers. +- Ledger entries contain payload digests and the preceding entry digest. +- Evidence seals authenticate the receipt, consumption, revalidation, outcome, + and ledger head. +- Operational receipt keys, seal keys, absolute adapter paths, and raw action + bytes are never embedded in receipts or seals. + +## Historical compatibility + +The original policy, packet, audit, admission, and runner implementations live +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. diff --git a/docs/CODEX-ADAPTER.md b/docs/CODEX-ADAPTER.md new file mode 100644 index 0000000..0396e54 --- /dev/null +++ b/docs/CODEX-ADAPTER.md @@ -0,0 +1,38 @@ +# CINT Adapter 01 — Codex delegation + +Codex delegation is one action type beneath CINT. The adapter preserves the +Agent Floor v0.1 clean-context and evidence kernel while removing its ability to +define final authority. + +## Preserved controls + +- strict task validation and `fork_turns="all"` rejection; +- clean self-contained packet and source manifest; +- allowlisted temporary source projection; +- ephemeral read-only Codex process; +- disabled nested fan-out and nonessential product features; +- restricted process and shell environment inheritance; +- cycle, runtime, output, concurrency, and request-local token controls; +- deduplicated request-local usage accounting; +- exact path, line, excerpt, source-hash, and semantic evidence checks. + +## New CINT boundary + +The adapter action binds the canonical legacy packet digest and a safe task +digest. The CINT core independently requires: + +1. explicit read-only delegation intent; +2. current principal, exact authority grant, policy, adapter capability, and + machine-state decision; +3. signed one-shot receipt; +4. immediate revalidation and consumption; +5. a second event-bound revalidation; +6. legacy execution and adapter outcome verification; +7. CINT ledger and evidence seal. + +The legacy result may be `ADMITTED` or `REJECTED`; both are evidence about the +delegated review. Neither value grants CINT authority. The wrapper exports no +decision, receipt, consumption, self-admission, or seal method. + +The original CLI remains available only through `agent-floor` or the explicit +`cint legacy` compatibility command. diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index f79c003..c685f2f 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -1,29 +1,31 @@ -# Limitations and non-claims - -## Telemetry, billing, and quota - -Agent Floor audits local request telemetry. It does not read authoritative account billing data and does not claim that the verified incremental total equals a bill, subscription debit, or quota formula. Cached input is reported separately; the repository does not classify it as free. - -The 555.3M fixture demonstrates an attribution defect: a cumulative counter was assigned to a worker despite a much smaller post-boundary increment. It does not prove why any provider-side usage meter changed. - -## Token ceilings - -The runner evaluates incremental usage after a completed request reports usage. It can reject an over-ceiling result from admission, but cannot guarantee that the provider performed no work beyond the ceiling. Cycle, runtime, and output limits are active process controls; the token ceiling is a post-response admission control. - -## Execution environment - -The runner depends on the installed Codex executable honoring its documented flags and emitting recognizable JSONL. `npm run doctor` verifies the required command-line surface, not the integrity of the executable or service. - -Model identifiers and reasoning tiers are environment-dependent. Policy acceptance means Agent Floor will preserve and record a declared selection; it does not guarantee that every account or Codex build exposes that model or tier. - -## Evidence - -File-line-excerpt and source-hash verification establishes that cited text exists and remained unchanged. It does not establish that the source itself is true, complete, or authoritative. Parent semantic assertions are task-specific and may be incomplete. - -## Authority - -AF-G0 is a read-only discovery and admission gate. It does not authorize source modification, deployment, publication, issue creation, messaging, or credential changes. A later action phase needs a separate explicit grant and should consume only admitted evidence. - -## Platform scope - -The implementation targets Node.js 20+ and the Codex non-interactive command surface verified by `npm run doctor`. Windows path and process behavior have not been validated in AF-G0. +# CINT-R0 limitations and non-claims + +- R0 is a local protocol and proof runtime, not a distributed authorization + service. +- Receipt and seal key generation, persistence, rotation, and hardware + protection belong to the embedding process. +- File-backed receipt consumption is atomic on one compatible local filesystem; + cross-host consensus and network filesystems are not established. +- Ambiguous locks fail closed. R0 deliberately provides no automatic stale-lock + deletion or recovery authority. +- Revalidation is event-bound, not continuous at every CPU instruction. Future + adapters that touch concurrent external systems need an adapter-specific + transaction or conditional-write mechanism. +- The synthetic adapter proves one disposable existing-file mutation. It does + not authorize mutation of another repository or live machine state. +- The Codex adapter is read-only toward its source projection. Its output + directory is writable local evidence state and requires operator review before + sharing. +- Adapter outcome verification is only as strong as the adapter-specific + verifier. R0 supplies exact byte verification for the synthetic proof and + packet/task/run binding for the legacy Codex adapter. +- HMAC receipts and seals establish integrity under secret-key custody; they are + not public-key attestations or provider-side execution proofs. +- CINT does not establish account billing, provider quota causation, hostile-code + containment, operating-system integrity, or protection after receipt/seal key + compromise. +- `CINT-R0` carries no push, publication, release, repository rename, package + publication, or external announcement authority. + +The AF-G0 accounting case remains a historical Adapter 01 regression. It does +not define CINT and does not establish a provider bill or quota formula. diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md index cb7237b..26ae668 100644 --- a/docs/PRIVACY.md +++ b/docs/PRIVACY.md @@ -1,21 +1,41 @@ -# Privacy model +# CINT privacy model -## Deterministic demonstration +CINT-R0 operates locally. Canonical receipts, consumption records, revalidation +records, ledger entries, outcomes, and seals contain identifiers, digests, +epochs, statuses, reason codes, timestamps, and aggregate adapter evidence. They +do not contain receipt keys, seal keys, absolute adapter paths, original +synthetic bytes, or complete Codex event streams. -`npm run demo` uses sanitized local fixtures. It makes no model call, requires no Codex authentication, and reads no private repository. +## Sensitive local material -## Live worker +The embedding process controls: -A native run uses the locally installed Codex command and its configured provider. The bounded packet and allowlisted source content may enter model requests. Agent Floor does not automatically send the entire repository. +- principal attributes and explicit request text; +- action parameters and target descriptions; +- receipt and seal keys; +- receipt-store and ledger locations; +- Codex task packets, allowlisted source content, model output, event JSONL, + stderr, and evidence directories; +- any external policy or machine-state source used during revalidation. -## Environment handling +These surfaces remain local unless the operator separately authorizes transfer. +Evidence seals prove digest relationships; they do not make the underlying +material safe to disclose. -The Codex process receives a small runtime allowlist rather than the complete parent environment. Worker shells retain default filtering and add exclusions for common secret-bearing names. Operators can explicitly extend the process allowlist; doing so transfers responsibility for those variables to the operator. +## Adapter-specific boundaries -## Local run records +The synthetic adapter retains original bytes only in process memory long enough +to restore a failed or interrupted action. Execution and rollback records expose +hashes and byte counts, not content or absolute paths. -The selected output directory may contain packet text, source hashes, model event JSONL, stderr diagnostics, cited excerpts, worker conclusions, usage fields, command metadata, and timestamps. Agent Floor does not promise automatic redaction of native run records. Review them before sharing or archiving. +The Codex adapter inherits the Agent Floor privacy boundary: only allowlisted +files enter the temporary child projection, but the bounded packet and selected +source content may enter the configured model request. Raw native output remains +sensitive and is written only to the caller-selected local evidence directory. -## Public evidence +## Public-tree controls -The repository contains only synthetic source files, sanitized telemetry-shaped fixtures, and redacted summaries. It contains no authentication file, raw private rollout, local absolute path, private source repository, cookie, bearer token, API key, account email, or user biometric. +The repository publication audit rejects absolute local paths, common credential +formats, email addresses, session identifiers, hidden metadata, raw logs outside +the sanitized historical fixture, and non-public project names. Public material +uses only sanitized stage outcomes and local proof counts. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 8442ebb..8f28600 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1,27 +1,33 @@ -# Release v0.1.0-af-g0 - -## Gate - -AF-G0 — Governed Child Execution: PASS. - -## Deterministic proof - -- 14 automated tests pass. -- Full-history delegation is rejected before execution. -- The fixture reproduces 555,300,000 raw cumulative tokens. -- Verified request-local usage is 1,492,621 tokens. -- The correction removes 553,807,379 tokens of false attribution, a 372.03× difference. -- Valid evidence is admitted. -- A contradictory conclusion using the same valid citations is rejected. - -## Native proof - -The redacted native smoke records a clean GPT-5.6 Terra child, one model call, two monitored cycles, 19,417 incremental tokens, and an admitted result. Raw native logs are excluded. - -## Security posture - -The release adds a process-environment allowlist, worker-shell secret-name exclusions, a temporary allowlisted source projection, read-only child execution, source-drift checking, and a public package scan. - -## Non-claims - -This release audits request-local telemetry. It does not calculate an invoice, reconcile account billing, explain a provider quota decision, or provide hostile-code containment. +# CINT-R0 local release-candidate state + +## Identity + +| Field | Value | +|---|---| +| Product code | `CINT` | +| Public display | `SI1 CINT` | +| Descriptor | Machine Counterintelligence Runtime | +| Invariant | No consequential action without current decision-bound authority | +| Branch | `cint-r0-reassembly` | +| Baseline | `v0.1.0-af-g0` at `d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49` | +| State | Local review candidate | + +## Local candidate contents + +- strict canonical contracts and schemas; +- principal, authority, policy, challenge, and decision engine; +- signed action-bound one-shot receipts; +- atomic consumption, replay rejection, expiry, and revocation; +- pre-action revalidation and fail-closed execution; +- synthetic action, verification, interrupt, rollback, ledger, and seal; +- preserved Agent Floor foundation as Codex Adapter 01; +- gate-by-gate local commits and evidence artifacts. + +## Publication hold + +No remote branch, push, merge, tag, release, package publication, repository +rename, media action, or external announcement is authorized or implied by a +local pass. + +Only an explicit later authority may select, review, and publish any successor. +The historical tag and public release remain immutable. diff --git a/docs/SECURITY-MODEL.md b/docs/SECURITY-MODEL.md index aea7a88..1529c3d 100644 --- a/docs/SECURITY-MODEL.md +++ b/docs/SECURITY-MODEL.md @@ -1,53 +1,67 @@ -# Security model +# CINT security model -## Objective +## Security properties -Agent Floor reduces four risks in delegated Codex execution: +CINT-R0 enforces these properties for its implemented local adapters: -1. inherited context that is irrelevant, sensitive, or incorrectly attributed; -2. authority expansion beyond explicitly granted files; -3. unbounded fan-out, cycles, runtime, or output; -4. admission of unsupported or semantically contradictory findings. +- explicit intent precedes authority evaluation; +- principal, authority, policy, adapter, target, action, context, and machine + state remain digest-bound through decision and receipt; +- decisions cannot execute directly; +- only the core receipt authority can authenticate executable authority; +- every receipt expires, revalidates, and reaches one terminal store state; +- replay, substitution, forgery, stale policy, revoked authority, drift, and + unavailable controls stop before action; +- outcome verification is distinct from execution; +- divergent or interrupted synthetic effects are restored and hash-verified; +- only verified or restored outcomes receive a core evidence seal; +- adapters cannot mint or admit their own authority. -## Trust boundaries +## Enforcement boundaries -| Boundary | Trusted | Untrusted until verified | -|---|---|---| -| Root task | Explicit task specification and local policy code | Ambient conversation history and undeclared files | -| Child input | Canonical packet and allowlisted projection | User configuration, project rules, memories, plugins, apps, and parent transcript | -| Child execution | Operating-system process controls and observed exit state | Model prose and recommendations | -| Usage | Unique request-local usage records after a verified boundary | Raw cumulative counters and replayed events | -| Evidence | Reopened file, exact line/excerpt, source hash, and parent semantic assertions | Citation strings supplied by the worker | +| Boundary | Enforcing component | +|---|---| +| Strict JSON and record integrity | `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` | -## Mechanical protections +## Key custody -- `fork_turns="all"` is rejected before execution. -- A new ephemeral Codex process is created; no resume identifier is accepted. -- User configuration and project rules are ignored for the child. -- The child receives only allowlisted files in a temporary read-only projection. -- The Codex process receives an explicit runtime environment allowlist instead of the complete parent environment. -- Worker shells retain default filtering and add exclusions for common secret-bearing variable names. -- Child multi-agent fan-out is disabled and depth is zero inside the worker. -- Concurrency, cycles, runtime, and output are monitored by the parent process. -- Original sources are re-hashed before admission. -- Final output must satisfy a JSON schema and parent-owned semantic assertions. +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, +storage, rotation, and process isolation. -## Local data handling +## Adapter containment -Agent Floor executes locally and uses the existing local Codex authentication surface. It does not read, copy, rewrite, export, or publish credential files. Raw native logs remain local and are excluded from the public package. +The synthetic adapter is restricted to a caller-selected disposable root and a +single existing relative regular file. It resolves the target inside that root, +checks the expected pre-action digest, writes atomically, verifies the final +digest, and can restore the exact original bytes. -The published telemetry fixtures are synthetic and sanitized. They preserve only the numerical shape needed to test boundary accounting. The published native proof is a redacted summary containing aggregate usage, control state, evidence references, and hashes. +The Codex adapter preserves the Agent Floor clean-context, allowlisted source +projection, environment filtering, child fan-out prohibition, execution limits, +request-local accounting, and evidence verification. It is classified read-only +and has no CINT authority functions. -## Threats not eliminated +## Audit posture -- A malicious or compromised local Codex executable can misreport events or ignore flags. -- The operating system and repository owner remain trusted. -- On platforms where the read-only sandbox permits other host-readable files, use a container, virtual machine, or dedicated account for hostile source content. -- Environment exclusions reduce accidental secret propagation; they are not hostile-code containment or a network firewall. -- Post-response token admission cannot stop provider-side work already completed. -- Exact file-line verification cannot establish broader factual truth by itself; semantic assertions reduce but do not eliminate that risk. -- Read-only AF-G0 does not govern a later write phase unless that phase receives a separate explicit policy and authority grant. +The CINT test corpus contains positive and negative controls for silent requests, +undeclared effects, target scope, missing rollback, record tampering, signature +forgery, expiry, concurrent replay, crash locks, policy drift before and after +consumption, target drift, unavailable services, divergence, interrupt, +rollback restoration, evidence sealing, adapter authority separation, and +legacy packet drift. -## Security verification - -The publication gate runs tests, deterministic demonstration, dependency audit, local secret/path/name scan, hidden-metadata inspection, and fresh-clone verification. See `SECURITY.md` for reporting vulnerabilities. +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. diff --git a/docs/THREAT-MODEL.md b/docs/THREAT-MODEL.md index 30d8fa5..7c191a0 100644 --- a/docs/THREAT-MODEL.md +++ b/docs/THREAT-MODEL.md @@ -1,39 +1,140 @@ -# Threat model +# CINT threat model -## Protected assets +## 1. Overview -- parent task history and unrelated account context; -- repositories and files outside the declared allowlist; -- host credentials and environment variables; -- execution budget and operator-selected reasoning authority; -- integrity of evidence and request-local usage attribution; -- source repository write state. +SI1 CINT is a local decision and execution runtime. It converts an explicit +request into a canonical intent, resolves current principal and authority state, +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. -## Trust boundaries +### Components and source -1. The root operator authors the task specification and admission policy. -2. Agent Floor validates the specification and creates a canonical packet. -3. Allowlisted files are copied into a temporary projection. -4. Codex executes as an ephemeral read-only child with an allowlisted process environment. -5. Worker output remains untrusted until deterministic admission completes. -6. Native event and output files remain sensitive local artifacts. +| Component | Security role | Source evidence | +|---|---|---| +| Canonical records | Reject unknown fields and tampered digests | `src/cint/canonical.js:25-46`, `src/cint/canonical.js:130-147` | +| 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 | Revalidate inside the lock and again before adapter execution | `src/cint/execution.js:82-283` | +| 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` | + +```mermaid +flowchart LR + P[Principal request] --> I[Canonical intent] + I --> C[Authority + policy + counter-intent] + C -->|DENY / REVIEW| X[No receipt] + C -->|ADMIT| D[Non-executable decision] + D --> R[Signed one-shot receipt] + R --> S[Lock + revalidate + consume] + S --> V[Fresh revalidation] + V --> A[Exact adapter action] + A --> O[Verify outcome] + O -->|match| E[Evidence seal] + O -->|diverge / interrupt| B[Rollback + hash check] + B --> E +``` + +### Effective resources + +| 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` | +| 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 | + +## 2. Threat Model, Trust Boundaries, and Assumptions -## Threats and controls +### Protected assets and objectives -| Threat | Control | Residual risk | +- integrity of principal intent and its declared consequences; +- exclusivity, freshness, scope, and provenance of executable authority; +- policy and authority revocation effectiveness; +- integrity of target state before and after action; +- receipt and seal signing keys; +- one-shot receipt state and replay resistance; +- integrity and confidentiality of local adapter inputs and raw evidence; +- exact rollback restoration for the synthetic consequential proof; +- separation between legacy adapter evidence and CINT authority. + +### Actors and starting capabilities + +| Actor | Realistic starting capability | Capability not assumed | |---|---|---| -| Complete parent history inherited by a child | Fresh process; no resume path; `fork_turns="none"` required | An operator can still place excessive context in the bounded packet | -| Recursive agent fan-out | Multi-agent features disabled; child depth zero; concurrency lock | Codex behavior can change; verify the installed command surface | -| Runaway resource use | Cycle, runtime, output, concurrency, and admission token ceilings | Usage may occur before completed telemetry is available | -| Cumulative telemetry assigned as child usage | Request-local events or explicit post-boundary deltas; duplicate fingerprinting | Local telemetry does not establish provider billing | -| Hallucinated citation | Path, line, excerpt, source hash, and allowlist checks | A real line can still be interpreted incorrectly | -| True citation with a false conclusion | Parent status, finding-code, required/forbidden-term, and evidence-count rules | Semantic rules are task-specific | -| Source changes after worker read | Source hashes rechecked before admission | External systems referenced by a file remain outside AF-G0 | -| Secret-bearing environment exposed | Process allowlist; shell secret-name exclusions; no shell profile | Explicit operator allowlisting can reintroduce sensitive values | -| Source-repository mutation | Temporary copy plus read-only sandbox | Agent Floor writes its own records to the selected output directory | -| Prompt injection in inspected source | Bounded instructions, read-only mode, schema, and parent admission | Host-readable files can remain visible on some platforms; isolate hostile content in a container or virtual machine | -| Network exfiltration | Network-capable product features disabled where exposed | This is not a network firewall; platform behavior must be verified independently | - -## Explicit non-claims - -Agent Floor does not provide billing reconciliation, cryptographic proof of provider-side execution, hostile-code containment, protection from a compromised executable or operating system, safe handling of secrets deliberately included in a packet, or write-capable remediation at AF-G0. +| Requesting principal | Can propose explicit, silent, ambiguous, or overbroad action input | Cannot forge current CINT authority or receipt key | +| Malicious source author | Can place adversarial instructions or false claims in an authority-allowed file | Cannot write the original repository through the read-only Codex adapter | +| Local replaying process | Can present a copied receipt and race another consumer | Cannot bypass exclusive store state without local filesystem compromise | +| Local state changer | Can change policy, authority, machine state, or synthetic target between decision and action | Cannot make changed digests equal without breaking SHA-256 assumptions | +| Adapter implementation | Can perform operations exposed by its configured local process authority | Cannot issue decisions, receipts, consumption records, or evidence seals through the adapter API | + +### Trust boundaries + +1. Principal input crosses into canonical intent reconstruction. The boundary + rejects unknown fields, noncanonical data, and missing explicit effects. +2. Intent crosses into principal, authority, policy, adapter, and machine-state + adjudication. Exact digests and epochs must converge. +3. `ADMIT` crosses into the receipt authority. Decision status and eligibility + are rechecked; the decision remains non-executable. +4. A receipt crosses into the store. Signature and lifetime verification, + immediate revalidation, exclusive locking, and digest comparison precede + consumption. +5. A consumed receipt crosses into the action adapter only after a fresh second + revalidation. Adapter identity and capability must still match. +6. Adapter output crosses into outcome verification. A divergent or interrupted + synthetic effect crosses into rollback before any seal is permitted. +7. Verified/restored outcome and hash-chained ledger head cross into the seal + authority. The adapter cannot cross this boundary directly. + +### Assumptions and exclusions + +- The embedding process, operating system, Node.js runtime, cryptographic + implementation, and receipt/seal key custody are trusted for R0. +- SHA-256 collision resistance and HMAC-SHA256 unforgeability hold. +- The receipt store, ledger, adapter root, and output directories are selected + 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. +- The Codex adapter is read-only toward its source projection. Its selected + evidence directory is intentionally writable and remains sensitive local + state. +- Source-level threat scenarios are hypotheses unless a conformance test in the + repository validates the control behavior. No scenario below is presented as + a confirmed vulnerability. + +## 3. Attack Surface, Mitigations, and Attacker Stories + +| 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 lock plus fresh revalidation after consumption | Embed policy and state providers with authoritative current snapshots | `src/cint/revalidation.js:8-127`, `src/cint/execution.js:132-191` | +| 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` | +| 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` | + +## 4. Severity Calibration + +| Level | CINT-specific example | Counterexample or lowering condition | +|---|---|---| +| Critical | Remote or untrusted input can forge current receipt authority and execute arbitrary host actions without the key | Not established by an adapter bug that still requires a trusted operator to supply both key and unrestricted host capability | +| High | A replay, policy-drift, or target-binding bypass permits an unauthorized consequential action within an exposed adapter scope | Lower when the only effect is confined to an already attacker-owned disposable file | +| Medium | Ledger or seal integrity failure causes materially false audit evidence without granting action authority | Lower when tampering is detected before any consumer accepts the bundle | +| Low | Local stale-lock behavior causes bounded denial of service with no action and explicit fail-closed state | Not a vulnerability when the caller deliberately retains a lock pending authorized recovery | + +Severity rises with reachable external input, broader adapter capability, key +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. diff --git a/docs/archive/README.md b/docs/archive/README.md new file mode 100644 index 0000000..6f46598 --- /dev/null +++ b/docs/archive/README.md @@ -0,0 +1,17 @@ +# Historical material + +This directory preserves lineage and regression context outside the active CINT +product surface. + +- `agent-floor-v0.1/` contains the historical Agent Floor narrative, + architecture, security, privacy, release, adoption, and media material. +- `build-week-2026/` contains competition-specific judge and independent + evaluation material. + +The immutable source of release identity is the Git tag `v0.1.0-af-g0`. Archive +copies on the CINT successor branch are contextual references and do not alter +that tag or its evidence manifest. + +Historical fixtures, protocol strings, AF error codes, deterministic demo, and +release evidence remain available for Adapter 01 compatibility tests. They do +not define CINT authority or product readiness. diff --git a/docs/archive/agent-floor-v0.1/ARCHITECTURE.md b/docs/archive/agent-floor-v0.1/ARCHITECTURE.md new file mode 100644 index 0000000..015f77d --- /dev/null +++ b/docs/archive/agent-floor-v0.1/ARCHITECTURE.md @@ -0,0 +1,117 @@ +# Historical Agent Floor architecture + +Agent Floor separates intent, enforcement, measurement, and adjudication. A policy declaration is not reported as a mechanical guarantee; telemetry is not reported as billing; a valid citation is not treated as a valid conclusion. + +```mermaid +flowchart LR + A["Root task specification"] --> B["Policy gate"] + B -->|invalid context or limits| X["REJECTED"] + B --> C["Canonical clean packet and lineage hash"] + C --> D["Allowlisted source projection"] + D --> E["Ephemeral bounded Codex child"] + E --> F["Request-local JSONL usage"] + E --> G["Schema-constrained result"] + F --> H["Incremental auditor"] + G --> I["Evidence and semantic verifier"] + H --> J["Parent admission gate"] + I --> J + J -->|all checks pass| K["ADMITTED"] + J -->|any check fails| X +``` + +## Control classification + +| Class | Examples | Authority | +|---|---|---| +| Policy controls | Allowlisted model, selected reasoning, maximum depth, concurrency, cycles, runtime, output, and incremental-token admission ceiling | Root task specification validated before execution | +| Mechanical controls | New ephemeral process, no resume path, clean packet, ignored inherited rules/configuration, read-only projection, disabled child fan-out, process lock, event monitor, timer | Runner and operating-system process boundary | +| Telemetry interpretation | Unique request usage, cached/fresh split, cumulative baseline, boundary delta, duplicate removal | Auditor over JSONL records | +| Semantic evidence admission | Allowed status and finding codes, required/forbidden terms, minimum evidence, exact file-line-excerpt verification, source hash | Parent gate after execution | + +## Context boundary + +The runner never forks or resumes a parent task. It starts `codex exec` as a new ephemeral process and sends one canonical packet through standard input. User configuration and project rules are ignored for the child. Project documentation is capped at zero bytes and replaced with a generated bounded instruction file. + +The packet records: + +- `context.mode: clean` and `inherited_turns: 0`; +- parent and child lineage; +- repository boundary and allowlisted authority; +- model and reasoning selection; +- depth, concurrency, cycle, runtime, output, and token-admission limits; +- source hashes and evidence requirements; +- final output schema; +- canonical packet SHA-256. + +The packet is limited to 8 KiB. Full conversation history has no packet field and `fork_turns="all"` fails validation before a child process starts. + +## Authority boundary + +Only task-declared regular files whose resolved paths remain inside the repository boundary are accepted. Before execution, those files are copied into a temporary projection. Codex runs from that projection in a read-only sandbox. The original repository is not the child working directory. + +Before admission, Agent Floor re-hashes the original sources and rejects drift. Each evidence item must: + +1. use an allowlisted repository-relative path; +2. resolve inside the original boundary; +3. cite an existing one-based line; +4. supply an excerpt contained in that normalized line; +5. carry a non-empty claim. + +The admitted evidence record hashes `path:line:normalized-line`, binding the decision to observed content. + +## Delegation and resource boundary + +The root-to-child depth is one. Inside the child process, multi-agent tools and fan-out are disabled and child `agents.max_depth` is zero. A repository-scoped cross-process lock enforces the declared concurrency ceiling. The event monitor counts model/tool cycles and terminates the process when the hard cycle, runtime, or output boundary is crossed. + +The token limit is different: completed request usage is measured from the returned telemetry and checked before admission. It is not a provider-side preflight spending cap. + +## Usage equations + +For current `codex exec --json` traces: + +```text +incremental_tokens = sum(turn.completed.usage.input_tokens + + turn.completed.usage.output_tokens) +fresh_input_tokens = input_tokens - cached_input_tokens +``` + +`reasoning_output_tokens` is reported separately but is not added again because it is a subset of output tokens. + +For legacy rollout traces: + +```text +incremental_tokens = sum(unique post-boundary last_token_usage.total_tokens) +cumulative_delta = last cumulative total - pre-boundary cumulative total +verified = incremental_tokens == cumulative_delta +``` + +A rollout without an explicit worker boundary is rejected. Exact duplicate token events are fingerprinted and removed. Cumulative totals remain diagnostic telemetry; they are never presented as request-local cost or account billing. + +## Admission state machine + +```mermaid +stateDiagram-v2 + [*] --> Candidate + Candidate --> Rejected: process or policy violation + Candidate --> Rejected: context is not clean + Candidate --> Rejected: incremental token ceiling exceeded + Candidate --> Rejected: schema, semantic, or evidence failure + Candidate --> Rejected: source drift + Candidate --> Admitted: every deterministic check passes + Admitted --> [*] + Rejected --> [*] +``` + +A worker's `recommendation` concerns the inspected target. It cannot admit or reject its own result. Parent-declared execution, usage, source-integrity, evidence, and semantic rules control admission. + +## Artifact flow + +| Artifact | Created by | Consumed by | +|---|---|---| +| Task specification | Root operator | Policy gate | +| Child packet | Packet builder | Runner and admission gate | +| Source projection | Runner | Child process | +| JSONL event stream | Codex child | Usage auditor | +| Worker output | Codex child | Admission gate | +| Admission record | Parent gate | Root task or later authorized phase | +| Evidence manifest | Release process | Public verifier | diff --git a/docs/CODEX-ADOPTION.md b/docs/archive/agent-floor-v0.1/CODEX-ADOPTION.md similarity index 99% rename from docs/CODEX-ADOPTION.md rename to docs/archive/agent-floor-v0.1/CODEX-ADOPTION.md index 2a778fe..2100aaf 100644 --- a/docs/CODEX-ADOPTION.md +++ b/docs/archive/agent-floor-v0.1/CODEX-ADOPTION.md @@ -1,4 +1,4 @@ -# Codex adoption +# Historical Codex adoption This guide applies Agent Floor's controls to another Codex task without inheriting the development task that produced Agent Floor. diff --git a/docs/archive/agent-floor-v0.1/LIMITATIONS.md b/docs/archive/agent-floor-v0.1/LIMITATIONS.md new file mode 100644 index 0000000..e333eaf --- /dev/null +++ b/docs/archive/agent-floor-v0.1/LIMITATIONS.md @@ -0,0 +1,29 @@ +# Historical Agent Floor limitations and non-claims + +## Telemetry, billing, and quota + +Agent Floor audits local request telemetry. It does not read authoritative account billing data and does not claim that the verified incremental total equals a bill, subscription debit, or quota formula. Cached input is reported separately; the repository does not classify it as free. + +The 555.3M fixture demonstrates an attribution defect: a cumulative counter was assigned to a worker despite a much smaller post-boundary increment. It does not prove why any provider-side usage meter changed. + +## Token ceilings + +The runner evaluates incremental usage after a completed request reports usage. It can reject an over-ceiling result from admission, but cannot guarantee that the provider performed no work beyond the ceiling. Cycle, runtime, and output limits are active process controls; the token ceiling is a post-response admission control. + +## Execution environment + +The runner depends on the installed Codex executable honoring its documented flags and emitting recognizable JSONL. `npm run doctor` verifies the required command-line surface, not the integrity of the executable or service. + +Model identifiers and reasoning tiers are environment-dependent. Policy acceptance means Agent Floor will preserve and record a declared selection; it does not guarantee that every account or Codex build exposes that model or tier. + +## Evidence + +File-line-excerpt and source-hash verification establishes that cited text exists and remained unchanged. It does not establish that the source itself is true, complete, or authoritative. Parent semantic assertions are task-specific and may be incomplete. + +## Authority + +AF-G0 is a read-only discovery and admission gate. It does not authorize source modification, deployment, publication, issue creation, messaging, or credential changes. A later action phase needs a separate explicit grant and should consume only admitted evidence. + +## Platform scope + +The implementation targets Node.js 20+ and the Codex non-interactive command surface verified by `npm run doctor`. Windows path and process behavior have not been validated in AF-G0. diff --git a/docs/archive/agent-floor-v0.1/PRIVACY.md b/docs/archive/agent-floor-v0.1/PRIVACY.md new file mode 100644 index 0000000..c1236db --- /dev/null +++ b/docs/archive/agent-floor-v0.1/PRIVACY.md @@ -0,0 +1,21 @@ +# Historical Agent Floor privacy model + +## Deterministic demonstration + +`npm run demo` uses sanitized local fixtures. It makes no model call, requires no Codex authentication, and reads no private repository. + +## Live worker + +A native run uses the locally installed Codex command and its configured provider. The bounded packet and allowlisted source content may enter model requests. Agent Floor does not automatically send the entire repository. + +## Environment handling + +The Codex process receives a small runtime allowlist rather than the complete parent environment. Worker shells retain default filtering and add exclusions for common secret-bearing names. Operators can explicitly extend the process allowlist; doing so transfers responsibility for those variables to the operator. + +## Local run records + +The selected output directory may contain packet text, source hashes, model event JSONL, stderr diagnostics, cited excerpts, worker conclusions, usage fields, command metadata, and timestamps. Agent Floor does not promise automatic redaction of native run records. Review them before sharing or archiving. + +## Public evidence + +The repository contains only synthetic source files, sanitized telemetry-shaped fixtures, and redacted summaries. It contains no authentication file, raw private rollout, local absolute path, private source repository, cookie, bearer token, API key, account email, or user biometric. diff --git a/docs/archive/agent-floor-v0.1/README.md b/docs/archive/agent-floor-v0.1/README.md new file mode 100644 index 0000000..c26d5d4 --- /dev/null +++ b/docs/archive/agent-floor-v0.1/README.md @@ -0,0 +1,173 @@ +# Agent Floor v0.1 historical record + +A Codex worker appeared to consume **555.3M tokens**. + +Its verified request-local usage was **1,492,621 tokens**. + +The **372× discrepancy** came from inherited cumulative history after `fork_turns="all"`. Agent Floor prevents this class of error before delegation and audits every worker afterward. + +**Release gate:** AF-G0 PASS · **Version:** `v0.1.0-af-g0` · **Runtime:** Node.js 20+ + +![Agent Floor architecture](media/architecture.svg) + +## What Agent Floor is + +Agent Floor is a zero-dependency Node.js control layer for governed Codex delegation. It turns a root task into a self-contained child packet, starts a fresh bounded Codex process, attributes only request-local usage, and admits a result only when the parent can verify its evidence and semantic contract. + +Four layers remain separate: + +| Layer | Responsibility | +|---|---| +| Policy controls | Declare allowed models, reasoning settings, context mode, authority, depth, concurrency, cycles, runtime, output, and token ceilings. | +| Mechanically enforced controls | Reject full-history inheritance, create a clean ephemeral process, disable child fan-out, project only allowlisted files, and terminate limit violations. | +| Telemetry interpretation | Deduplicate usage events and calculate post-boundary input, cached input, fresh input, output, and incremental totals. | +| Semantic evidence admission | Verify file, line, excerpt, source hash, finding code, status, required terms, and forbidden terms before admitting a result. | + +## The failure it prevents + +A cumulative counter can include work that existed before a child request. Treating that counter as the child total creates false attribution. + +| Quantity | Tokens | Interpretation | +|---|---:|---| +| Raw cumulative counter | 555,300,000 | Inherited baseline plus child work | +| Request-local increment | 1,492,621 | Sixteen unique post-boundary request records | +| Overstatement removed | 553,807,379 | Raw counter minus verified child increment | +| Correction | 372.03× | Raw counter divided by verified child increment | + +The sanitized regression preserves the measured request-local split: 1,380,352 cached input tokens, 103,132 fresh input tokens, and 9,137 output tokens. It adds a declared synthetic baseline solely to reproduce the 555.3M failure shape. + +## How governed delegation works + +1. Validate a root-owned task specification. +2. Reject any context mode other than `clean` and any `fork_turns` value other than `none`. +3. Build a canonical packet no larger than 8 KiB with lineage, authority, limits, source hashes, and output schema. +4. Copy only allowlisted regular files into a temporary read-only authority projection. +5. Start a new ephemeral Codex process with inherited rules, configuration, apps, plugins, memories, browser tools, computer tools, image tools, and multi-agent fan-out disabled. Pass only an explicit runtime environment allowlist and exclude common secret-bearing variable names from worker shells. +6. Monitor model/tool cycles, runtime, output bytes, and process status. +7. Audit only request-local JSONL usage or verified post-boundary legacy deltas. +8. Reopen the original sources and admit the result only if every deterministic evidence and semantic check passes. + +The worker may recommend accepting or rejecting the target under review. That recommendation never controls whether the worker's own result is admitted; only the parent gate does. + +## Architecture + +The root-to-child boundary is one-way: the child receives a bounded packet and an allowlisted source projection, then returns schema-constrained output and request-local telemetry. It cannot resume the parent task or spawn another worker. + +See [Architecture](docs/ARCHITECTURE.md), [Security model](docs/SECURITY-MODEL.md), [Threat model](docs/THREAT-MODEL.md), and [Privacy model](docs/PRIVACY.md). + +## Quick start + +```sh +git clone https://github.com/sntllgnc/agent-floor.git +cd agent-floor +npm ci +npm test +npm run demo +``` + +No package dependency is installed; `npm ci` verifies the frozen package metadata. + +## Deterministic demonstration + +`npm run demo` makes no model call. It evaluates the public sanitized fixture and writes reproducible detail records to the ignored `artifacts/generated/` directory. + +Expected proof: + +```text +14 tests passed +fork_turns="all" -> REJECTED +raw cumulative attribution -> 555,300,000 +verified incremental usage -> 1,492,621 +overstatement removed -> 553,807,379 +correction -> 372.03x +evidence result -> ADMITTED +contradictory semantic control -> REJECTED +``` + +The frozen release record is [artifacts/af-g0.json](artifacts/af-g0.json). + +## Native Codex execution + +Validate the installed Codex surface without a model call: + +```sh +npm run doctor +``` + +Run a governed child only when authenticated local Codex execution is intended: + +```sh +node ./bin/agent-floor.js run ./examples/live-smoke-task.json --out ./artifacts/runs/native +``` + +The release's redacted native smoke used a clean GPT-5.6 Terra child at medium reasoning: one model call, two monitored cycles, 19,417 incremental tokens, and an admitted evidence record. No raw native event stream is published. See [artifacts/native-smoke-summary.json](artifacts/native-smoke-summary.json). + +## Evidence admission + +An evidence item is admitted only when: + +- its repository-relative path was granted to the child; +- its one-based line exists; +- the exact excerpt occurs on that normalized line; +- the source still matches the packet-time SHA-256; +- the result satisfies parent-declared status, finding-code, required-term, forbidden-term, and evidence-count rules; +- execution succeeded within the declared bounds; +- measured incremental usage remains within the admission ceiling. + +The fixture also includes a negative control that cites two real lines but reaches the opposite conclusion. Agent Floor rejects it. Real citations are necessary, not sufficient. + +## AF-G0 verification + +AF-G0 passes only when one command proves all of the following: + +- clean context with zero inherited turns; +- mechanical rejection of full-history delegation; +- bounded model, reasoning, depth, concurrency, cycle, runtime, and output policy; +- request-local usage accounting with duplicate removal; +- reproduction of the 555.3M versus 1.49M attribution defect; +- deterministic evidence admission and semantic rejection; +- no dependency on a private repository. + +Run `npm test`, `npm run demo`, `npm run public:audit`, and `npm run evidence:verify`. The release evidence inventory is [artifacts/evidence-manifest.json](artifacts/evidence-manifest.json). + +## Security and privacy + +Agent Floor executes locally. It does not upload raw logs, modify Codex credentials, or publish operator task history. The release contains only synthetic source files, sanitized telemetry-shaped fixtures, and a redacted native summary. + +The child sees a temporary read-only projection instead of the original repository. The Codex process receives a small runtime environment allowlist instead of the complete parent environment; worker shells retain default filtering and add common key, secret, token, password, credential, and authentication name exclusions. The public package is scanned for local absolute paths, common credential formats, email addresses, session UUIDs, hidden metadata, raw logs, and private project names. + +See [SECURITY.md](SECURITY.md) for vulnerability reporting and [Security model](docs/SECURITY-MODEL.md) for trust boundaries. + +## Limitations and non-claims + +Agent Floor verifies request-local telemetry attribution. It does **not** establish an account billing total, explain a provider quota decision, or assert that cached input is free. A token ceiling is enforced at admission after the completed request reports usage; it is not a pre-request provider-side spending cap. + +AF-G0 is read-only. A later mutation phase must consume an admitted record under a separate explicit authority grant. See [Limitations](docs/LIMITATIONS.md). + +## How Codex and GPT-5.6 were used + +Codex implemented and verified the runner, accounting logic, fixtures, tests, evidence gate, documentation, and publication audit. GPT-5.6 Terra executed the redacted native smoke. The public deterministic demo and test suite do not require model access. + +Agent Floor preserves the operator-selected root reasoning setting. Its policy accepts `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra`, and `max`; the selected value is recorded rather than silently reduced. Model identifiers and reasoning tiers remain environment-dependent capabilities, not repository promises. + +The runner is built around Codex non-interactive JSONL execution, ephemeral sessions, structured output, and configuration controls described in the official [non-interactive mode](https://learn.chatgpt.com/docs/non-interactive-mode), [subagent configuration](https://learn.chatgpt.com/docs/agent-configuration/subagents), and [configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference). + +## Codex adoption + +Another Codex task can apply the policy without inheriting this repository's development history. Use the bounded instructions in [Codex adoption](docs/CODEX-ADOPTION.md). + +## Build Week judge instructions + +```sh +git clone https://github.com/sntllgnc/agent-floor.git +cd agent-floor +npm ci +npm test +npm run demo +``` + +The complete judge path, expected fields, and under-three-minute narration are in [Judge guide](docs/JUDGE-GUIDE.md). A separate fresh-task packet is available in [Independent Codex evaluation](CODEX_EVALUATION.md). + +## Contributing and licence + +Contributions are governed by [CONTRIBUTING.md](CONTRIBUTING.md), [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md), and repository instructions in [AGENTS.md](AGENTS.md). Code, documentation, and sanitized fixtures are licensed under [Apache-2.0](LICENSE); [NOTICE](NOTICE) preserves attribution and the separate trademark boundary. Release evidence is summarized in [Release v0.1.0-af-g0](docs/RELEASE.md) and [CHANGELOG.md](CHANGELOG.md). diff --git a/docs/archive/agent-floor-v0.1/RELEASE.md b/docs/archive/agent-floor-v0.1/RELEASE.md new file mode 100644 index 0000000..72eee22 --- /dev/null +++ b/docs/archive/agent-floor-v0.1/RELEASE.md @@ -0,0 +1,27 @@ +# Historical release v0.1.0-af-g0 + +## Gate + +AF-G0 — Governed Child Execution: PASS. + +## Deterministic proof + +- 14 automated tests pass. +- Full-history delegation is rejected before execution. +- The fixture reproduces 555,300,000 raw cumulative tokens. +- Verified request-local usage is 1,492,621 tokens. +- The correction removes 553,807,379 tokens of false attribution, a 372.03× difference. +- Valid evidence is admitted. +- A contradictory conclusion using the same valid citations is rejected. + +## Native proof + +The redacted native smoke records a clean GPT-5.6 Terra child, one model call, two monitored cycles, 19,417 incremental tokens, and an admitted result. Raw native logs are excluded. + +## Security posture + +The release adds a process-environment allowlist, worker-shell secret-name exclusions, a temporary allowlisted source projection, read-only child execution, source-drift checking, and a public package scan. + +## Non-claims + +This release audits request-local telemetry. It does not calculate an invoice, reconcile account billing, explain a provider quota decision, or provide hostile-code containment. diff --git a/docs/archive/agent-floor-v0.1/SECURITY-MODEL.md b/docs/archive/agent-floor-v0.1/SECURITY-MODEL.md new file mode 100644 index 0000000..9a89ad0 --- /dev/null +++ b/docs/archive/agent-floor-v0.1/SECURITY-MODEL.md @@ -0,0 +1,53 @@ +# Historical Agent Floor security model + +## Objective + +Agent Floor reduces four risks in delegated Codex execution: + +1. inherited context that is irrelevant, sensitive, or incorrectly attributed; +2. authority expansion beyond explicitly granted files; +3. unbounded fan-out, cycles, runtime, or output; +4. admission of unsupported or semantically contradictory findings. + +## Trust boundaries + +| Boundary | Trusted | Untrusted until verified | +|---|---|---| +| Root task | Explicit task specification and local policy code | Ambient conversation history and undeclared files | +| Child input | Canonical packet and allowlisted projection | User configuration, project rules, memories, plugins, apps, and parent transcript | +| Child execution | Operating-system process controls and observed exit state | Model prose and recommendations | +| Usage | Unique request-local usage records after a verified boundary | Raw cumulative counters and replayed events | +| Evidence | Reopened file, exact line/excerpt, source hash, and parent semantic assertions | Citation strings supplied by the worker | + +## Mechanical protections + +- `fork_turns="all"` is rejected before execution. +- A new ephemeral Codex process is created; no resume identifier is accepted. +- User configuration and project rules are ignored for the child. +- The child receives only allowlisted files in a temporary read-only projection. +- The Codex process receives an explicit runtime environment allowlist instead of the complete parent environment. +- Worker shells retain default filtering and add exclusions for common secret-bearing variable names. +- Child multi-agent fan-out is disabled and depth is zero inside the worker. +- Concurrency, cycles, runtime, and output are monitored by the parent process. +- Original sources are re-hashed before admission. +- Final output must satisfy a JSON schema and parent-owned semantic assertions. + +## Local data handling + +Agent Floor executes locally and uses the existing local Codex authentication surface. It does not read, copy, rewrite, export, or publish credential files. Raw native logs remain local and are excluded from the public package. + +The published telemetry fixtures are synthetic and sanitized. They preserve only the numerical shape needed to test boundary accounting. The published native proof is a redacted summary containing aggregate usage, control state, evidence references, and hashes. + +## Threats not eliminated + +- A malicious or compromised local Codex executable can misreport events or ignore flags. +- The operating system and repository owner remain trusted. +- On platforms where the read-only sandbox permits other host-readable files, use a container, virtual machine, or dedicated account for hostile source content. +- Environment exclusions reduce accidental secret propagation; they are not hostile-code containment or a network firewall. +- Post-response token admission cannot stop provider-side work already completed. +- Exact file-line verification cannot establish broader factual truth by itself; semantic assertions reduce but do not eliminate that risk. +- Read-only AF-G0 does not govern a later write phase unless that phase receives a separate explicit policy and authority grant. + +## Security verification + +The publication gate runs tests, deterministic demonstration, dependency audit, local secret/path/name scan, hidden-metadata inspection, and fresh-clone verification. See `SECURITY.md` for reporting vulnerabilities. diff --git a/docs/archive/agent-floor-v0.1/SECURITY.md b/docs/archive/agent-floor-v0.1/SECURITY.md new file mode 100644 index 0000000..7b20422 --- /dev/null +++ b/docs/archive/agent-floor-v0.1/SECURITY.md @@ -0,0 +1,25 @@ +# Historical Agent Floor security policy + +## Supported release + +Security fixes are applied to the latest `0.1.x` release line. + +## Report a vulnerability + +Use GitHub's private vulnerability reporting feature for this repository. Include the affected version, control boundary, reproduction steps, impact, and the smallest sanitized evidence needed to verify the report. + +Do not place secrets, credentials, private source material, raw Codex logs, or operator conversation history in a public issue. If private reporting is unavailable, open a minimal public issue requesting a private coordination channel without including vulnerability details. + +## Data and credential boundary + +Agent Floor executes locally. It does not require raw log upload, does not modify Codex credentials, and does not publish authentication material. The runner uses the already authenticated local Codex command surface when a user explicitly invokes a native run. + +The public repository contains sanitized fixtures only. Native proof is reduced to aggregate usage, control state, evidence references, and content hashes. + +## Billing and quota boundary + +Agent Floor reports request-local telemetry. Security reports and project documentation must not convert that telemetry into an unsupported billing total, subscription debit, or provider quota-causation claim. + +## Disclosure handling + +Reports will be acknowledged, reproduced against the smallest safe fixture, classified by affected boundary, and corrected with regression evidence. Coordinated disclosure should occur only after a fix or documented mitigation is available. diff --git a/docs/archive/agent-floor-v0.1/THREAT-MODEL.md b/docs/archive/agent-floor-v0.1/THREAT-MODEL.md new file mode 100644 index 0000000..b455e23 --- /dev/null +++ b/docs/archive/agent-floor-v0.1/THREAT-MODEL.md @@ -0,0 +1,39 @@ +# Historical Agent Floor threat model + +## Protected assets + +- parent task history and unrelated account context; +- repositories and files outside the declared allowlist; +- host credentials and environment variables; +- execution budget and operator-selected reasoning authority; +- integrity of evidence and request-local usage attribution; +- source repository write state. + +## Trust boundaries + +1. The root operator authors the task specification and admission policy. +2. Agent Floor validates the specification and creates a canonical packet. +3. Allowlisted files are copied into a temporary projection. +4. Codex executes as an ephemeral read-only child with an allowlisted process environment. +5. Worker output remains untrusted until deterministic admission completes. +6. Native event and output files remain sensitive local artifacts. + +## Threats and controls + +| Threat | Control | Residual risk | +|---|---|---| +| Complete parent history inherited by a child | Fresh process; no resume path; `fork_turns="none"` required | An operator can still place excessive context in the bounded packet | +| Recursive agent fan-out | Multi-agent features disabled; child depth zero; concurrency lock | Codex behavior can change; verify the installed command surface | +| Runaway resource use | Cycle, runtime, output, concurrency, and admission token ceilings | Usage may occur before completed telemetry is available | +| Cumulative telemetry assigned as child usage | Request-local events or explicit post-boundary deltas; duplicate fingerprinting | Local telemetry does not establish provider billing | +| Hallucinated citation | Path, line, excerpt, source hash, and allowlist checks | A real line can still be interpreted incorrectly | +| True citation with a false conclusion | Parent status, finding-code, required/forbidden-term, and evidence-count rules | Semantic rules are task-specific | +| Source changes after worker read | Source hashes rechecked before admission | External systems referenced by a file remain outside AF-G0 | +| Secret-bearing environment exposed | Process allowlist; shell secret-name exclusions; no shell profile | Explicit operator allowlisting can reintroduce sensitive values | +| Source-repository mutation | Temporary copy plus read-only sandbox | Agent Floor writes its own records to the selected output directory | +| Prompt injection in inspected source | Bounded instructions, read-only mode, schema, and parent admission | Host-readable files can remain visible on some platforms; isolate hostile content in a container or virtual machine | +| Network exfiltration | Network-capable product features disabled where exposed | This is not a network firewall; platform behavior must be verified independently | + +## Explicit non-claims + +Agent Floor does not provide billing reconciliation, cryptographic proof of provider-side execution, hostile-code containment, protection from a compromised executable or operating system, safe handling of secrets deliberately included in a packet, or write-capable remediation at AF-G0. diff --git a/media/architecture.svg b/docs/archive/agent-floor-v0.1/media/architecture.svg similarity index 98% rename from media/architecture.svg rename to docs/archive/agent-floor-v0.1/media/architecture.svg index 5f14573..3b12014 100644 --- a/media/architecture.svg +++ b/docs/archive/agent-floor-v0.1/media/architecture.svg @@ -1,5 +1,5 @@ - Agent Floor governed delegation architecture + Historical Agent Floor governed delegation architecture A root task passes through policy validation into a clean packet and read-only source projection. An ephemeral bounded Codex child returns request-local usage and schema-constrained evidence to a parent admission gate. diff --git a/media/screenshots/af-g0-proof.svg b/docs/archive/agent-floor-v0.1/media/screenshots/af-g0-proof.svg similarity index 96% rename from media/screenshots/af-g0-proof.svg rename to docs/archive/agent-floor-v0.1/media/screenshots/af-g0-proof.svg index 29907ac..904dc7a 100644 --- a/media/screenshots/af-g0-proof.svg +++ b/docs/archive/agent-floor-v0.1/media/screenshots/af-g0-proof.svg @@ -1,5 +1,5 @@ - AF-G0 deterministic proof summary + Historical AF-G0 deterministic proof summary The raw cumulative attribution is 555.3 million tokens, verified request-local usage is 1,492,621 tokens, and the correction removes 553,807,379 tokens of false attribution. AF-G0 — GOVERNED CHILD EXECUTION diff --git a/CODEX_EVALUATION.md b/docs/archive/build-week-2026/CODEX_EVALUATION.md similarity index 97% rename from CODEX_EVALUATION.md rename to docs/archive/build-week-2026/CODEX_EVALUATION.md index 449103d..1ab8fd4 100644 --- a/CODEX_EVALUATION.md +++ b/docs/archive/build-week-2026/CODEX_EVALUATION.md @@ -1,4 +1,4 @@ -# Independent Codex evaluation +# Historical independent Codex evaluation After cloning the public release, give a fresh Codex task this bounded packet: diff --git a/docs/JUDGE-GUIDE.md b/docs/archive/build-week-2026/JUDGE-GUIDE.md similarity index 98% rename from docs/JUDGE-GUIDE.md rename to docs/archive/build-week-2026/JUDGE-GUIDE.md index 65ed639..6ee50bd 100644 --- a/docs/JUDGE-GUIDE.md +++ b/docs/archive/build-week-2026/JUDGE-GUIDE.md @@ -1,4 +1,4 @@ -# Build Week judge guide +# Historical Build Week judge guide ## Fast path diff --git a/docs/cint-r0/07_DOCUMENTATION_REASSEMBLY.md b/docs/cint-r0/07_DOCUMENTATION_REASSEMBLY.md new file mode 100644 index 0000000..2febc9f --- /dev/null +++ b/docs/cint-r0/07_DOCUMENTATION_REASSEMBLY.md @@ -0,0 +1,51 @@ +# CINT-R0 documentation and identity reassembly + +## Active identity + +```text +PRODUCT CODE CINT +PUBLIC DISPLAY SI1 CINT +DESCRIPTOR Machine Counterintelligence Runtime +TAGLINE No consequential action without current authority. +MISSION Prevent silent or unauthorized intent from becoming machine action. +STATE LOCAL_R0_CANDIDATE +PUBLICATION NONE +``` + +The root README, package metadata, CLI identity, architecture, security model, +threat model, privacy model, limitations, release state, contribution guidance, +security policy, notice, changelog, and independent evaluation now describe +CINT rather than Agent Floor. + +## Historical isolation + +The following material is outside the active product surface: + +- Agent Floor v0.1 README and architecture; +- historical security, privacy, limitation, release, and adoption documents; +- Build Week judge and independent-evaluation material; +- historical architecture and proof media. + +They remain under `docs/archive/`. The immutable tag `v0.1.0-af-g0`, sanitized +fixtures, AF protocols, AF error codes, deterministic demo, and release evidence +remain available for Adapter 01 regression. + +## Active operator surfaces + +- `cint identity` reports the frozen R0 identity and publication authority + `NONE`; +- `cint schemas` locates the strict CINT schema set; +- `cint legacy` makes compatibility use explicit; +- `agent-floor` remains the unchanged historical CLI; +- the package stays private and adds no publication or release action. + +## Gate proof + +- active Markdown links resolve; +- all threat-model source citations resolve to existing source lines; +- package and lock metadata agree on `si1-cint@0.1.0-cint-r0`; +- two CINT CLI tests pass; +- all 51 prior tests remain green; +- competition language and the historical accounting case no longer headline + the active product; +- remote and external mutations remain zero. diff --git a/package-lock.json b/package-lock.json index f92ea38..a9cedab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { - "name": "agent-floor", - "version": "0.1.0", + "name": "si1-cint", + "version": "0.1.0-cint-r0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "agent-floor", - "version": "0.1.0", + "name": "si1-cint", + "version": "0.1.0-cint-r0", "license": "Apache-2.0", "bin": { - "agent-floor": "bin/agent-floor.js" + "agent-floor": "bin/agent-floor.js", + "cint": "bin/cint.js" }, "engines": { "node": ">=20" diff --git a/package.json b/package.json index d26d407..2391095 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,22 @@ { - "name": "agent-floor", - "version": "0.1.0", - "description": "Bounded Codex child execution with request-local usage and evidence admission.", + "name": "si1-cint", + "version": "0.1.0-cint-r0", + "description": "Machine Counterintelligence Runtime for decision-bound consequential action.", "type": "module", "private": true, "bin": { + "cint": "./bin/cint.js", "agent-floor": "./bin/agent-floor.js" }, "scripts": { + "cint:identity": "node ./bin/cint.js identity", + "cint:test": "node --test ./tests/cint-*.test.js", "demo": "node ./bin/agent-floor.js demo", "doctor": "node ./bin/agent-floor.js doctor", "test": "node --test ./tests/*.test.js", "public:audit": "node ./scripts/publication-audit.mjs", - "evidence:verify": "node ./scripts/verify-evidence-manifest.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" }, "engines": { "node": ">=20" diff --git a/scripts/publication-audit.mjs b/scripts/publication-audit.mjs index 025b2eb..02da21d 100644 --- a/scripts/publication-audit.mjs +++ b/scripts/publication-audit.mjs @@ -8,7 +8,7 @@ const ignoredDirectories = new Set([".git", "node_modules"]); const ignoredPrefixes = ["artifacts/generated/"]; const allowedDotfiles = new Set([".gitignore"]); const privateNames = ["S" + "I1", "Y" + "I1", "S" + "I6", "F" + "YRE"]; -const authorizedPublicNames = [new RegExp("\\b" + "SI1 CINT" + "\\b", "gi")]; +const authorizedPublicNames = [new RegExp("\\b" + "SI1(?:[ -]CINT)" + "\\b", "gi")]; const findings = []; const files = []; const excludedMetadata = []; @@ -88,7 +88,7 @@ for (const file of files) { } const result = { - gate: "AF-PUBLICATION-AUDIT", + gate: "CINT-PUBLICATION-AUDIT", verdict: findings.length === 0 ? "PASS" : "FAIL", files_scanned: files.length, excluded_metadata_files: excludedMetadata.length, diff --git a/src/cint/cli.js b/src/cint/cli.js new file mode 100644 index 0000000..4cdca0b --- /dev/null +++ b/src/cint/cli.js @@ -0,0 +1,49 @@ +import { main as legacyMain } from "../cli.js"; +import { CintError } from "./canonical.js"; + +export const CINT_IDENTITY = Object.freeze({ + product_code: "CINT", + public_display: "SI1 CINT", + formal_descriptor: "Machine Counterintelligence Runtime", + tagline: "No consequential action without current authority.", + mission: "Prevent silent or unauthorized intent from becoming machine action.", + invariant: "No consequential action without current decision-bound authority.", + release_state: "LOCAL_R0_CANDIDATE", + publication_authority: "NONE", + adapters: Object.freeze([ + "cint.adapter.synthetic-file-patch", + "cint.adapter.codex-delegation" + ]) +}); + +export function renderCintHelp() { + return [ + "SI1 CINT — Machine Counterintelligence Runtime", + "", + "No consequential action without current authority.", + "", + "Commands:", + " identity Print the frozen CINT-R0 identity and authority state", + " schemas List strict CINT protocol schemas", + " legacy Invoke the preserved Agent Floor Adapter 01 CLI" + ].join("\n"); +} + +export async function main(argv = process.argv.slice(2), options = {}) { + const write = options.write ?? ((value) => process.stdout.write(value)); + const [command = "help", ...rest] = argv; + if (command === "help" || command === "--help" || command === "-h") { + write(`${renderCintHelp()}\n`); + return 0; + } + if (command === "identity") { + write(`${JSON.stringify(CINT_IDENTITY, null, 2)}\n`); + return 0; + } + if (command === "schemas") { + write(`${JSON.stringify({ protocol: "cint/schema-index/1", directory: "schemas/cint", strict: true }, null, 2)}\n`); + return 0; + } + if (command === "legacy") return legacyMain(rest); + throw new CintError("CINT_CLI_COMMAND", `Unknown CINT command: ${command}`); +} diff --git a/src/cint/index.js b/src/cint/index.js index 0045cb3..90fd9c4 100644 --- a/src/cint/index.js +++ b/src/cint/index.js @@ -3,6 +3,7 @@ export * from "./adapters/synthetic-file-patch.js"; export * from "./adapters/codex/index.js"; export * from "./canonical.js"; export * from "./challenge.js"; +export * from "./cli.js"; export * from "./decision.js"; export * from "./evidence.js"; export * from "./execution.js"; diff --git a/tests/cint-cli.test.js b/tests/cint-cli.test.js new file mode 100644 index 0000000..e11ebf7 --- /dev/null +++ b/tests/cint-cli.test.js @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { CINT_IDENTITY, main, renderCintHelp } from "../src/cint/cli.js"; + +test("CINT CLI exposes the frozen product identity and no publication authority", async () => { + let output = ""; + assert.equal(await main(["identity"], { write: (value) => (output += value) }), 0); + const identity = JSON.parse(output); + assert.deepEqual(identity, CINT_IDENTITY); + assert.equal(identity.public_display, "SI1 CINT"); + assert.equal(identity.publication_authority, "NONE"); +}); + +test("CINT CLI keeps legacy execution behind an explicit command", () => { + const help = renderCintHelp(); + assert.match(help, /legacy /); + assert.match(help, /No consequential action without current authority/); +}); From 71a2bb1b680f2d00ada63b9892109de4231f8ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Thu, 27 Aug 2026 20:53:51 +0300 Subject: [PATCH 08/16] cint(g8): seal independent terminal verification --- artifacts/cint-r0/gate-ledger.json | 8 ++- artifacts/cint-r0/gates/CINT-R0.G8.json | 32 +++++++++ artifacts/cint-r0/root-final.json | 60 ++++++++++++++++ artifacts/cint-r0/root-final.md | 32 +++++++++ docs/cint-r0/08_TERMINAL_VERDICT.md | 93 +++++++++++++++++++++++++ scripts/publication-audit.mjs | 1 + 6 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 artifacts/cint-r0/gates/CINT-R0.G8.json create mode 100644 artifacts/cint-r0/root-final.json create mode 100644 artifacts/cint-r0/root-final.md create mode 100644 docs/cint-r0/08_TERMINAL_VERDICT.md diff --git a/artifacts/cint-r0/gate-ledger.json b/artifacts/cint-r0/gate-ledger.json index 777bbc0..bb9d1a8 100644 --- a/artifacts/cint-r0/gate-ledger.json +++ b/artifacts/cint-r0/gate-ledger.json @@ -52,7 +52,13 @@ "name": "Documentation and technology reassembly", "status": "PASS", "evidence": "docs/cint-r0/07_DOCUMENTATION_REASSEMBLY.md" + }, + { + "gate": "CINT-R0.G8", + "name": "Independent clean verification", + "status": "PASS", + "evidence": "docs/cint-r0/08_TERMINAL_VERDICT.md" } ], - "terminal_verdict": null + "terminal_verdict": "READY-FOR-CINT-REVIEW" } diff --git a/artifacts/cint-r0/gates/CINT-R0.G8.json b/artifacts/cint-r0/gates/CINT-R0.G8.json new file mode 100644 index 0000000..e537112 --- /dev/null +++ b/artifacts/cint-r0/gates/CINT-R0.G8.json @@ -0,0 +1,32 @@ +{ + "gate": "CINT-R0.G8", + "status": "PASS", + "verification_source": "independent-detached-clean-worktree", + "tests": { + "total": 53, + "passed": 53, + "failed": 0 + }, + "schemas": 11, + "threat_model_source_citations": 38, + "active_documents": 10, + "readme_local_links": 13, + "npm_audit_vulnerabilities": 0, + "publication_audit": { + "verdict": "PASS", + "findings": 0 + }, + "historical_evidence": { + "verdict": "PASS", + "entries": 53, + "verification_source": "immutable-git-release" + }, + "package_dry_run": { + "name": "si1-cint", + "verdict": "PASS" + }, + "worktree": "CLEAN", + "remote_mutations": 0, + "public_actions": 0, + "evidence": "docs/cint-r0/08_TERMINAL_VERDICT.md" +} diff --git a/artifacts/cint-r0/root-final.json b/artifacts/cint-r0/root-final.json new file mode 100644 index 0000000..f3d968a --- /dev/null +++ b/artifacts/cint-r0/root-final.json @@ -0,0 +1,60 @@ +{ + "protocol": "cint/root-final/1", + "verdict": "READY-FOR-CINT-REVIEW", + "branch": "cint-r0-reassembly", + "branch_scope": "LOCAL_ONLY", + "baseline": { + "commit": "d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49", + "tag": "v0.1.0-af-g0", + "status": "FROZEN_HISTORICAL_FOUNDATION" + }, + "successor": { + "product": "SI1 CINT", + "descriptor": "Machine Counterintelligence Runtime", + "release_state": "LOCAL_R0_CANDIDATE", + "legacy_adapter": "CINT Adapter 01" + }, + "gate_ledger": { + "passed": 9, + "failed": 0, + "range": "CINT-R0.G0..CINT-R0.G8", + "modifying_commits": 8 + }, + "verification": { + "source": "independent-detached-clean-worktree", + "tests": { + "total": 53, + "passed": 53, + "failed": 0 + }, + "schemas": 11, + "threat_model_source_citations": 38, + "active_documents": 10, + "readme_local_links": 13, + "npm_audit_vulnerabilities": 0, + "publication_audit_findings": 0, + "historical_evidence_entries": 53, + "package_dry_run": "PASS", + "worktree": "CLEAN" + }, + "changed_file_register": { + "comparison": "d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49..HEAD", + "paths": 102, + "added": 76, + "modified": 21, + "renamed": 5, + "query": "git diff --name-status d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49..HEAD" + }, + "authority": { + "publication": "NONE", + "push": "NONE", + "release": "NONE", + "repository_rename": "NONE", + "npm_publication": "NONE", + "external_announcement": "NONE", + "public_actions": 0, + "remote_mutations": 0 + }, + "next_authority": "/D. REVIEW AFTER READY-FOR-CINT-REVIEW", + "resume_marker": "CINT-R0-FOUNDATION-REASSEMBLY / LOCAL-BRANCH / NO-PUSH / AWAITING-/D.-REVIEW" +} diff --git a/artifacts/cint-r0/root-final.md b/artifacts/cint-r0/root-final.md new file mode 100644 index 0000000..65f3d03 --- /dev/null +++ b/artifacts/cint-r0/root-final.md @@ -0,0 +1,32 @@ +# CINT-R0 root terminal + +```text +READY-FOR-CINT-REVIEW +``` + +The local successor branch passed CINT-R0.G0 through CINT-R0.G8. Eight +modifying gate commits follow the frozen historical baseline. Independent +verification ran from a detached clean worktree. + +```text +TESTS 53 / 53 PASS +CINT SCHEMAS 11 +THREAT-MODEL CITATIONS 38 +ACTIVE DOCUMENTS 10 +README LOCAL LINKS 13 +DEPENDENCY VULNERABILITIES 0 +PUBLICATION FINDINGS 0 +HISTORICAL EVIDENCE 53 / 53 PASS +PACKAGE DRY RUN PASS +WORKTREE CLEAN +``` + +No push, release, repository rename, npm publication, external announcement, +or other public action occurred. + +```text +CINT-R0-FOUNDATION-REASSEMBLY / +LOCAL-BRANCH / +NO-PUSH / +AWAITING-/D.-REVIEW +``` diff --git a/docs/cint-r0/08_TERMINAL_VERDICT.md b/docs/cint-r0/08_TERMINAL_VERDICT.md new file mode 100644 index 0000000..dde7ef8 --- /dev/null +++ b/docs/cint-r0/08_TERMINAL_VERDICT.md @@ -0,0 +1,93 @@ +# CINT-R0 terminal verdict + +```text +READY-FOR-CINT-REVIEW +``` + +## Gate ledger + +| Gate | Result | Sealed outcome | +|---|---|---| +| CINT-R0.G0 | PASS | Historical baseline frozen | +| CINT-R0.G1 | PASS | Preservation map and archive plan reconciled | +| CINT-R0.G2 | PASS | Legacy modules isolated with behavioral equivalence | +| CINT-R0.G3 | PASS | Canonical contracts and decision engine established | +| CINT-R0.G4 | PASS | Decision receipt and atomic one-shot consumption established | +| CINT-R0.G5 | PASS | Synthetic action, verification, and rollback proved | +| CINT-R0.G6 | PASS | Legacy Codex delegation admitted only as Adapter 01 | +| CINT-R0.G7 | PASS | Product identity and documentation reassembled | +| CINT-R0.G8 | PASS | Independent detached clean-worktree verification passed | + +Eight modifying gate commits follow the historical baseline. G0 is the sealed +admission condition and introduces no successor mutation. + +## Independent verification evidence + +```text +TEST RUNNER 53 total / 53 pass / 0 fail +CINT JSON SCHEMAS 11 parsed +JSON EVIDENCE OBJECTS parsed without error +THREAT-MODEL CITATIONS 38 source-bound references +ACTIVE DOCUMENTS 10 present +README LOCAL LINKS 13 resolved +NPM AUDIT 0 vulnerabilities +CINT PUBLICATION AUDIT PASS / 0 findings +HISTORICAL EVIDENCE PASS / 53 entries +CINT IDENTITY PASS / publication authority NONE +PACKAGE DRY RUN PASS +DETACHED WORKTREE CLEAN +``` + +The verification subject was checked outside the branch checkout. Syntax, +schemas, evidence JSON, source citations, links, dependencies, all tests, the +historical demo, publication hygiene, historical evidence identity, CINT CLI +identity, package contents, and final cleanliness were independently evaluated. + +## Changed-file register + +The exact register is the output of: + +```text +git diff --name-status d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49..HEAD +``` + +It contains 102 paths: 76 additions, 21 modifications, and five history-aware +renames. The changes are bounded to the CINT kernel and schemas, Adapter 01 +isolation, compatibility shims, conformance tests, active product surfaces, +historical archives, local evidence, and verification tooling. + +## Preserved boundary + +- `v0.1.0-af-g0` resolves to + `d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49`. +- local `main` and the locally observed `origin/main` tracking reference remain + at that baseline. +- the successor exists only on local branch `cint-r0-reassembly`. +- legacy `ADMITTED` remains evidence and cannot issue CINT authority. +- consequential execution requires current decision-bound authority, exclusive + one-shot receipt consumption, immediate revalidation, verified outcome, and + seal or rollback. + +## Public non-actions + +```text +REMOTE PUSH NONE +PUBLIC BRANCH CREATION NONE +REPOSITORY RENAME NONE +RELEASE CREATION NONE +NPM PUBLICATION NONE +EXTERNAL ANNOUNCEMENT NONE +PUBLIC ACTIONS 0 +REMOTE MUTATIONS 0 +``` + +These are execution-history facts, not authorization for a later action. + +## Resume marker + +```text +CINT-R0-FOUNDATION-REASSEMBLY / +LOCAL-BRANCH / +NO-PUSH / +AWAITING-/D.-REVIEW +``` diff --git a/scripts/publication-audit.mjs b/scripts/publication-audit.mjs index 02da21d..817fb33 100644 --- a/scripts/publication-audit.mjs +++ b/scripts/publication-audit.mjs @@ -18,6 +18,7 @@ async function walk(directory) { for (const entry of entries) { const absolute = path.join(directory, entry.name); const relative = path.relative(root, absolute).split(path.sep).join("/"); + if (entry.name === ".git") continue; if (ignoredPrefixes.some((prefix) => relative.startsWith(prefix))) continue; if (entry.name === ".DS_Store" || entry.name.startsWith("._")) { excludedMetadata.push(relative); From 3b03610dacd70e30529e3c50db6ebe9126cfdf3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Thu, 27 Aug 2026 23:03:26 +0300 Subject: [PATCH 09/16] cint(r0): close independent authority blockers --- README.md | 15 +- docs/ARCHITECTURE.md | 42 +++-- docs/CODEX-ADAPTER.md | 10 +- docs/LIMITATIONS.md | 4 + docs/RELEASE.md | 5 +- docs/SECURITY-MODEL.md | 9 +- docs/THREAT-MODEL.md | 34 +++- package-lock.json | 74 ++++++++ package.json | 12 +- schemas/cint/execution-result.schema.json | 4 +- schemas/cint/receipt.schema.json | 2 +- src/cint/adapters/codex/index.js | 4 +- src/cint/adapters/synthetic-file-patch.js | 4 +- src/cint/authority.js | 10 +- src/cint/canonical.js | 32 +++- src/cint/challenge.js | 33 ++-- src/cint/cli.js | 6 +- src/cint/execution.js | 119 ++++++++++--- src/cint/index.js | 3 +- src/cint/outcome.js | 11 +- src/cint/policy.js | 5 +- src/cint/receipt.js | 6 +- src/cint/revalidation.js | 23 +-- src/cint/schema.js | 62 +++++++ src/cint/seal.js | 17 +- src/cint/store.js | 5 +- tests/cint-codex-adapter.test.js | 6 +- tests/cint-decision.test.js | 16 ++ tests/cint-execution.test.js | 182 +++++++++++++++++++- tests/cint-import-boundary.test.js | 33 ++++ tests/cint-receipt.test.js | 1 + tests/fixtures/deny-cint-adapter-loader.mjs | 13 ++ 32 files changed, 681 insertions(+), 121 deletions(-) create mode 100644 src/cint/schema.js create mode 100644 tests/cint-import-boundary.test.js create mode 100644 tests/fixtures/deny-cint-adapter-loader.mjs diff --git a/README.md b/README.md index 5abca2f..93fb360 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,9 @@ request -> policy + machine-state challenge -> ADMIT / DENY / REVIEW -> signed one-shot receipt - -> immediate revalidation - -> atomic receipt consumption + -> locked revalidation + atomic receipt consumption + -> fresh snapshot + side-effect-free adapter preparation + -> execution-bound revalidation -> action adapter -> outcome verification -> evidence seal or rollback @@ -33,7 +34,8 @@ absent. The R0 proof establishes: -- strict canonical objects and JSON schemas; +- strict canonical objects and runtime enforcement of all 11 public JSON + schemas; - explicit principal, authority, policy, machine-state, and counter-intent contracts; - `ADMIT / DENY / REVIEW` decisions with zero executable authority; @@ -41,7 +43,8 @@ The R0 proof establishes: context, authority epoch, policy epoch, adapter, and machine state; - file-backed atomic one-shot consumption, replay rejection, expiry, and fail-closed crash locks; -- event-bound revalidation before and after receipt consumption; +- event-bound revalidation inside consumption, after consumption, and again + after preparation immediately before execution; - synthetic consequential action, outcome verification, interrupt, rollback, hash restoration, execution ledger, and evidence seal; - Agent Floor imported as **CINT Adapter 01 — Delegated Coding-Agent @@ -69,8 +72,8 @@ regression. It is not the CINT product definition. ## Repository map ```text -src/cint/ CINT protocols and execution boundary -src/cint/adapters/ R0 action adapters +src/cint/ adapter-independent CINT core and execution boundary +src/cint/adapters/ explicit, separately imported R0 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 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 09989d9..eeea9b0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -12,14 +12,14 @@ executable only after immediate revalidation and atomic one-shot consumption. | Plane | Responsibility | Cannot do | |---|---|---| -| Canonical | Strict objects, exact fields, canonical JSON, digests, immutable records | Decide or execute | +| Canonical | Strict objects, runtime JSON Schema validation, canonical JSON, digests, immutable records | Decide or execute | | Intent | Reconstruct request, action, target, context, effects, uncertainties | Grant authority | | Principal and authority | Resolve actor, delegation chain, exact grants, epochs, validity, revocation | Execute an action | | Policy and challenge | Compare current policy, adapter capability, machine state, and counter-intent | Mint a receipt | | Decision | Emit `ADMIT`, `DENY`, or `REVIEW`; bind every current digest | Cross the execution boundary | | Receipt authority | Authenticate an eligible `ADMIT` binding and lifetime | Reconstruct or widen the decision | | Receipt store | Register, lock, revalidate, consume once, reject replay | Decide policy | -| Execution gate | Require services, revalidate, consume, revalidate again, invoke exact adapter | Mint or reinterpret authority | +| Execution gate | Preflight every required service, revalidate, consume, prepare, revalidate immediately before invoking the exact adapter | Mint or reinterpret authority | | Outcome and rollback | Verify final state or restore the pre-action state | Seal itself | | Ledger and seal | Hash-chain events and authenticate terminal evidence | Authorize another action | @@ -39,11 +39,18 @@ executable only after immediate revalidation and atomic one-shot consumption. 6. The receipt authority signs only a receipt-eligible `ADMIT` decision. 7. The store obtains an exclusive receipt lock, invokes immediate revalidation, and writes exactly one terminal consumed or rejected record. -8. The execution gate obtains a fresh snapshot and revalidates again. Drift - after consumption still blocks action. -9. The selected adapter verifies its current target state and performs only the - receipt-bound action. -10. Outcome verification either confirms the intended final digest or invokes +8. The execution gate obtains a fresh snapshot, revalidates again, and confirms + the runtime adapter's exact decision-bound capability before preparation. +9. The selected adapter performs only side-effect-free preparation. That + property is explicit in the signed adapter capability and is required by the + decision and execution preflight. +10. The gate obtains a third fresh snapshot after preparation and revalidates + authority, policy, action, target, context, adapter capability, machine + state, expiry, and revocation immediately before invoking execution. No + asynchronous operation occurs between that check and invocation. +11. The adapter checks current target state and performs only the receipt-bound + action. +12. Outcome verification either confirms the intended final digest or invokes rollback. Only verified or demonstrably restored outcomes can be sealed. ## Atomicity and failure posture @@ -53,16 +60,25 @@ terminal records. A concurrent caller, existing terminal record, or ambiguous lock is rejected. A crash does not trigger guessed lock recovery. The receipt remains unusable until an explicit higher-authority recovery protocol exists. -Execution fails closed when the receipt authority, store, snapshot provider, -adapter, ledger, or seal authority is unavailable. If failure occurs after a -synthetic action begins, the gate invokes rollback and verifies the restored -hash before sealing the restored outcome. +Execution fails closed before receipt consumption when the receipt verifier, +store consumer, snapshot provider, adapter preparer/executor/verifier, required +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. ## Adapter boundary Adapters implement preparation, execution, outcome verification, and optional -rollback. They receive no receipt or seal key. They cannot resolve principals, -change authority, issue decisions, consume receipts, or seal outcomes. +rollback. Preparation is contractually side-effect-free and its declaration is +decision-bound; the final core revalidation detects bound-state drift introduced +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 Codex delegation adapter binds the preserved Agent Floor packet and task digests, then executes the existing clean, bounded, read-only runner. Its diff --git a/docs/CODEX-ADAPTER.md b/docs/CODEX-ADAPTER.md index 0396e54..a69e977 100644 --- a/docs/CODEX-ADAPTER.md +++ b/docs/CODEX-ADAPTER.md @@ -26,13 +26,15 @@ digest. The CINT core independently requires: machine-state decision; 3. signed one-shot receipt; 4. immediate revalidation and consumption; -5. a second event-bound revalidation; -6. legacy execution and adapter outcome verification; -7. CINT ledger and evidence seal. +5. a second event-bound revalidation before side-effect-free preparation; +6. a third execution-bound revalidation after preparation; +7. legacy execution and adapter outcome verification; +8. CINT ledger and evidence seal. The legacy result may be `ADMITTED` or `REJECTED`; both are evidence about the delegated review. Neither value grants CINT authority. The wrapper exports no decision, receipt, consumption, self-admission, or seal method. The original CLI remains available only through `agent-floor` or the explicit -`cint legacy` compatibility command. +`cint legacy` compatibility command. Importing the CINT core or running +`cint identity` does not load the adapter or legacy CLI. diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index c685f2f..66a31e1 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -11,6 +11,10 @@ - Revalidation is event-bound, not continuous at every CPU instruction. Future 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 + 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 not authorize mutation of another repository or live machine state. - The Codex adapter is read-only toward its source projection. Its output diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 8f28600..dbf7872 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -14,11 +14,12 @@ ## Local candidate contents -- strict canonical contracts and schemas; +- strict canonical contracts and runtime enforcement of all 11 public schemas; - principal, authority, policy, challenge, and decision engine; - signed action-bound one-shot receipts; - atomic consumption, replay rejection, expiry, and revocation; -- pre-action revalidation and fail-closed execution; +- receipt-locked, pre-preparation, and execution-bound revalidation with + fail-closed dependency preflight; - synthetic action, verification, interrupt, rollback, ledger, and seal; - preserved Agent Floor foundation as Codex Adapter 01; - gate-by-gate local commits and evidence artifacts. diff --git a/docs/SECURITY-MODEL.md b/docs/SECURITY-MODEL.md index 1529c3d..30bd07f 100644 --- a/docs/SECURITY-MODEL.md +++ b/docs/SECURITY-MODEL.md @@ -12,6 +12,9 @@ CINT-R0 enforces these properties for its implemented local adapters: - every receipt expires, revalidates, and reaches one terminal store state; - replay, substitution, forgery, stale policy, revoked authority, drift, and unavailable controls stop before action; +- all 11 public authority-bearing schemas execute at record construction and + verification boundaries; +- missing adapter or seal verification fails before receipt consumption; - outcome verification is distinct from execution; - divergent or interrupted synthetic effects are restored and hash-verified; - only verified or restored outcomes receive a core evidence seal; @@ -21,7 +24,7 @@ CINT-R0 enforces these properties for its implemented local adapters: | Boundary | Enforcing component | |---|---| -| Strict JSON and record integrity | `src/cint/canonical.js` | +| 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` | @@ -59,7 +62,9 @@ undeclared effects, target scope, missing rollback, record tampering, signature forgery, expiry, concurrent replay, crash locks, policy drift before and after consumption, target drift, unavailable services, divergence, interrupt, rollback restoration, evidence sealing, adapter authority separation, and -legacy packet drift. +legacy packet drift. It also executes the independent-review regressions for a +missing adapter verifier, preparation-time policy and authority drift, invalid +rehashed authority, all 11 schemas, and denied eager Adapter 01 loading. The publication audit continues to reject local absolute paths, raw logs, credential formats, email addresses, session identifiers, hidden metadata, and diff --git a/docs/THREAT-MODEL.md b/docs/THREAT-MODEL.md index 7c191a0..ea5c6cb 100644 --- a/docs/THREAT-MODEL.md +++ b/docs/THREAT-MODEL.md @@ -12,13 +12,13 @@ valid consumed receipt can reach an action adapter. | Component | Security role | Source evidence | |---|---|---| -| Canonical records | Reject unknown fields and tampered digests | `src/cint/canonical.js:25-46`, `src/cint/canonical.js:130-147` | +| Canonical records | Execute all 11 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 | Revalidate inside the lock and again before adapter execution | `src/cint/execution.js:82-283` | +| 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` | @@ -32,7 +32,9 @@ flowchart LR D --> R[Signed one-shot receipt] R --> S[Lock + revalidate + consume] S --> V[Fresh revalidation] - V --> A[Exact adapter action] + V --> P[Side-effect-free preparation] + P --> F[Execution-bound revalidation] + F --> A[Exact adapter action] A --> O[Verify outcome] O -->|match| E[Evidence seal] O -->|diverge / interrupt| B[Rollback + hash check] @@ -86,11 +88,14 @@ flowchart LR 4. A receipt crosses into the store. Signature and lifetime verification, immediate revalidation, exclusive locking, and digest comparison precede consumption. -5. A consumed receipt crosses into the action adapter only after a fresh second - revalidation. Adapter identity and capability must still match. -6. Adapter output crosses into outcome verification. A divergent or interrupted +5. A consumed receipt reaches side-effect-free adapter preparation only after a + fresh second revalidation and exact runtime-capability match. +6. Prepared state reaches adapter execution only after a third fresh + revalidation. Adapter identity and the full capability digest must still + match, and no asynchronous operation intervenes before invocation. +7. Adapter output crosses into outcome verification. A divergent or interrupted synthetic effect crosses into rollback before any seal is permitted. -7. Verified/restored outcome and hash-chained ledger head cross into the seal +8. Verified/restored outcome and hash-chained ledger head cross into the seal authority. The adapter cannot cross this boundary directly. ### Assumptions and exclusions @@ -102,6 +107,9 @@ 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 + side-effect-free preparation contract. The core detects bound-state drift + after preparation but does not sandbox arbitrary adapter code. - The Codex adapter is read-only toward its source projection. Its selected evidence directory is intentionally writable and remains sensitive local state. @@ -115,7 +123,7 @@ flowchart LR |---|---|---|---|---|---|---| | 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 lock plus fresh revalidation after consumption | Embed policy and state providers with authoritative current snapshots | `src/cint/revalidation.js:8-127`, `src/cint/execution.js:132-191` | +| 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 | @@ -123,6 +131,16 @@ flowchart LR | 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` | +### Control-status discipline + +| Classification | R0 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 | +| Evidenced | The runtime records a fact but the record is not itself execution authority | Decision status, receipt consumption, ledger events, adapter result, evidence seal | +| Deferred | A stronger control requires a later architecture or deployment decision | Cross-host consensus, hardware key custody, adapter-specific external transactions, machine-resident enforcement | +| Unimplemented | R0 intentionally supplies no mechanism | Automatic stale-lock recovery, public-key attestation, hostile in-process adapter containment | + ## 4. Severity Calibration | Level | CINT-specific example | Counterexample or lowering condition | diff --git a/package-lock.json b/package-lock.json index a9cedab..210c4ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "si1-cint", "version": "0.1.0-cint-r0", "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1" + }, "bin": { "agent-floor": "bin/agent-floor.js", "cint": "bin/cint.js" @@ -15,6 +19,76 @@ "engines": { "node": ">=20" } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } } } } diff --git a/package.json b/package.json index 2391095..9991764 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,12 @@ "cint": "./bin/cint.js", "agent-floor": "./bin/agent-floor.js" }, + "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" + }, "scripts": { "cint:identity": "node ./bin/cint.js identity", "cint:test": "node --test ./tests/cint-*.test.js", @@ -21,5 +27,9 @@ "engines": { "node": ">=20" }, - "license": "Apache-2.0" + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1" + } } diff --git a/schemas/cint/execution-result.schema.json b/schemas/cint/execution-result.schema.json index 9bc190b..1f57027 100644 --- a/schemas/cint/execution-result.schema.json +++ b/schemas/cint/execution-result.schema.json @@ -26,8 +26,8 @@ "consumption_digest": { "$ref": "#/$defs/nullableSha256" }, "revalidation_digest": { "$ref": "#/$defs/nullableSha256" }, "action_started": { "type": "boolean" }, - "outcome": { "oneOf": [{ "$ref": "outcome.schema.json" }, { "type": "null" }] }, - "evidence_seal": { "oneOf": [{ "$ref": "seal.schema.json" }, { "type": "null" }] }, + "outcome": { "oneOf": [{ "$ref": "https://cint.invalid/schema/outcome-1.json" }, { "type": "null" }] }, + "evidence_seal": { "oneOf": [{ "$ref": "https://cint.invalid/schema/seal-1.json" }, { "type": "null" }] }, "error_code": { "type": ["string", "null"] }, "completed_at": { "type": "string", "format": "date-time" }, "digest": { "$ref": "#/$defs/sha256" } diff --git a/schemas/cint/receipt.schema.json b/schemas/cint/receipt.schema.json index 09533a0..d2a3123 100644 --- a/schemas/cint/receipt.schema.json +++ b/schemas/cint/receipt.schema.json @@ -30,7 +30,7 @@ "issued_at": { "type": "string", "format": "date-time" }, "expires_at": { "type": "string", "format": "date-time" }, "nonce": { "type": "string", "minLength": 16, "maxLength": 128 }, - "binding": { "$ref": "decision.schema.json#/properties/binding" }, + "binding": { "$ref": "https://cint.invalid/schema/decision-1.json#/properties/binding" }, "binding_digest": { "$ref": "#/$defs/sha256" }, "signature_algorithm": { "const": "HMAC-SHA256" }, "signature": { "$ref": "#/$defs/sha256" }, diff --git a/src/cint/adapters/codex/index.js b/src/cint/adapters/codex/index.js index eb0c35c..d41fdd4 100644 --- a/src/cint/adapters/codex/index.js +++ b/src/cint/adapters/codex/index.js @@ -14,6 +14,7 @@ import { immutableRecord, isoInstant, sealRecord, + verifyProtocolRecord, verifySealedRecord } from "../../canonical.js"; import { createAdapterCapability } from "../../challenge.js"; @@ -57,6 +58,7 @@ export class CodexDelegationCintAdapter { id: this.id, action_types: ["CODEX_DELEGATED_REVIEW"], consequence_classes: ["READ_ONLY"], + prepare_side_effect_free: true, rollback: false, interrupt: false, outcome_verification: true @@ -65,7 +67,7 @@ export class CodexDelegationCintAdapter { } async prepare(intent, options) { - verifySealedRecord(intent, "intent"); + 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"); diff --git a/src/cint/adapters/synthetic-file-patch.js b/src/cint/adapters/synthetic-file-patch.js index 02cca90..fb418c9 100644 --- a/src/cint/adapters/synthetic-file-patch.js +++ b/src/cint/adapters/synthetic-file-patch.js @@ -11,6 +11,7 @@ import { isoInstant, sealRecord, sha256Digest, + verifyProtocolRecord, verifySealedRecord } from "../canonical.js"; import { createAdapterCapability } from "../challenge.js"; @@ -39,6 +40,7 @@ export class SyntheticFilePatchAdapter { id: this.id, action_types: ["SYNTHETIC_FILE_PATCH"], consequence_classes: ["CONSEQUENTIAL"], + prepare_side_effect_free: true, rollback: true, interrupt: true, outcome_verification: true @@ -46,7 +48,7 @@ export class SyntheticFilePatchAdapter { } async prepare(intent, options) { - verifySealedRecord(intent, "intent"); + 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"); diff --git a/src/cint/authority.js b/src/cint/authority.js index b54b528..8668058 100644 --- a/src/cint/authority.js +++ b/src/cint/authority.js @@ -9,7 +9,7 @@ import { isoInstant, sealRecord, stringArray, - verifySealedRecord + verifyProtocolRecord } from "./canonical.js"; function grantRecord(value, index) { @@ -71,7 +71,7 @@ export function createAuthorityGrant(input) { } export function revokeAuthority(authority, input) { - verifySealedRecord(authority, "authority"); + 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"); @@ -87,9 +87,9 @@ export function revokeAuthority(authority, input) { } export function evaluateAuthority({ authority, intent, policy, now }) { - verifySealedRecord(authority, "authority"); - verifySealedRecord(intent, "intent"); - if (policy) verifySealedRecord(policy, "policy"); + 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"); diff --git a/src/cint/canonical.js b/src/cint/canonical.js index fb17d7a..e986f05 100644 --- a/src/cint/canonical.js +++ b/src/cint/canonical.js @@ -1,4 +1,5 @@ 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}$/; @@ -130,7 +131,15 @@ export function immutableRecord(value) { export function sealRecord(value) { assertCint(!Object.hasOwn(value, "digest"), "CINT_DIGEST_PRESET", "Record digest must be computed by CINT"); const canonical = canonicalize(value); - return immutableRecord({ ...canonical, digest: canonicalDigest(canonical) }); + 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") { @@ -142,5 +151,26 @@ export function verifySealedRecord(value, label = "record") { "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/challenge.js b/src/cint/challenge.js index d016b45..a574aff 100644 --- a/src/cint/challenge.js +++ b/src/cint/challenge.js @@ -5,6 +5,7 @@ import { isoInstant, sealRecord, stringArray, + verifyProtocolRecord, verifySealedRecord } from "./canonical.js"; import { evaluateAuthority } from "./authority.js"; @@ -19,11 +20,19 @@ function challengeReason(code, disposition, message) { export function createAdapterCapability(input) { assertExactKeys( input, - ["id", "action_types", "consequence_classes", "rollback", "interrupt", "outcome_verification"], + [ + "id", + "action_types", + "consequence_classes", + "prepare_side_effect_free", + "rollback", + "interrupt", + "outcome_verification" + ], [], "adapter capability" ); - for (const field of ["rollback", "interrupt", "outcome_verification"]) { + 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", { @@ -45,6 +54,7 @@ export function createAdapterCapability(input) { 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 @@ -60,16 +70,12 @@ export function runCounterIntentChallenge({ machine_state, now }) { - for (const [label, record] of Object.entries({ - intent, - principal, - authority, - policy, - adapter_capability, - machine_state - })) { - verifySealedRecord(record, label); - } + verifyProtocolRecord(intent, "cint/intent/1", "intent"); + verifyProtocolRecord(principal, "cint/principal/1", "principal"); + verifyProtocolRecord(authority, "cint/authority/1", "authority"); + verifyProtocolRecord(policy, "cint/policy/1", "policy"); + verifySealedRecord(adapter_capability, "adapter capability"); + verifySealedRecord(machine_state, "machine state"); const checkedAt = isoInstant(now, "challenge time"); const reasons = []; if (!machine_state.available) { @@ -87,6 +93,9 @@ export function runCounterIntentChallenge({ 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")); } diff --git a/src/cint/cli.js b/src/cint/cli.js index 4cdca0b..dc5d62e 100644 --- a/src/cint/cli.js +++ b/src/cint/cli.js @@ -1,4 +1,3 @@ -import { main as legacyMain } from "../cli.js"; import { CintError } from "./canonical.js"; export const CINT_IDENTITY = Object.freeze({ @@ -44,6 +43,9 @@ export async function main(argv = process.argv.slice(2), options = {}) { write(`${JSON.stringify({ protocol: "cint/schema-index/1", directory: "schemas/cint", strict: true }, null, 2)}\n`); return 0; } - if (command === "legacy") return legacyMain(rest); + if (command === "legacy") { + const { main: legacyMain } = await import("../cli.js"); + return legacyMain(rest); + } throw new CintError("CINT_CLI_COMMAND", `Unknown CINT command: ${command}`); } diff --git a/src/cint/execution.js b/src/cint/execution.js index 8d898a0..a92a651 100644 --- a/src/cint/execution.js +++ b/src/cint/execution.js @@ -5,6 +5,7 @@ import { assertExactKeys, isoInstant, sealRecord, + verifyProtocolRecord, verifySealedRecord } from "./canonical.js"; import { createOutcome } from "./outcome.js"; @@ -15,6 +16,31 @@ function eventId(type) { return `event.${type.toLowerCase()}.${randomUUID()}`; } +function availableExecutionRuntime(input) { + try { + verifySealedRecord(input.adapter?.capability, "adapter capability"); + } catch { + return false; + } + const capability = input.adapter.capability; + return ( + typeof input.snapshot_provider === "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", @@ -96,15 +122,7 @@ export async function executeWithReceipt(input) { "execution request" ); const at = isoInstant(input.at, "execution time"); - const available = - typeof input.snapshot_provider === "function" && - input.receipt_authority?.verify instanceof Function && - input.store?.consume instanceof Function && - input.adapter?.prepare instanceof Function && - input.adapter?.execute instanceof Function && - input.seal_authority?.issue instanceof Function && - input.ledger?.record instanceof Function; - if (!available) { + if (!availableExecutionRuntime(input)) { return failureResult({ receipt: input.receipt, status: "FAIL_CLOSED", @@ -114,7 +132,7 @@ export async function executeWithReceipt(input) { }); } try { - verifySealedRecord(input.receipt, "receipt"); + verifyProtocolRecord(input.receipt, "cint/decision-receipt/1", "receipt"); } catch (error) { return failureResult({ receipt: input.receipt, @@ -181,7 +199,12 @@ export async function executeWithReceipt(input) { actionStarted: false }); } - assertCint(input.adapter.id === currentSnapshot.adapter_capability.id, "CINT_ADAPTER_MISMATCH", "Runtime adapter does not match the current capability"); + 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 @@ -203,14 +226,44 @@ export async function executeWithReceipt(input) { let actionStarted = false; try { prepared = await input.adapter.prepare(currentSnapshot.intent, { at, signal: input.signal }); - await record(input.ledger, "EXECUTION_STARTED", at, { + 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(); + 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 execution-bound capability" + ); actionStarted = true; execution = await input.adapter.execute(prepared, { at, signal: input.signal }); verifySealedRecord(execution, "adapter execution"); + await record(input.ledger, "EXECUTION_COMPLETED", at, { + receipt_digest: input.receipt.digest, + execution_digest: execution.digest, + revalidation_digest: revalidation.digest + }); const verification = await input.adapter.verify(prepared, execution, { at, signal: input.signal }); verifySealedRecord(verification, "outcome verification"); if (verification.status === "VERIFIED") { @@ -241,16 +294,20 @@ export async function executeWithReceipt(input) { error_code: error.code ?? "CINT_EXECUTION_FAILED", interrupted_at: at }); - const verification = await input.adapter.verify(prepared, interruptedExecution, { at, signal: input.signal }).catch(() => - sealRecord({ + let verification; + try { + verification = await input.adapter.verify(prepared, interruptedExecution, { at, signal: input.signal }); + verifySealedRecord(verification, "outcome verification"); + } catch { + verification = sealRecord({ protocol: "cint/outcome-verification/1", status: "DIVERGED", target: "unknown", - expected_sha256: prepared.after_sha256, + expected_sha256: prepared.after_sha256 ?? "0".repeat(64), actual_sha256: "0".repeat(64), checked_at: at - }) - ); + }); + } const rollback = await performRollback(input.adapter, prepared, { at, signal: input.signal }); if (rollback.status === "RESTORED") { const outcome = createOutcome({ @@ -260,14 +317,26 @@ export async function executeWithReceipt(input) { rollback, completed_at: at }); - return await sealedResult({ - input, - consumption, - revalidation, - outcome, - status: "ROLLED_BACK", - errorCode: error.code ?? "CINT_EXECUTION_FAILED" - }); + try { + return await sealedResult({ + input, + consumption, + revalidation, + outcome, + status: "ROLLED_BACK", + errorCode: error.code ?? "CINT_EXECUTION_FAILED" + }); + } catch (sealError) { + return failureResult({ + receipt: input.receipt, + status: "FAIL_CLOSED", + code: sealError.code ?? "CINT_SEAL_FAILED", + at, + consumption, + revalidation, + actionStarted + }); + } } } return failureResult({ diff --git a/src/cint/index.js b/src/cint/index.js index 90fd9c4..031c42d 100644 --- a/src/cint/index.js +++ b/src/cint/index.js @@ -1,6 +1,4 @@ export * from "./authority.js"; -export * from "./adapters/synthetic-file-patch.js"; -export * from "./adapters/codex/index.js"; export * from "./canonical.js"; export * from "./challenge.js"; export * from "./cli.js"; @@ -15,6 +13,7 @@ export * from "./principal.js"; export * from "./receipt.js"; export * from "./revalidation.js"; export * from "./rollback.js"; +export * from "./schema.js"; export * from "./seal.js"; export * from "./state-machine.js"; export * from "./store.js"; diff --git a/src/cint/outcome.js b/src/cint/outcome.js index 60477c4..b8c2066 100644 --- a/src/cint/outcome.js +++ b/src/cint/outcome.js @@ -1,4 +1,11 @@ -import { assertCint, assertExactKeys, isoInstant, sealRecord, verifySealedRecord } from "./canonical.js"; +import { + assertCint, + assertExactKeys, + isoInstant, + sealRecord, + verifyProtocolRecord, + verifySealedRecord +} from "./canonical.js"; export function createOutcome(input) { assertExactKeys( @@ -7,7 +14,7 @@ export function createOutcome(input) { [], "outcome" ); - verifySealedRecord(input.receipt, "receipt"); + 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"); diff --git a/src/cint/policy.js b/src/cint/policy.js index 43a7197..c397316 100644 --- a/src/cint/policy.js +++ b/src/cint/policy.js @@ -6,6 +6,7 @@ import { isoInstant, sealRecord, stringArray, + verifyProtocolRecord, verifySealedRecord } from "./canonical.js"; @@ -74,8 +75,8 @@ export function createPolicySnapshot(input) { } export function evaluatePolicy({ policy, intent, adapter_capability }) { - verifySealedRecord(policy, "policy"); - verifySealedRecord(intent, "intent"); + verifyProtocolRecord(policy, "cint/policy/1", "policy"); + verifyProtocolRecord(intent, "cint/intent/1", "intent"); verifySealedRecord(adapter_capability, "adapter capability"); const reasons = []; if (policy.status !== "ACTIVE") reasons.push("CINT_POLICY_INACTIVE"); diff --git a/src/cint/receipt.js b/src/cint/receipt.js index cd29a83..a0ae7be 100644 --- a/src/cint/receipt.js +++ b/src/cint/receipt.js @@ -10,7 +10,7 @@ import { isoInstant, sealRecord, sha256Digest, - verifySealedRecord + verifyProtocolRecord } from "./canonical.js"; function unsignedReceipt(receipt) { @@ -37,7 +37,7 @@ export class DecisionReceiptAuthority { issue(input) { assertExactKeys(input, ["decision", "issued_at"], ["id", "nonce"], "receipt issuance"); - verifySealedRecord(input.decision, "decision"); + verifyProtocolRecord(input.decision, "cint/decision/1", "decision"); assertCint( input.decision.protocol === "cint/decision/1" && input.decision.status === "ADMIT" && input.decision.receipt_eligible, "CINT_RECEIPT_DECISION_INELIGIBLE", @@ -64,7 +64,7 @@ export class DecisionReceiptAuthority { } verify(receipt, options = {}) { - verifySealedRecord(receipt, "receipt"); + verifyProtocolRecord(receipt, "cint/decision-receipt/1", "receipt"); assertExactKeys( receipt, [ diff --git a/src/cint/revalidation.js b/src/cint/revalidation.js index 568306a..06ca9dd 100644 --- a/src/cint/revalidation.js +++ b/src/cint/revalidation.js @@ -1,4 +1,4 @@ -import { isoInstant, sealRecord, verifySealedRecord } from "./canonical.js"; +import { isoInstant, sealRecord, verifyProtocolRecord, verifySealedRecord } from "./canonical.js"; import { runCounterIntentChallenge } from "./challenge.js"; function unique(values) { @@ -16,16 +16,17 @@ export function revalidateReceipt(input) { reasonCodes.push(error.code ?? "CINT_RECEIPT_INVALID"); } - for (const [label, record] of Object.entries({ - intent: input.intent, - principal: input.principal, - authority: input.authority, - policy: input.policy, - adapter_capability: input.adapter_capability, - machine_state: input.machine_state - })) { + 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, null], + ["machine_state", input.machine_state, null] + ]) { try { - verifySealedRecord(record, label); + if (protocol) verifyProtocolRecord(record, protocol, label); + else verifySealedRecord(record, label); } catch (error) { reasonCodes.push(error.code ?? "CINT_RECORD_INVALID"); } @@ -97,6 +98,8 @@ export function revalidateReceipt(input) { "CINT_TARGET_CHANGED", "CINT_CONTEXT_CHANGED", "CINT_INTENT_CHANGED", + "CINT_PROTOCOL_INVALID", + "CINT_SCHEMA_INVALID", "CINT_RECORD_TAMPERED" ].includes(code) ); diff --git a/src/cint/schema.js b/src/cint/schema.js new file mode 100644 index 0000000..86e9aa1 --- /dev/null +++ b/src/cint/schema.js @@ -0,0 +1,62 @@ +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/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/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/seal.js b/src/cint/seal.js index fa351c2..080a62b 100644 --- a/src/cint/seal.js +++ b/src/cint/seal.js @@ -8,6 +8,7 @@ import { isoInstant, sealRecord, sha256Digest, + verifyProtocolRecord, verifySealedRecord } from "./canonical.js"; @@ -38,15 +39,11 @@ export class OutcomeSealAuthority { ["id"], "outcome seal issuance" ); - for (const [label, record] of Object.entries({ - receipt: input.receipt, - consumption: input.consumption, - revalidation: input.revalidation, - outcome: input.outcome, - ledger_head: input.ledger_head - })) { - verifySealedRecord(record, label); - } + 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"); @@ -70,7 +67,7 @@ export class OutcomeSealAuthority { } verify(seal) { - verifySealedRecord(seal, "evidence seal"); + verifyProtocolRecord(seal, "cint/evidence-seal/1", "evidence seal"); assertExactKeys( seal, [ diff --git a/src/cint/store.js b/src/cint/store.js index 29f28c2..d49884a 100644 --- a/src/cint/store.js +++ b/src/cint/store.js @@ -12,6 +12,7 @@ import { parseCanonicalJson, sealRecord, sha256Digest, + verifyProtocolRecord, verifySealedRecord } from "./canonical.js"; @@ -64,7 +65,7 @@ export class FileReceiptStore { } async register(receipt, input) { - verifySealedRecord(receipt, "receipt"); + verifyProtocolRecord(receipt, "cint/decision-receipt/1", "receipt"); assertCint( receipt.protocol === "cint/decision-receipt/1" && receipt.status === "ISSUED", "CINT_RECEIPT_PROTOCOL", @@ -105,7 +106,7 @@ export class FileReceiptStore { } async consume(receipt, input) { - verifySealedRecord(receipt, "receipt"); + verifyProtocolRecord(receipt, "cint/decision-receipt/1", "receipt"); assertCint( receipt.protocol === "cint/decision-receipt/1" && receipt.status === "ISSUED", "CINT_RECEIPT_PROTOCOL", diff --git a/tests/cint-codex-adapter.test.js b/tests/cint-codex-adapter.test.js index 8299e10..cd6ea98 100644 --- a/tests/cint-codex-adapter.test.js +++ b/tests/cint-codex-adapter.test.js @@ -5,13 +5,11 @@ import path from "node:path"; import test from "node:test"; import { - CodexDelegationCintAdapter, DecisionReceiptAuthority, ExecutionLedger, FileReceiptStore, OutcomeSealAuthority, createAuthorityGrant, - createCodexDelegationAction, createIntent, createMachineStateSnapshot, createPolicySnapshot, @@ -19,6 +17,10 @@ import { executeWithReceipt, 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"; diff --git a/tests/cint-decision.test.js b/tests/cint-decision.test.js index 8d300d0..99622eb 100644 --- a/tests/cint-decision.test.js +++ b/tests/cint-decision.test.js @@ -11,6 +11,7 @@ import { decide, parseCanonicalJson, resolvePrincipal, + sealRecord, transitionState } from "../src/cint/index.js"; import { canonicalJson } from "../src/util.js"; @@ -85,6 +86,7 @@ function records(overrides = {}) { 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, @@ -191,6 +193,20 @@ test("tampered sealed input cannot reach a decision", () => { ); }); +test("rehashed authority with an alien protocol cannot reach ADMIT", () => { + const current = records(); + const { digest, ...unsigned } = current.authority; + const forged = sealRecord({ + ...unsigned, + protocol: "not-cint/authority/999", + schema_forbidden_field: true + }); + assert.throws( + () => decideRecords({ ...current, authority: forged }, { id: "decision.schema-invalid-authority" }), + (error) => error.code === "CINT_PROTOCOL_INVALID" + ); +}); + test("decision cannot outlive its authority", () => { assert.throws( () => decideRecords(records(), { expires_at: "2026-08-27T02:00:00.000Z" }), diff --git a/tests/cint-execution.test.js b/tests/cint-execution.test.js index 95b1452..8bd9f06 100644 --- a/tests/cint-execution.test.js +++ b/tests/cint-execution.test.js @@ -6,11 +6,12 @@ import test from "node:test"; import { CintError, + CINT_SCHEMA_PROTOCOLS, DecisionReceiptAuthority, ExecutionLedger, FileReceiptStore, OutcomeSealAuthority, - SyntheticFilePatchAdapter, + canonicalDigest, createAdapterCapability, createAuthorityGrant, createIntent, @@ -18,8 +19,15 @@ import { createPolicySnapshot, decide, executeWithReceipt, - resolvePrincipal + resolvePrincipal, + revalidateReceipt, + revokeAuthority, + runCounterIntentChallenge, + sealRecord, + validateCintSchema, + verifyProtocolRecord } from "../src/cint/index.js"; +import { SyntheticFilePatchAdapter } from "../src/cint/adapters/synthetic-file-patch.js"; import { sha256 } from "../src/util.js"; const T0 = "2026-08-27T00:00:00.000Z"; @@ -218,6 +226,7 @@ test("silent, out-of-authority, and rollback-free actions cannot obtain a receip id: "cint.adapter.synthetic-file-patch", action_types: ["SYNTHETIC_FILE_PATCH"], consequence_classes: ["CONSEQUENTIAL"], + prepare_side_effect_free: true, rollback: false, interrupt: true, outcome_verification: true @@ -334,6 +343,175 @@ test("unavailable CINT dependency fails closed without consuming or acting", asy }); }); +test("missing adapter verifier fails closed before receipt consumption", async () => { + await withRuntime(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), + 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_UNAVAILABLE"); + assert.equal(result.action_started, false); + assert.equal((await runtime.store.inspect(runtime.receipt.id)).state, "PENDING"); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("runtime adapter capability mismatch is rejected before preparation", async () => { + await withRuntime(async (runtime) => { + let prepareCalls = 0; + const capability = createAdapterCapability({ + id: runtime.adapter.id, + action_types: ["SYNTHETIC_FILE_PATCH"], + consequence_classes: ["CONSEQUENTIAL"], + prepare_side_effect_free: true, + rollback: true, + interrupt: false, + outcome_verification: true + }); + const adapter = { + id: runtime.adapter.id, + capability, + prepare: async (...args) => { + 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"); + assert.equal(result.action_started, false); + assert.equal(prepareCalls, 0); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("policy drift introduced during preparation is revalidated before execution", async () => { + await withRuntime(async (runtime) => { + let activeSnapshot = 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); + activeSnapshot = { + ...runtime.snapshot, + policy: runtime.makePolicy({ version: "r0.2", epoch: 2, issued_at: T1 }) + }; + return prepared; + }, + execute: async (...args) => { + 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 () => { + snapshotCalls += 1; + return activeSnapshot; + } + }); + assert.equal(result.status, "REVOKED"); + assert.equal(result.action_started, false); + assert.equal(snapshotCalls, 3); + assert.equal(executeCalls, 0); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("authority revocation introduced during preparation is revalidated before execution", async () => { + await withRuntime(async (runtime) => { + let activeSnapshot = 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); + activeSnapshot = { + ...runtime.snapshot, + authority: revokeAuthority(runtime.authority, { + revoked_at: T1, + reason: "Execution authority withdrawn during preparation" + }) + }; + return prepared; + }, + execute: async (...args) => { + 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); + assert.equal(executeCalls, 0); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("all eleven public protocol schemas execute on valid and invalid runtime records", async () => { + await withRuntime(async (runtime) => { + const challenge = runCounterIntentChallenge({ ...runtime.snapshot, now: T1 }); + const revalidation = revalidateReceipt({ + receipt: runtime.receipt, + receipt_authority: runtime.receipt_authority, + ...runtime.snapshot, + now: T1 + }); + const result = await runtime.execute(); + const records = [ + runtime.authority, + challenge, + runtime.decision, + result, + runtime.snapshot.intent, + result.outcome, + runtime.policy, + runtime.principal, + runtime.receipt, + revalidation, + result.evidence_seal + ]; + assert.equal(CINT_SCHEMA_PROTOCOLS.length, 11); + assert.deepEqual( + new Set(records.map((record) => record.protocol)), + new Set(CINT_SCHEMA_PROTOCOLS) + ); + for (const record of records) { + assert.equal(validateCintSchema(record).valid, true, record.protocol); + const { digest, ...unsigned } = record; + assert.throws( + () => sealRecord({ ...unsigned, schema_forbidden_field: true }), + (error) => error.code === "CINT_SCHEMA_INVALID", + record.protocol + ); + const invalidUnsigned = { ...unsigned, schema_forbidden_field: true }; + const forgedInput = { ...invalidUnsigned, digest: canonicalDigest(invalidUnsigned) }; + assert.equal(validateCintSchema(forgedInput).valid, false, record.protocol); + assert.throws( + () => verifyProtocolRecord(forgedInput, record.protocol, "forged input"), + (error) => error.code === "CINT_SCHEMA_INVALID", + record.protocol + ); + } + }); +}); + test("changed target bytes after decision fail closed before action", async () => { await withRuntime(async (runtime) => { await writeFile(runtime.target, "external-change\n", "utf8"); diff --git a/tests/cint-import-boundary.test.js b/tests/cint-import-boundary.test.js new file mode 100644 index 0000000..7537926 --- /dev/null +++ b/tests/cint-import-boundary.test.js @@ -0,0 +1,33 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import path from "node:path"; +import { promisify } from "node:util"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import test from "node:test"; + +const executeFile = promisify(execFile); +const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const LOADER = path.join(PROJECT_ROOT, "tests", "fixtures", "deny-cint-adapter-loader.mjs"); + +async function runDeniedImportProbe(arguments_) { + return executeFile(process.execPath, ["--no-warnings", "--experimental-loader", LOADER, ...arguments_], { + cwd: PROJECT_ROOT + }); +} + +test("the supported CINT core entrypoint loads without Adapter 01", async () => { + const core = pathToFileURL(path.join(PROJECT_ROOT, "src", "cint", "index.js")).href; + await runDeniedImportProbe(["--input-type=module", "--eval", `await import(${JSON.stringify(core)})`]); +}); + +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"); +}); + +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") + ); +}); diff --git a/tests/cint-receipt.test.js b/tests/cint-receipt.test.js index edb8ec4..2bb7e5a 100644 --- a/tests/cint-receipt.test.js +++ b/tests/cint-receipt.test.js @@ -86,6 +86,7 @@ function decisionFor(overrides = {}) { 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 diff --git a/tests/fixtures/deny-cint-adapter-loader.mjs b/tests/fixtures/deny-cint-adapter-loader.mjs new file mode 100644 index 0000000..9a7d6bd --- /dev/null +++ b/tests/fixtures/deny-cint-adapter-loader.mjs @@ -0,0 +1,13 @@ +const DENIED_PATHS = [ + "/src/adapters/codex-delegation/", + "/src/cint/adapters/codex/", + "/src/cli.js" +]; + +export async function resolve(specifier, context, nextResolve) { + const resolved = await nextResolve(specifier, context); + if (DENIED_PATHS.some((denied) => resolved.url.includes(denied))) { + throw new Error(`CINT_IMPORT_BOUNDARY: denied eager load of ${resolved.url}`); + } + return resolved; +} From 1343b88e1b95c8e299a8bfa7d3b0786d8347c8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Thu, 27 Aug 2026 23:43:22 +0300 Subject: [PATCH 10/16] cint(r0): enforce exact state protocols --- README.md | 2 +- docs/RELEASE.md | 2 +- docs/SECURITY-MODEL.md | 4 +- docs/THREAT-MODEL.md | 2 +- schemas/cint/adapter-capability.schema.json | 45 +++++++++++++ schemas/cint/machine-state.schema.json | 21 ++++++ src/cint/challenge.js | 7 +- src/cint/execution.js | 2 +- src/cint/policy.js | 5 +- src/cint/revalidation.js | 9 ++- src/cint/schema.js | 2 + tests/cint-decision.test.js | 28 ++++++++ tests/cint-execution.test.js | 71 ++++++++++++++++++++- 13 files changed, 180 insertions(+), 20 deletions(-) create mode 100644 schemas/cint/adapter-capability.schema.json create mode 100644 schemas/cint/machine-state.schema.json diff --git a/README.md b/README.md index 93fb360..b9800c0 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ absent. The R0 proof establishes: -- strict canonical objects and runtime enforcement of all 11 public JSON +- strict canonical objects and runtime enforcement of all 13 public JSON schemas; - explicit principal, authority, policy, machine-state, and counter-intent contracts; diff --git a/docs/RELEASE.md b/docs/RELEASE.md index dbf7872..ea7688c 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -14,7 +14,7 @@ ## Local candidate contents -- strict canonical contracts and runtime enforcement of all 11 public schemas; +- strict canonical contracts and runtime enforcement of all 13 public schemas; - principal, authority, policy, challenge, and decision engine; - signed action-bound one-shot receipts; - atomic consumption, replay rejection, expiry, and revocation; diff --git a/docs/SECURITY-MODEL.md b/docs/SECURITY-MODEL.md index 30bd07f..847eed6 100644 --- a/docs/SECURITY-MODEL.md +++ b/docs/SECURITY-MODEL.md @@ -12,7 +12,7 @@ CINT-R0 enforces these properties for its implemented local adapters: - every receipt expires, revalidates, and reaches one terminal store state; - replay, substitution, forgery, stale policy, revoked authority, drift, and unavailable controls stop before action; -- all 11 public authority-bearing schemas execute at record construction and +- all 13 public authority-bearing schemas execute at record construction and verification boundaries; - missing adapter or seal verification fails before receipt consumption; - outcome verification is distinct from execution; @@ -64,7 +64,7 @@ consumption, target drift, unavailable services, divergence, interrupt, rollback restoration, evidence sealing, adapter authority separation, and legacy packet drift. It also executes the independent-review regressions for a missing adapter verifier, preparation-time policy and authority drift, invalid -rehashed authority, all 11 schemas, and denied eager Adapter 01 loading. +rehashed authority, all 13 schemas, and denied eager Adapter 01 loading. The publication audit continues to reject local absolute paths, raw logs, credential formats, email addresses, session identifiers, hidden metadata, and diff --git a/docs/THREAT-MODEL.md b/docs/THREAT-MODEL.md index ea5c6cb..9ff2689 100644 --- a/docs/THREAT-MODEL.md +++ b/docs/THREAT-MODEL.md @@ -12,7 +12,7 @@ valid consumed receipt can reach an action adapter. | Component | Security role | Source evidence | |---|---|---| -| Canonical records | Execute all 11 public schemas; reject unknown fields, wrong protocols, and tampered digests | `src/cint/schema.js`, `src/cint/canonical.js` | +| 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` | diff --git a/schemas/cint/adapter-capability.schema.json b/schemas/cint/adapter-capability.schema.json new file mode 100644 index 0000000..a0266e7 --- /dev/null +++ b/schemas/cint/adapter-capability.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/adapter-capability-1.json", + "title": "CINT adapter capability", + "type": "object", + "additionalProperties": false, + "required": [ + "protocol", + "id", + "action_types", + "consequence_classes", + "prepare_side_effect_free", + "rollback", + "interrupt", + "outcome_verification", + "digest" + ], + "properties": { + "protocol": { "const": "cint/adapter-capability/1" }, + "id": { "$ref": "#/$defs/id" }, + "action_types": { + "type": "array", + "minItems": 1, + "maxItems": 64, + "uniqueItems": true, + "items": { "$ref": "#/$defs/id" } + }, + "consequence_classes": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "uniqueItems": true, + "items": { "enum": ["READ_ONLY", "CONSEQUENTIAL"] } + }, + "prepare_side_effect_free": { "type": "boolean" }, + "rollback": { "type": "boolean" }, + "interrupt": { "type": "boolean" }, + "outcome_verification": { "type": "boolean" }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/schemas/cint/machine-state.schema.json b/schemas/cint/machine-state.schema.json new file mode 100644 index 0000000..6e882bf --- /dev/null +++ b/schemas/cint/machine-state.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cint.invalid/schema/machine-state-1.json", + "title": "CINT machine state", + "type": "object", + "additionalProperties": false, + "required": ["protocol", "id", "epoch", "available", "observed_at", "state_digest", "digest"], + "properties": { + "protocol": { "const": "cint/machine-state/1" }, + "id": { "$ref": "#/$defs/id" }, + "epoch": { "type": "integer", "minimum": 1 }, + "available": { "type": "boolean" }, + "observed_at": { "type": "string", "format": "date-time" }, + "state_digest": { "$ref": "#/$defs/sha256" }, + "digest": { "$ref": "#/$defs/sha256" } + }, + "$defs": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } +} diff --git a/src/cint/challenge.js b/src/cint/challenge.js index a574aff..521bc76 100644 --- a/src/cint/challenge.js +++ b/src/cint/challenge.js @@ -5,8 +5,7 @@ import { isoInstant, sealRecord, stringArray, - verifyProtocolRecord, - verifySealedRecord + verifyProtocolRecord } from "./canonical.js"; import { evaluateAuthority } from "./authority.js"; import { evaluatePolicy } from "./policy.js"; @@ -74,8 +73,8 @@ export function runCounterIntentChallenge({ verifyProtocolRecord(principal, "cint/principal/1", "principal"); verifyProtocolRecord(authority, "cint/authority/1", "authority"); verifyProtocolRecord(policy, "cint/policy/1", "policy"); - verifySealedRecord(adapter_capability, "adapter capability"); - verifySealedRecord(machine_state, "machine state"); + 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) { diff --git a/src/cint/execution.js b/src/cint/execution.js index a92a651..59b0ac9 100644 --- a/src/cint/execution.js +++ b/src/cint/execution.js @@ -18,7 +18,7 @@ function eventId(type) { function availableExecutionRuntime(input) { try { - verifySealedRecord(input.adapter?.capability, "adapter capability"); + verifyProtocolRecord(input.adapter?.capability, "cint/adapter-capability/1", "adapter capability"); } catch { return false; } diff --git a/src/cint/policy.js b/src/cint/policy.js index c397316..4ac7f91 100644 --- a/src/cint/policy.js +++ b/src/cint/policy.js @@ -6,8 +6,7 @@ import { isoInstant, sealRecord, stringArray, - verifyProtocolRecord, - verifySealedRecord + verifyProtocolRecord } from "./canonical.js"; export function createPolicySnapshot(input) { @@ -77,7 +76,7 @@ export function createPolicySnapshot(input) { export function evaluatePolicy({ policy, intent, adapter_capability }) { verifyProtocolRecord(policy, "cint/policy/1", "policy"); verifyProtocolRecord(intent, "cint/intent/1", "intent"); - verifySealedRecord(adapter_capability, "adapter capability"); + 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"); diff --git a/src/cint/revalidation.js b/src/cint/revalidation.js index 06ca9dd..b49b095 100644 --- a/src/cint/revalidation.js +++ b/src/cint/revalidation.js @@ -1,4 +1,4 @@ -import { isoInstant, sealRecord, verifyProtocolRecord, verifySealedRecord } from "./canonical.js"; +import { isoInstant, sealRecord, verifyProtocolRecord } from "./canonical.js"; import { runCounterIntentChallenge } from "./challenge.js"; function unique(values) { @@ -21,12 +21,11 @@ export function revalidateReceipt(input) { ["principal", input.principal, "cint/principal/1"], ["authority", input.authority, "cint/authority/1"], ["policy", input.policy, "cint/policy/1"], - ["adapter_capability", input.adapter_capability, null], - ["machine_state", input.machine_state, null] + ["adapter_capability", input.adapter_capability, "cint/adapter-capability/1"], + ["machine_state", input.machine_state, "cint/machine-state/1"] ]) { try { - if (protocol) verifyProtocolRecord(record, protocol, label); - else verifySealedRecord(record, label); + verifyProtocolRecord(record, protocol, label); } catch (error) { reasonCodes.push(error.code ?? "CINT_RECORD_INVALID"); } diff --git a/src/cint/schema.js b/src/cint/schema.js index 86e9aa1..8d5ebf3 100644 --- a/src/cint/schema.js +++ b/src/cint/schema.js @@ -6,11 +6,13 @@ 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"], diff --git a/tests/cint-decision.test.js b/tests/cint-decision.test.js index 99622eb..9351665 100644 --- a/tests/cint-decision.test.js +++ b/tests/cint-decision.test.js @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + canonicalDigest, createAdapterCapability, createAuthorityGrant, createIntent, @@ -112,6 +113,12 @@ function decideRecords(values, overrides = {}) { }); } +function rehashRecord(record, overrides) { + const { digest, ...unsigned } = record; + const changed = { ...unsigned, ...overrides }; + return { ...changed, digest: canonicalDigest(changed) }; +} + test("canonical parser admits only the canonical byte representation", () => { const value = { z: 1, a: [true, null] }; assert.deepEqual(parseCanonicalJson(canonicalJson(value)), { a: [true, null], z: 1 }); @@ -207,6 +214,27 @@ test("rehashed authority with an alien protocol cannot reach ADMIT", () => { ); }); +test("adapter capability and machine state require exact protocols and fields", () => { + const current = records(); + for (const [field, alienProtocol] of [ + ["adapter_capability", "not-cint/adapter-capability/999"], + ["machine_state", "not-cint/machine-state/999"] + ]) { + 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", + 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", + field + ); + } +}); + test("decision cannot outlive its authority", () => { assert.throws( () => decideRecords(records(), { expires_at: "2026-08-27T02:00:00.000Z" }), diff --git a/tests/cint-execution.test.js b/tests/cint-execution.test.js index 8bd9f06..d71c9cb 100644 --- a/tests/cint-execution.test.js +++ b/tests/cint-execution.test.js @@ -35,6 +35,12 @@ const T1 = "2026-08-27T00:01:00.000Z"; const T2 = "2026-08-27T00:10:00.000Z"; const EXPIRY = "2026-08-27T01:00:00.000Z"; +function rehashRecord(record, overrides) { + const { digest, ...unsigned } = record; + const changed = { ...unsigned, ...overrides }; + return { ...changed, digest: canonicalDigest(changed) }; +} + async function withRuntime(run) { const root = await mkdtemp(path.join(os.tmpdir(), "cint-execution-test-")); const target = path.join(root, "target.txt"); @@ -393,6 +399,65 @@ test("runtime adapter capability mismatch is rejected before preparation", async }); }); +test("alien runtime adapter capability fails before receipt consumption", async () => { + await withRuntime(async (runtime) => { + const capability = rehashRecord(runtime.adapter.capability, { + protocol: "not-cint/adapter-capability/999", + forbidden_field: true + }); + const adapter = { + id: runtime.adapter.id, + capability, + prepare: runtime.adapter.prepare.bind(runtime.adapter), + 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_UNAVAILABLE"); + assert.equal(result.action_started, false); + assert.equal((await runtime.store.inspect(runtime.receipt.id)).state, "PENDING"); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("revalidation rejects alien adapter capability and machine state protocols", async () => { + await withRuntime(async (runtime) => { + for (const [field, protocol] of [ + ["adapter_capability", "not-cint/adapter-capability/999"], + ["machine_state", "not-cint/machine-state/999"] + ]) { + const record = rehashRecord(runtime.snapshot[field], { protocol, forbidden_field: true }); + const revalidation = revalidateReceipt({ + receipt: runtime.receipt, + receipt_authority: runtime.receipt_authority, + ...runtime.snapshot, + [field]: record, + now: T1 + }); + assert.equal(revalidation.status, "REJECTED", field); + assert(revalidation.reason_codes.includes("CINT_PROTOCOL_INVALID"), field); + } + }); +}); + +test("alien machine state fails execution admission before action", async () => { + await withRuntime(async (runtime) => { + const machine_state = rehashRecord(runtime.machine_state, { + protocol: "not-cint/machine-state/999", + forbidden_field: true + }); + const result = await runtime.execute({ + snapshot_provider: async () => ({ ...runtime.snapshot, machine_state }) + }); + assert.equal(result.status, "REJECTED"); + assert.equal(result.action_started, false); + assert.equal((await runtime.store.inspect(runtime.receipt.id)).state, "REJECTED"); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + test("policy drift introduced during preparation is revalidated before execution", async () => { await withRuntime(async (runtime) => { let activeSnapshot = runtime.snapshot; @@ -464,7 +529,7 @@ test("authority revocation introduced during preparation is revalidated before e }); }); -test("all eleven public protocol schemas execute on valid and invalid runtime records", async () => { +test("all thirteen public protocol schemas execute on valid and invalid runtime records", async () => { await withRuntime(async (runtime) => { const challenge = runCounterIntentChallenge({ ...runtime.snapshot, now: T1 }); const revalidation = revalidateReceipt({ @@ -475,11 +540,13 @@ test("all eleven public protocol schemas execute on valid and invalid runtime re }); const result = await runtime.execute(); const records = [ + runtime.adapter.capability, runtime.authority, challenge, runtime.decision, result, runtime.snapshot.intent, + runtime.machine_state, result.outcome, runtime.policy, runtime.principal, @@ -487,7 +554,7 @@ test("all eleven public protocol schemas execute on valid and invalid runtime re revalidation, result.evidence_seal ]; - assert.equal(CINT_SCHEMA_PROTOCOLS.length, 11); + assert.equal(CINT_SCHEMA_PROTOCOLS.length, 13); assert.deepEqual( new Set(records.map((record) => record.protocol)), new Set(CINT_SCHEMA_PROTOCOLS) From a008b81e745d98637288be07e83f00a894e786bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Fri, 28 Aug 2026 00:13:34 +0300 Subject: [PATCH 11/16] ci(cint): add remote verification gate --- .github/workflows/cint-r0-verification.yml | 73 +++++++++++++++++ CHANGELOG.md | 12 +++ CINT_EVALUATION.md | 18 +++-- README.md | 19 ++++- docs/RELEASE.md | 25 ++++-- package.json | 1 + scripts/publication-audit.mjs | 5 +- scripts/verify-cint-schema-package.mjs | 92 ++++++++++++++++++++++ src/cint/cli.js | 7 +- tests/cint-cli.test.js | 8 +- 10 files changed, 238 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/cint-r0-verification.yml create mode 100644 scripts/verify-cint-schema-package.mjs diff --git a/.github/workflows/cint-r0-verification.yml b/.github/workflows/cint-r0-verification.yml new file mode 100644 index 0000000..3bb946b --- /dev/null +++ b/.github/workflows/cint-r0-verification.yml @@ -0,0 +1,73 @@ +name: CINT R0 verification + +on: + pull_request: + branches: + - main + push: + branches: + - main + - cint-r0-reassembly + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: cint-r0-verification-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + verification-matrix: + name: ${{ matrix.os }} / Node ${{ matrix.node }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: + - ubuntu-24.04 + - macos-26 + - windows-2025 + node: + - "20" + - "24" + - "26" + + steps: + - name: Check out the exact candidate + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Select Node.js ${{ matrix.node }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ matrix.node }} + package-manager-cache: false + + - name: Install the locked dependency graph + run: npm ci + + - name: Verify all 13 registered and packaged schemas + run: npm run schema:verify + + - name: Run the complete CINT and historical AF-G0 verification + run: npm run verify + + remote-verification: + name: CINT-R0 remote verification + if: ${{ always() }} + needs: + - verification-matrix + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require every 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/CHANGELOG.md b/CHANGELOG.md index 6aba3ed..734aafd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## CINT-R0 — remote review gate + +- Published the architecture-accepted candidate as the public + `cint-r0-reassembly` review branch and draft pull request. +- Added pinned, read-only GitHub Actions verification for Node.js 20, 24, and 26 + on Linux, macOS, and Windows. +- Added a mechanical 13-schema registry and package-surface check. +- Corrected active terminology: public source exposure is yes; public default + product and public release remain no. +- Preserved the ten architecture commits without squash, rebase, merge, release, + package publication, or repository rename. + ## CINT-R0 — local review candidate - Reassembled the active product as SI1 CINT, Machine Counterintelligence diff --git a/CINT_EVALUATION.md b/CINT_EVALUATION.md index 3a4f4bf..29a76ff 100644 --- a/CINT_EVALUATION.md +++ b/CINT_EVALUATION.md @@ -1,26 +1,32 @@ -# Independent CINT-R0 evaluation +# Independent CINT-R0 remote evaluation -Use a clean local checkout of the candidate branch. Do not push, publish, -release, rename, or operate external services. +Review draft pull request #1 from `cint-r0-reassembly` into the unchanged legacy +`main`. The accepted architecture commit is +`1343b88e1b95c8e299a8bfa7d3b0786d8347c8c4`. Do not merge, release, rename the +repository, publish a package, or operate unrelated external services. ```sh npm ci npm test npm run cint:identity +npm run schema:verify npm run verify ``` Require all of the following: - every test passes with zero failures; +- all 13 runtime schemas are registered and present in the package dry run; - the historical AF-G0 demo remains reproducible; - dependency, publication, and immutable historical evidence checks pass; -- `cint:identity` reports `LOCAL_R0_CANDIDATE` and publication authority - `NONE`; +- `cint:identity` reports `REMOTE_R0_REVIEW_CANDIDATE`, public source exposure + `YES`, public default product `NO`, public release `NO`, and remaining + publication authority `NONE`; - `artifacts/cint-r0/gate-ledger.json` contains one ordered pass per completed gate; - Git status is clean after verification; -- the branch has no remote publication side effect. +- the `CINT-R0 remote verification` check passes for Node.js 20, 24, and 26 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/README.md b/README.md index b9800c0..3f47597 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,22 @@ request ## R0 state -`CINT-R0` is a local successor candidate. Publication, push, release, package -publication, repository rename, and external announcement authority are all -absent. +`CINT-R0` is an architecture-accepted public review candidate on +`cint-r0-reassembly`. Its source is publicly exposed through the review branch +and draft pull request. The default branch remains the legacy Agent Floor +product, and no merge, public release, package publication, repository rename, +or external announcement is authorized. + +| Public state | Value | +|---|---| +| Source exposure | Yes — review branch and draft pull request | +| Default product | No — `main` remains legacy Agent Floor | +| Release | No | + +Remote review 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`. The R0 proof establishes: diff --git a/docs/RELEASE.md b/docs/RELEASE.md index ea7688c..87454e9 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1,4 +1,4 @@ -# CINT-R0 local release-candidate state +# CINT-R0 remote review-candidate state ## Identity @@ -10,7 +10,11 @@ | Invariant | No consequential action without current decision-bound authority | | Branch | `cint-r0-reassembly` | | Baseline | `v0.1.0-af-g0` at `d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49` | -| State | Local review candidate | +| Accepted architecture | `1343b88e1b95c8e299a8bfa7d3b0786d8347c8c4` | +| State | Remote review candidate | +| Public source exposure | Yes | +| Public default product | No — `main` remains legacy Agent Floor | +| Public release | No | ## Local candidate contents @@ -24,11 +28,16 @@ - preserved Agent Floor foundation as Codex Adapter 01; - gate-by-gate local commits and evidence artifacts. -## Publication hold +## Remote verification -No remote branch, push, merge, tag, release, package publication, repository -rename, media action, or external announcement is authorized or implied by a -local pass. +Every pull-request head 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. -Only an explicit later authority may select, review, and publish any successor. -The historical tag and public release remain immutable. +## Publication boundary + +The source branch and draft pull request are public. That exposure does not +authorize merge, default-product cutover, tag, release, package publication, +repository rename, media action, or external announcement. The historical tag, +default branch, and public Agent Floor release remain immutable until a separate +authority decides otherwise. diff --git a/package.json b/package.json index 9991764..dab9651 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "demo": "node ./bin/agent-floor.js demo", "doctor": "node ./bin/agent-floor.js doctor", "test": "node --test ./tests/*.test.js", + "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" diff --git a/scripts/publication-audit.mjs b/scripts/publication-audit.mjs index 817fb33..9782f22 100644 --- a/scripts/publication-audit.mjs +++ b/scripts/publication-audit.mjs @@ -7,6 +7,7 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const ignoredDirectories = new Set([".git", "node_modules"]); const ignoredPrefixes = ["artifacts/generated/"]; const allowedDotfiles = new Set([".gitignore"]); +const allowedHiddenDirectories = new Set([".github"]); const privateNames = ["S" + "I1", "Y" + "I1", "S" + "I6", "F" + "YRE"]; const authorizedPublicNames = [new RegExp("\\b" + "SI1(?:[ -]CINT)" + "\\b", "gi")]; const findings = []; @@ -26,7 +27,9 @@ async function walk(directory) { } if (entry.isDirectory()) { if (ignoredDirectories.has(entry.name)) continue; - if (entry.name.startsWith(".")) findings.push({ code: "HIDDEN_DIRECTORY", path: relative }); + if (entry.name.startsWith(".") && !allowedHiddenDirectories.has(relative)) { + findings.push({ code: "HIDDEN_DIRECTORY", path: relative }); + } await walk(absolute); continue; } diff --git a/scripts/verify-cint-schema-package.mjs b/scripts/verify-cint-schema-package.mjs new file mode 100644 index 0000000..4d64fcf --- /dev/null +++ b/scripts/verify-cint-schema-package.mjs @@ -0,0 +1,92 @@ +#!/usr/bin/env node +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"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const schemaDirectory = path.join(root, "schemas", "cint"); +const failures = []; + +const directorySchemas = (await readdir(schemaDirectory)) + .filter((name) => name.endsWith(".schema.json")) + .sort(); +const directoryProtocols = []; + +for (const file of directorySchemas) { + const schema = JSON.parse(await readFile(path.join(schemaDirectory, file), "utf8")); + const protocol = schema.properties?.protocol?.const; + if (typeof protocol !== "string") { + failures.push({ code: "SCHEMA_PROTOCOL_MISSING", file }); + } else { + directoryProtocols.push(protocol); + } +} + +const registeredProtocols = [...CINT_SCHEMA_PROTOCOLS].sort(); +directoryProtocols.sort(); +if (directorySchemas.length !== 13) { + failures.push({ code: "SCHEMA_DIRECTORY_COUNT", expected: 13, actual: directorySchemas.length }); +} +if (registeredProtocols.length !== 13) { + failures.push({ code: "SCHEMA_REGISTRY_COUNT", expected: 13, actual: registeredProtocols.length }); +} +if (JSON.stringify(directoryProtocols) !== JSON.stringify(registeredProtocols)) { + failures.push({ code: "SCHEMA_REGISTRY_MISMATCH" }); +} + +const npmCli = process.env.npm_execpath; +const pack = npmCli + ? spawnSync( + process.execPath, + [npmCli, "pack", "--dry-run", "--json", "--ignore-scripts"], + { cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 } + ) + : spawnSync( + process.platform === "win32" ? "npm.cmd" : "npm", + ["pack", "--dry-run", "--json", "--ignore-scripts"], + { + cwd: root, + encoding: "utf8", + maxBuffer: 16 * 1024 * 1024, + shell: process.platform === "win32" + } + ); + +let packagedSchemas = []; +if (pack.status !== 0) { + failures.push({ code: "PACKAGE_DRY_RUN_FAILED", status: pack.status }); +} else { + try { + const packageResult = JSON.parse(pack.stdout); + packagedSchemas = (packageResult[0]?.files ?? []) + .map(({ path: packagedPath }) => packagedPath) + .filter((packagedPath) => packagedPath.startsWith("schemas/cint/") && packagedPath.endsWith(".schema.json")) + .map((packagedPath) => path.posix.basename(packagedPath)) + .sort(); + } catch { + failures.push({ code: "PACKAGE_DRY_RUN_INVALID_JSON" }); + } +} + +if (JSON.stringify(packagedSchemas) !== JSON.stringify(directorySchemas)) { + failures.push({ + code: "PACKAGED_SCHEMA_MISMATCH", + expected: directorySchemas, + actual: packagedSchemas + }); +} + +const result = { + gate: "CINT-SCHEMA-PACKAGE", + verdict: failures.length === 0 ? "PASS" : "FAIL", + registered_schemas: registeredProtocols.length, + directory_schemas: directorySchemas.length, + packaged_schemas: packagedSchemas.length, + failures +}; + +process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +if (failures.length > 0) process.exitCode = 1; diff --git a/src/cint/cli.js b/src/cint/cli.js index dc5d62e..4e92100 100644 --- a/src/cint/cli.js +++ b/src/cint/cli.js @@ -7,8 +7,11 @@ export const CINT_IDENTITY = Object.freeze({ tagline: "No consequential action without current authority.", mission: "Prevent silent or unauthorized intent from becoming machine action.", invariant: "No consequential action without current decision-bound authority.", - release_state: "LOCAL_R0_CANDIDATE", - publication_authority: "NONE", + release_state: "REMOTE_R0_REVIEW_CANDIDATE", + public_source_exposure: "YES", + public_default_product: "NO", + public_release: "NO", + remaining_publication_authority: "NONE", adapters: Object.freeze([ "cint.adapter.synthetic-file-patch", "cint.adapter.codex-delegation" diff --git a/tests/cint-cli.test.js b/tests/cint-cli.test.js index e11ebf7..5b1f528 100644 --- a/tests/cint-cli.test.js +++ b/tests/cint-cli.test.js @@ -3,13 +3,17 @@ import test from "node:test"; import { CINT_IDENTITY, main, renderCintHelp } from "../src/cint/cli.js"; -test("CINT CLI exposes the frozen product identity and no publication authority", async () => { +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); assert.deepEqual(identity, CINT_IDENTITY); assert.equal(identity.public_display, "SI1 CINT"); - assert.equal(identity.publication_authority, "NONE"); + assert.equal(identity.release_state, "REMOTE_R0_REVIEW_CANDIDATE"); + assert.equal(identity.public_source_exposure, "YES"); + assert.equal(identity.public_default_product, "NO"); + assert.equal(identity.public_release, "NO"); + assert.equal(identity.remaining_publication_authority, "NONE"); }); test("CINT CLI keeps legacy execution behind an explicit command", () => { From fa81bbab0a7c43f3459631c71fd5569c48303a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Fri, 28 Aug 2026 00:14:35 +0300 Subject: [PATCH 12/16] ci(cint): avoid duplicate PR verification --- .github/workflows/cint-r0-verification.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cint-r0-verification.yml b/.github/workflows/cint-r0-verification.yml index 3bb946b..297e7bf 100644 --- a/.github/workflows/cint-r0-verification.yml +++ b/.github/workflows/cint-r0-verification.yml @@ -7,7 +7,6 @@ on: push: branches: - main - - cint-r0-reassembly workflow_dispatch: permissions: From 2ab6f5a6655ea8c9b033753bbd08e789d099a1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Fri, 28 Aug 2026 00:17:39 +0300 Subject: [PATCH 13/16] fix(ci): close cross-platform verification gaps --- .github/workflows/cint-r0-verification.yml | 1 + CHANGELOG.md | 2 ++ src/adapters/codex-delegation/runner.js | 32 ++++++++++++++++------ src/cint/adapters/codex/index.js | 12 ++++++-- tests/cint-codex-adapter.test.js | 7 ++++- tests/cint-import-boundary.test.js | 4 ++- tests/runner.test.js | 7 ++++- 7 files changed, 52 insertions(+), 13 deletions(-) diff --git a/.github/workflows/cint-r0-verification.yml b/.github/workflows/cint-r0-verification.yml index 297e7bf..742e443 100644 --- a/.github/workflows/cint-r0-verification.yml +++ b/.github/workflows/cint-r0-verification.yml @@ -37,6 +37,7 @@ jobs: - name: Check out the exact candidate uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + fetch-depth: 0 persist-credentials: false - name: Select Node.js ${{ matrix.node }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 734aafd..9f5f80b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Added pinned, read-only GitHub Actions verification for Node.js 20, 24, and 26 on Linux, macOS, and Windows. - Added a mechanical 13-schema registry and package-surface check. +- Made immutable AF-G0 tag verification and the legacy adapter test harness + reproducible across Linux, macOS, and Windows. - Corrected active terminology: public source exposure is yes; public default product and public release remain no. - Preserved the ten architecture commits without squash, rebase, merge, release, diff --git a/src/adapters/codex-delegation/runner.js b/src/adapters/codex-delegation/runner.js index 51e3bcf..7870e22 100644 --- a/src/adapters/codex-delegation/runner.js +++ b/src/adapters/codex-delegation/runner.js @@ -179,6 +179,13 @@ export async function resolveCodexBinary(override = undefined) { } } +function normalizeCodexArgs(args = []) { + if (!Array.isArray(args) || args.some((value) => typeof value !== "string" || value.length === 0)) { + throw new AgentFloorError("AF_CODEX_ARGS_INVALID", "Codex argument prefix must contain non-empty strings"); + } + return [...args]; +} + export function buildChildProcessEnv(source = process.env) { const requested = String(source.AGENT_FLOOR_CODEX_ENV_ALLOWLIST ?? "") .split(",") @@ -373,7 +380,12 @@ async function executeCodex({ command, args, cwd, prompt, guard, timeoutSeconds }); } -export async function runGovernedChild({ spec, outputDir, codexBinary = undefined }) { +export async function runGovernedChild({ + spec, + outputDir, + codexBinary = undefined, + codexArgs = [] +}) { const out = path.resolve(outputDir); await mkdir(out, { recursive: true }); const packetRecord = await createChildPacket(spec); @@ -384,11 +396,14 @@ export async function runGovernedChild({ spec, outputDir, codexBinary = undefine await createSourceProjection(spec, projectionPath); await writeFile(instructionsPath, `${renderWorkerInstructions()}\n`, "utf8"); const command = await resolveCodexBinary(codexBinary); - const args = buildCodexArgs(spec, { - instructions: instructionsPath, - output: outputPath, - workspace: projectionPath - }); + const args = [ + ...normalizeCodexArgs(codexArgs), + ...buildCodexArgs(spec, { + instructions: instructionsPath, + output: outputPath, + workspace: projectionPath + }) + ]; const guard = new ExecutionGuard(spec.delegation); const slot = await acquireConcurrencySlot(spec); const startedAt = new Date().toISOString(); @@ -502,11 +517,12 @@ export async function runGovernedChild({ spec, outputDir, codexBinary = undefine } } -export async function inspectCodexSurface(codexBinary = undefined) { +export async function inspectCodexSurface(codexBinary = undefined, codexArgs = []) { const command = await resolveCodexBinary(codexBinary); + const prefixArgs = normalizeCodexArgs(codexArgs); const run = (args) => new Promise((resolve) => { - const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] }); + const child = spawn(command, [...prefixArgs, ...args], { stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; child.stdout.setEncoding("utf8"); diff --git a/src/cint/adapters/codex/index.js b/src/cint/adapters/codex/index.js index d41fdd4..ac68e08 100644 --- a/src/cint/adapters/codex/index.js +++ b/src/cint/adapters/codex/index.js @@ -49,11 +49,18 @@ export async function createCodexDelegationAction(spec) { export class CodexDelegationCintAdapter { constructor(input) { - assertExactKeys(input, ["spec", "output_dir"], ["codex_binary"], "Codex adapter configuration"); + 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"], @@ -89,7 +96,8 @@ export class CodexDelegationCintAdapter { const run = await runGovernedChild({ spec: this.spec, outputDir: this.output_dir, - codexBinary: this.codex_binary + codexBinary: this.codex_binary, + codexArgs: this.codex_args }); return sealRecord({ protocol: "cint/codex-delegation-execution/1", diff --git a/tests/cint-codex-adapter.test.js b/tests/cint-codex-adapter.test.js index cd6ea98..21c7305 100644 --- a/tests/cint-codex-adapter.test.js +++ b/tests/cint-codex-adapter.test.js @@ -83,7 +83,12 @@ async function withAdapterRuntime(run) { review_on_uncertainty: true, issued_at: T0 }); - const adapter = new CodexDelegationCintAdapter({ spec, output_dir: output, codex_binary: mock }); + const adapter = new CodexDelegationCintAdapter({ + spec, + output_dir: output, + codex_binary: process.execPath, + codex_args: [mock] + }); const machine_state = createMachineStateSnapshot({ id: "machine.codex-adapter", epoch: 1, diff --git a/tests/cint-import-boundary.test.js b/tests/cint-import-boundary.test.js index 7537926..22a22cc 100644 --- a/tests/cint-import-boundary.test.js +++ b/tests/cint-import-boundary.test.js @@ -7,7 +7,9 @@ import test from "node:test"; const executeFile = promisify(execFile); const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const LOADER = path.join(PROJECT_ROOT, "tests", "fixtures", "deny-cint-adapter-loader.mjs"); +const LOADER = pathToFileURL( + path.join(PROJECT_ROOT, "tests", "fixtures", "deny-cint-adapter-loader.mjs") +).href; async function runDeniedImportProbe(arguments_) { return executeFile(process.execPath, ["--no-warnings", "--experimental-loader", LOADER, ...arguments_], { diff --git a/tests/runner.test.js b/tests/runner.test.js index f3d9ea4..38e8c90 100644 --- a/tests/runner.test.js +++ b/tests/runner.test.js @@ -42,7 +42,12 @@ test("one command creates and audits a clean bounded child process", async () => await chmod(mock, 0o755); try { const { spec } = await loadTaskSpec(DEMO_SPEC); - const record = await runGovernedChild({ spec, outputDir: temporary, codexBinary: mock }); + const record = await runGovernedChild({ + spec, + outputDir: temporary, + codexBinary: process.execPath, + codexArgs: [mock] + }); assert.equal(record.context_enforcement.process_boundary, true); assert.equal(record.context_enforcement.inherited_turns, 0); assert.equal(record.context_enforcement.multi_agent_tools, "disabled"); From a45ee10d705f6e51207315f5d61e920784d6627a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Fri, 28 Aug 2026 00:20:05 +0300 Subject: [PATCH 14/16] fix(test): use cross-platform Node discovery --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dab9651..30ed30c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "cint:test": "node --test ./tests/cint-*.test.js", "demo": "node ./bin/agent-floor.js demo", "doctor": "node ./bin/agent-floor.js doctor", - "test": "node --test ./tests/*.test.js", + "test": "node --test", "schema:verify": "node ./scripts/verify-cint-schema-package.mjs", "public:audit": "node ./scripts/publication-audit.mjs", "evidence:verify": "node ./scripts/verify-evidence-manifest.mjs", From ae3502779c97ae44464714fea25e1868d5ecaa1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Fri, 28 Aug 2026 02:40:39 +0300 Subject: [PATCH 15/16] fix(cint): revalidate expiry with trusted final time --- src/cint/execution.js | 50 ++++++---- src/cint/revalidation.js | 15 ++- tests/cint-codex-adapter.test.js | 1 + tests/cint-execution.test.js | 156 +++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 24 deletions(-) diff --git a/src/cint/execution.js b/src/cint/execution.js index 59b0ac9..0d4737b 100644 --- a/src/cint/execution.js +++ b/src/cint/execution.js @@ -16,6 +16,10 @@ 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"); @@ -25,6 +29,7 @@ function availableExecutionRuntime(input) { 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" && @@ -74,10 +79,11 @@ async function sealedResult({ revalidation, outcome, status, + at, errorCode = null, actionStarted = true }) { - const outcomeEvent = await record(input.ledger, `OUTCOME_${outcome.status}`, input.at, { + const outcomeEvent = await record(input.ledger, `OUTCOME_${outcome.status}`, at, { receipt_digest: input.receipt.digest, outcome_digest: outcome.digest }); @@ -87,7 +93,7 @@ async function sealedResult({ revalidation, outcome, ledger_head: outcomeEvent, - issued_at: input.at + issued_at: at }); input.seal_authority.verify(seal); return sealRecord({ @@ -101,7 +107,7 @@ async function sealedResult({ outcome, evidence_seal: seal, error_code: errorCode, - completed_at: input.at + completed_at: at }); } @@ -113,6 +119,7 @@ export async function executeWithReceipt(input) { "receipt_authority", "store", "snapshot_provider", + "clock", "adapter", "seal_authority", "ledger", @@ -224,6 +231,7 @@ export async function executeWithReceipt(input) { 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, { @@ -233,18 +241,19 @@ export async function executeWithReceipt(input) { }); currentSnapshot = await input.snapshot_provider(); + executionAt = trustedExecutionTime(input.clock); revalidation = revalidateReceipt({ receipt: input.receipt, receipt_authority: input.receipt_authority, ...currentSnapshot, - now: at + now: executionAt }); if (revalidation.status !== "VALID") { return failureResult({ receipt: input.receipt, status: statusForError(null, revalidation), code: revalidation.reason_codes[0] ?? "CINT_REVALIDATION_FAILED", - at, + at: executionAt, consumption, revalidation, actionStarted: false @@ -257,14 +266,14 @@ export async function executeWithReceipt(input) { "Runtime adapter does not match the execution-bound capability" ); actionStarted = true; - execution = await input.adapter.execute(prepared, { at, signal: input.signal }); + execution = await input.adapter.execute(prepared, { at: executionAt, signal: input.signal }); verifySealedRecord(execution, "adapter execution"); - await record(input.ledger, "EXECUTION_COMPLETED", at, { + 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, signal: input.signal }); + const verification = await input.adapter.verify(prepared, execution, { at: executionAt, signal: input.signal }); verifySealedRecord(verification, "outcome verification"); if (verification.status === "VERIFIED") { const outcome = createOutcome({ @@ -272,31 +281,31 @@ export async function executeWithReceipt(input) { execution, verification, rollback: null, - completed_at: at + completed_at: executionAt }); - return await sealedResult({ input, consumption, revalidation, outcome, status: "SEALED" }); + return await sealedResult({ input, consumption, revalidation, outcome, status: "SEALED", at: executionAt }); } - const rollback = await performRollback(input.adapter, prepared, { at, signal: input.signal }); + 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: at + completed_at: executionAt }); - return await sealedResult({ input, consumption, revalidation, outcome, status: "ROLLED_BACK" }); + 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: at + interrupted_at: executionAt }); let verification; try { - verification = await input.adapter.verify(prepared, interruptedExecution, { at, signal: input.signal }); + verification = await input.adapter.verify(prepared, interruptedExecution, { at: executionAt, signal: input.signal }); verifySealedRecord(verification, "outcome verification"); } catch { verification = sealRecord({ @@ -305,17 +314,17 @@ export async function executeWithReceipt(input) { target: "unknown", expected_sha256: prepared.after_sha256 ?? "0".repeat(64), actual_sha256: "0".repeat(64), - checked_at: at + checked_at: executionAt }); } - const rollback = await performRollback(input.adapter, prepared, { at, signal: input.signal }); + 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: at + completed_at: executionAt }); try { return await sealedResult({ @@ -324,6 +333,7 @@ export async function executeWithReceipt(input) { revalidation, outcome, status: "ROLLED_BACK", + at: executionAt, errorCode: error.code ?? "CINT_EXECUTION_FAILED" }); } catch (sealError) { @@ -331,7 +341,7 @@ export async function executeWithReceipt(input) { receipt: input.receipt, status: "FAIL_CLOSED", code: sealError.code ?? "CINT_SEAL_FAILED", - at, + at: executionAt, consumption, revalidation, actionStarted @@ -343,7 +353,7 @@ export async function executeWithReceipt(input) { receipt: input.receipt, status: "FAIL_CLOSED", code: error.code ?? "CINT_FAIL_CLOSED", - at, + at: executionAt, consumption, revalidation, actionStarted diff --git a/src/cint/revalidation.js b/src/cint/revalidation.js index b49b095..e4c8a4b 100644 --- a/src/cint/revalidation.js +++ b/src/cint/revalidation.js @@ -8,13 +8,20 @@ function unique(values) { export function revalidateReceipt(input) { const checkedAt = isoInstant(input.now, "revalidation time"); const reasonCodes = []; - let signatureValid = false; + let receiptAuthenticated = false; try { - input.receipt_authority.verify(input.receipt, { now: checkedAt }); - signatureValid = true; + 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"], @@ -68,7 +75,7 @@ export function revalidateReceipt(input) { } if (input.machine_state?.available !== true) reasonCodes.push("CINT_UNAVAILABLE"); - if (signatureValid && reasonCodes.length === 0) { + if (receiptAuthenticated && reasonCodes.every((code) => code === "CINT_RECEIPT_EXPIRED")) { try { const challenge = runCounterIntentChallenge({ intent: input.intent, diff --git a/tests/cint-codex-adapter.test.js b/tests/cint-codex-adapter.test.js index 21c7305..3c107bd 100644 --- a/tests/cint-codex-adapter.test.js +++ b/tests/cint-codex-adapter.test.js @@ -167,6 +167,7 @@ test("legacy bounded review runs only under a consumed CINT receipt", async () = receipt_authority: runtime.receipt_authority, store: runtime.store, snapshot_provider: async () => runtime.snapshot, + clock: { now: () => T1 }, adapter: runtime.adapter, seal_authority: runtime.seal_authority, ledger: runtime.ledger, diff --git a/tests/cint-execution.test.js b/tests/cint-execution.test.js index d71c9cb..4ac6545 100644 --- a/tests/cint-execution.test.js +++ b/tests/cint-execution.test.js @@ -32,9 +32,38 @@ import { sha256 } from "../src/util.js"; const T0 = "2026-08-27T00:00:00.000Z"; const T1 = "2026-08-27T00:01:00.000Z"; +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) { + let current = initial; + return Object.freeze({ + now: () => current, + set: (next) => { + current = next; + } + }); +} + +function adapterAdvancingClock(runtime, next, counters = { execute_calls: 0 }) { + return { + id: runtime.adapter.id, + capability: runtime.adapter.capability, + prepare: async (...args) => { + const prepared = await runtime.adapter.prepare(...args); + runtime.clock.set(next); + return prepared; + }, + execute: async (...args) => { + counters.execute_calls += 1; + return runtime.adapter.execute(...args); + }, + verify: runtime.adapter.verify.bind(runtime.adapter), + rollback: runtime.adapter.rollback.bind(runtime.adapter) + }; +} + function rehashRecord(record, overrides) { const { digest, ...unsigned } = record; const changed = { ...unsigned, ...overrides }; @@ -149,6 +178,7 @@ async function withRuntime(run) { key: Buffer.alloc(32, 19) }); const ledger = new ExecutionLedger(path.join(root, "evidence", "execution.jsonl")); + const clock = createTestClock(); const runtime = { root, @@ -170,12 +200,14 @@ async function withRuntime(run) { store, seal_authority, ledger, + clock, execute: (overrides = {}) => executeWithReceipt({ receipt, receipt_authority, store, snapshot_provider: async () => snapshot, + clock, adapter, seal_authority, ledger, @@ -367,6 +399,17 @@ test("missing adapter verifier fails closed before receipt consumption", async ( }); }); +test("missing trusted clock fails closed before receipt consumption", async () => { + await withRuntime(async (runtime) => { + const result = await runtime.execute({ clock: null }); + assert.equal(result.status, "FAIL_CLOSED"); + assert.equal(result.error_code, "CINT_UNAVAILABLE"); + assert.equal(result.action_started, false); + assert.equal((await runtime.store.inspect(runtime.receipt.id)).state, "PENDING"); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + test("runtime adapter capability mismatch is rejected before preparation", async () => { await withRuntime(async (runtime) => { let prepareCalls = 0; @@ -529,6 +572,119 @@ test("authority revocation introduced during preparation is revalidated before e }); }); +test("receipt expiry crossed during preparation prevents every action", async () => { + await withRuntime(async (runtime) => { + const counters = { execute_calls: 0 }; + const result = await runtime.execute({ + adapter: adapterAdvancingClock(runtime, T2, counters) + }); + assert.equal(result.status, "REVOKED"); + assert.equal(result.error_code, "CINT_RECEIPT_EXPIRED"); + assert.equal(result.completed_at, T2); + assert.match(result.revalidation_digest, /^[a-f0-9]{64}$/); + assert.equal(result.action_started, false); + assert.equal(counters.execute_calls, 0); + assert.equal((await runtime.store.inspect(runtime.receipt.id)).state, "CONSUMED"); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("authority expiry crossed during preparation is evaluated at the final trusted time", async () => { + await withRuntime(async (runtime) => { + const counters = { execute_calls: 0 }; + const finalRevalidation = revalidateReceipt({ + receipt: runtime.receipt, + receipt_authority: runtime.receipt_authority, + ...runtime.snapshot, + now: EXPIRY + }); + assert(finalRevalidation.reason_codes.includes("CINT_RECEIPT_EXPIRED")); + assert(finalRevalidation.reason_codes.includes("CINT_AUTHORITY_EXPIRED")); + const result = await runtime.execute({ + adapter: adapterAdvancingClock(runtime, EXPIRY, counters) + }); + assert.equal(result.status, "REVOKED"); + assert.equal(result.action_started, false); + assert.equal(counters.execute_calls, 0); + assert(result.error_code === "CINT_RECEIPT_EXPIRED" || result.error_code === "CINT_AUTHORITY_EXPIRED"); + assert.equal(result.revalidation_digest, finalRevalidation.digest); + assert.equal(result.completed_at, EXPIRY); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +test("receipt lifetime is valid one millisecond before expiry and revoked at the exact boundary", async () => { + await withRuntime(async (runtime) => { + const result = await runtime.execute({ + adapter: adapterAdvancingClock(runtime, T2_BEFORE) + }); + assert.equal(result.status, "SEALED"); + assert.equal(result.action_started, true); + assert.equal(result.completed_at, T2_BEFORE); + assert.equal(await readFile(runtime.target, "utf8"), "after\n"); + }); + await withRuntime(async (runtime) => { + const counters = { execute_calls: 0 }; + const result = await runtime.execute({ + adapter: adapterAdvancingClock(runtime, T2, counters) + }); + assert.equal(result.status, "REVOKED"); + assert.equal(result.action_started, false); + assert.equal(counters.execute_calls, 0); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +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) => { + 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"); + assert.equal(result.action_started, false); + assert.equal(executeCalls, 0); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + +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) => { + 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"); } } + }); + assert.equal(result.status, "FAIL_CLOSED"); + assert.equal(result.error_code, "CINT_FAIL_CLOSED"); + assert.equal(result.action_started, false); + assert.equal(executeCalls, 0); + assert.equal(await readFile(runtime.target, "utf8"), "before\n"); + }); +}); + test("all thirteen public protocol schemas execute on valid and invalid runtime records", async () => { await withRuntime(async (runtime) => { const challenge = runCounterIntentChallenge({ ...runtime.snapshot, now: T1 }); From 75939f33c3430a7a8315ddf3b3be8b629f645469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=84=E2=84=A2=20SDK?= Date: Fri, 28 Aug 2026 03:51:19 +0300 Subject: [PATCH 16/16] CINT-R0: finalize public source release state --- CHANGELOG.md | 47 +++++++----------- CINT_EVALUATION.md | 16 +++--- README.md | 45 ++++++++++------- SECURITY.md | 5 +- docs/LIMITATIONS.md | 8 +-- docs/RELEASE.md | 43 ++++++++-------- docs/cint-r0/PUBLIC-RELEASE-NOTES.md | 74 ++++++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 12 ++++- src/cint/cli.js | 8 +-- tests/cint-cli.test.js | 8 +-- 11 files changed, 178 insertions(+), 92 deletions(-) create mode 100644 docs/cint-r0/PUBLIC-RELEASE-NOTES.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f5f80b..dfa5c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,36 +1,23 @@ # Changelog -## CINT-R0 — remote review gate +## v0.1.0-cint-r0 — 2026-08-28 -- Published the architecture-accepted candidate as the public - `cint-r0-reassembly` review branch and draft pull request. -- Added pinned, read-only GitHub Actions verification for Node.js 20, 24, and 26 - on Linux, macOS, and Windows. -- Added a mechanical 13-schema registry and package-surface check. -- Made immutable AF-G0 tag verification and the legacy adapter test harness - reproducible across Linux, macOS, and Windows. -- Corrected active terminology: public source exposure is yes; public default - product and public release remain no. -- Preserved the ten architecture commits without squash, rebase, merge, release, - package publication, or repository rename. - -## CINT-R0 — local review candidate - -- Reassembled the active product as SI1 CINT, Machine Counterintelligence - Runtime. -- Added canonical intent, principal, authority, policy, challenge, decision, - machine-state, receipt, revalidation, outcome, rollback, ledger, and seal - protocols. -- Added signed action-bound receipts with atomic one-shot consumption, replay - rejection, expiry, revocation, and fail-closed locks. -- Added the disposable synthetic action proof with immediate revalidation, - exact outcome verification, interrupt, rollback, and hash restoration. -- Isolated Agent Floor as CINT Adapter 01 while preserving historical APIs, - protocols, tests, fixtures, and evidence. -- Archived competition and historical Agent Floor narrative outside the active - product documentation. -- Kept the candidate local with no push, publication, release, package - publication, repository rename, or external announcement. +- Made SI1 CINT and `sntllgnc/CINT` the active repository and default-source + identity. +- Closed architecture findings F1-F5 at accepted architecture commit + `1343b88e1b95c8e299a8bfa7d3b0786d8347c8c4`. +- Closed `CINT-EXPIRY-001` through trusted execution-time revalidation at + `ae3502779c97ae44464714fea25e1868d5ecaa1d`. +- Passed 72/72 automated tests before the publication-state commit and in final + publication verification. +- Enforced all 13/13 public runtime schemas and preserved AF-G0 evidence at + 53/53. +- Passed remote verification on Linux, macOS, and Windows across Node.js 20, 24, + and 26. +- Preserved Agent Floor as CINT Adapter 01 and the historical + `v0.1.0-af-g0` release lineage. +- Published the source as a GitHub prerelease without npm publication or a + production-readiness claim. ## v0.1.0-af-g0 — 2026-07-18 diff --git a/CINT_EVALUATION.md b/CINT_EVALUATION.md index 29a76ff..5f303ab 100644 --- a/CINT_EVALUATION.md +++ b/CINT_EVALUATION.md @@ -1,9 +1,9 @@ -# Independent CINT-R0 remote evaluation +# Independent SI1 CINT R0 verification -Review draft pull request #1 from `cint-r0-reassembly` into the unchanged legacy +Verify the public source and verification baseline at `sntllgnc/CINT` on `main`. The accepted architecture commit is -`1343b88e1b95c8e299a8bfa7d3b0786d8347c8c4`. Do not merge, release, rename the -repository, publish a package, or operate unrelated external services. +`1343b88e1b95c8e299a8bfa7d3b0786d8347c8c4`, and the trusted execution-time +correction is `ae3502779c97ae44464714fea25e1868d5ecaa1d`. ```sh npm ci @@ -19,9 +19,9 @@ Require all of the following: - all 13 runtime schemas are registered and present in the package dry run; - the historical AF-G0 demo remains reproducible; - dependency, publication, and immutable historical evidence checks pass; -- `cint:identity` reports `REMOTE_R0_REVIEW_CANDIDATE`, public source exposure - `YES`, public default product `NO`, public release `NO`, and remaining - publication authority `NONE`; +- `cint:identity` reports `PUBLIC_R0_SOURCE`, public source exposure `YES`, public + default product `YES`, public release `YES`, and remaining publication authority + `SOURCE_RELEASE_COMPLETE`; - `artifacts/cint-r0/gate-ledger.json` contains one ordered pass per completed gate; - Git status is clean after verification; @@ -30,4 +30,4 @@ Require all of the following: 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 -model before accepting the local candidate for operator review. +model when evaluating the public R0 source baseline. diff --git a/README.md b/README.md index 3f47597..44206be 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,12 @@ **Machine Counterintelligence Runtime** -> No consequential action without current authority. +> No consequential action without current decision-bound authority. -CINT prevents silent, stale, replayed, or unauthorized intent from becoming -machine action. It reconstructs the requested intent, resolves the principal -and authority, challenges current policy and machine state, and emits one of +CINT prevents silent, stale, altered, replayed, or unauthorized intent from +becoming consequential machine action. It reconstructs the requested intent, +resolves the principal and authority, challenges current policy and machine +state, and emits one of `ADMIT`, `DENY`, or `REVIEW`. Only an `ADMIT` decision can receive a signed, action-bound, expiring, one-shot receipt. A decision alone carries no executable authority. @@ -28,19 +29,28 @@ request ## R0 state -`CINT-R0` is an architecture-accepted public review candidate on -`cint-r0-reassembly`. Its source is publicly exposed through the review branch -and draft pull request. The default branch remains the legacy Agent Floor -product, and no merge, public release, package publication, repository rename, -or external announcement is authorized. +`CINT-R0` is the public source and verification baseline for the SI1 CINT +decision-bound action-control architecture. -| Public state | Value | -|---|---| -| Source exposure | Yes — review branch and draft pull request | -| Default product | No — `main` remains legacy Agent Floor | -| Release | No | +The R0 release establishes a local, event-bound execution protocol with +runtime-schema enforcement, decision-bound one-shot receipts, trusted-time +revalidation immediately before consequential execution, outcome verification, +rollback, and evidence sealing. -Remote review is enforced by a pinned GitHub Actions matrix covering the +It is not a production authorization service, operating-system enforcement +layer, distributed consensus service, hostile-adapter sandbox, or physical +autonomous-system controller. + +Repository: +https://github.com/sntllgnc/CINT + +Release: +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`. @@ -90,7 +100,7 @@ src/cint/adapters/ explicit, separately imported R0 action adapte 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 -docs/cint-r0/ gate evidence and local review packet +docs/cint-r0/ gate evidence and public R0 release notes docs/archive/ historical Agent Floor and competition material artifacts/cint-r0/ gate ledger and sanitized gate receipts ``` @@ -104,7 +114,8 @@ artifacts/cint-r0/ gate ledger and sanitized gate receipts - [Limitations](docs/LIMITATIONS.md) - [Codex Adapter 01](docs/CODEX-ADAPTER.md) - [CINT-R0 gate ledger](artifacts/cint-r0/gate-ledger.json) -- [Local release-candidate state](docs/RELEASE.md) +- [Public 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 `v0.1.0-af-g0`. Its narrative, evaluation material, media, fixtures, protocols, diff --git a/SECURITY.md b/SECURITY.md index 677ee73..d509cfa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,8 +2,9 @@ ## Supported line -Security corrections for the local CINT-R0 candidate are developed on the -successor branch. The immutable Agent Floor release remains historical evidence. +Security corrections for the public CINT-R0 source prerelease are developed +through protected changes to `main`. The immutable Agent Floor release remains +historical evidence. ## Report a vulnerability diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 66a31e1..54d1f00 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -3,7 +3,8 @@ - R0 is a local protocol and proof runtime, not a distributed authorization service. - Receipt and seal key generation, persistence, rotation, and hardware - protection belong to the embedding process. + protection belong to the trusted embedding process. R0 also trusts that + process to supply a correct execution-time clock. - File-backed receipt consumption is atomic on one compatible local filesystem; cross-host consensus and network filesystems are not established. - Ambiguous locks fail closed. R0 deliberately provides no automatic stale-lock @@ -28,8 +29,9 @@ - CINT does not establish account billing, provider quota causation, hostile-code containment, operating-system integrity, or protection after receipt/seal key compromise. -- `CINT-R0` carries no push, publication, release, repository rename, package - publication, or external announcement authority. +- `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. The AF-G0 accounting case remains a historical Adapter 01 regression. It does not define CINT and does not establish a provider bill or quota formula. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 87454e9..796f7fe 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1,22 +1,22 @@ -# CINT-R0 remote review-candidate state +# SI1 CINT v0.1.0 — R0 source release ## Identity | Field | Value | |---|---| -| Product code | `CINT` | -| Public display | `SI1 CINT` | -| Descriptor | Machine Counterintelligence Runtime | -| Invariant | No consequential action without current decision-bound authority | -| Branch | `cint-r0-reassembly` | -| Baseline | `v0.1.0-af-g0` at `d57a80404e04d2c376cba9cc4b3fc06a5d8c8c49` | +| Product code | CINT | +| Public display | SI1 CINT | +| Repository | `sntllgnc/CINT` | +| Default branch | `main` | +| Release | `v0.1.0-cint-r0` | +| Historical baseline | `v0.1.0-af-g0` | | Accepted architecture | `1343b88e1b95c8e299a8bfa7d3b0786d8347c8c4` | -| State | Remote review candidate | -| Public source exposure | Yes | -| Public default product | No — `main` remains legacy Agent Floor | -| Public release | No | +| Trusted-time correction | `ae3502779c97ae44464714fea25e1868d5ecaa1d` | +| State | Public R0 source and verification baseline | +| Production readiness | Not claimed | +| npm publication | Not performed | -## Local candidate contents +## R0 contents - strict canonical contracts and runtime enforcement of all 13 public schemas; - principal, authority, policy, challenge, and decision engine; @@ -26,18 +26,21 @@ fail-closed dependency preflight; - synthetic action, verification, interrupt, rollback, ledger, and seal; - preserved Agent Floor foundation as Codex Adapter 01; -- gate-by-gate local commits and evidence artifacts. +- gate-by-gate commits and sanitized evidence artifacts. ## Remote verification -Every pull-request head must pass `CINT-R0 remote verification`. The aggregate +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. -## Publication boundary +## Release boundary -The source branch and draft pull request are public. That exposure does not -authorize merge, default-product cutover, tag, release, package publication, -repository rename, media action, or external announcement. The historical tag, -default branch, and public Agent Floor release remain immutable until a separate -authority decides otherwise. +`v0.1.0-cint-r0` is the public source prerelease. It does not claim production +readiness and does not publish `@sntllgnc/cint` to npm. It grants no authority +for production deployment, third-party operational reliance, media action, or +physical autonomous action. The historical `v0.1.0-af-g0` tag and Agent Floor +release remain unchanged. + +The complete technical limitations and non-claims are maintained in +[`docs/LIMITATIONS.md`](LIMITATIONS.md). diff --git a/docs/cint-r0/PUBLIC-RELEASE-NOTES.md b/docs/cint-r0/PUBLIC-RELEASE-NOTES.md new file mode 100644 index 0000000..a742247 --- /dev/null +++ b/docs/cint-r0/PUBLIC-RELEASE-NOTES.md @@ -0,0 +1,74 @@ +# SI1 CINT v0.1.0 — R0 + +## Identity + +SI1 CINT is the Machine Counterintelligence Runtime for decision-bound +consequential action. Its public source repository is `sntllgnc/CINT`, and this +source prerelease is `v0.1.0-cint-r0`. + +CINT prevents silent, stale, altered, replayed, or unauthorized intent from +becoming consequential machine action. + +> No consequential action without current decision-bound authority. + +## What R0 proves + +R0 establishes strict canonical intent, principal, authority, policy, +machine-state, challenge, decision, receipt, revalidation, execution-result, +outcome, and seal contracts with runtime enforcement of all 13 public schemas. +It demonstrates a local event-bound protocol from decision through controlled +action, outcome verification, rollback, and evidence sealing. + +## Decision and receipt boundary + +An `ADMIT`, `DENY`, or `REVIEW` decision carries zero executable authority. Only +an `ADMIT` decision may receive an HMAC-authenticated, action-bound, expiring, +one-shot receipt. The receipt binds the decision, action, target, context, +principal, authority and policy epochs, adapter, and machine state. Consumption +is atomic on one compatible local filesystem; replay and ambiguous locks fail +closed. + +## Trusted-time execution revalidation + +R0 revalidates authority, policy, machine state, receipt state, and expiry while +the receipt is locked, after consumption, and again with fresh trusted time +immediately before consequential execution. A receipt that expires during +preparation cannot cross the execution boundary. + +## Adapters + +- The synthetic file-patch adapter proves a disposable existing-file mutation, + exact outcome verification, interrupt handling, rollback, and byte restoration. +- Agent Floor remains CINT Adapter 01 for read-only delegated coding-agent + review. It cannot decide, issue or consume receipts, self-admit, or seal CINT + evidence. + +## Verification + +The release baseline passes 72/72 automated tests, enforces 13/13 runtime +schemas, and preserves 53/53 historical AF-G0 evidence entries. Dependency audit +reports zero vulnerabilities, and the publication audit reports zero findings. +The required `CINT-R0 remote verification` matrix covers Linux, macOS, and +Windows on Node.js 20, 24, and 26. + +## Historical Agent Floor lineage + +The complete Agent Floor lineage remains preserved. Historical tag +`v0.1.0-af-g0` and its GitHub release are unchanged, and its compatibility CLI +remains available as `agent-floor` alongside the primary `cint` CLI. + +## Limitations and non-claims + +This prerelease is a local protocol and proof runtime. It is not a production +authorization service, operating-system enforcement layer, distributed +consensus service, hostile-adapter sandbox, machine-wide integrity mechanism, or +physical autonomous-system controller. It assumes a trusted embedding process, +correct execution-time clock, protected HMAC key custody, and a compatible local +filesystem. Revalidation is event-bound rather than continuous. No npm +publication, production deployment, or third-party operational reliance is +authorized or claimed. + +## Licence + +Code, documentation, schemas, and sanitized fixtures are licensed under the +Apache License 2.0. Trademark rights are not granted by that licence. diff --git a/package-lock.json b/package-lock.json index 210c4ca..96446a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "si1-cint", + "name": "@sntllgnc/cint", "version": "0.1.0-cint-r0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "si1-cint", + "name": "@sntllgnc/cint", "version": "0.1.0-cint-r0", "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 30ed30c..97152f1 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,17 @@ { - "name": "si1-cint", + "name": "@sntllgnc/cint", "version": "0.1.0-cint-r0", - "description": "Machine Counterintelligence Runtime for decision-bound consequential action.", + "description": "SI1 CINT — Machine Counterintelligence Runtime for decision-bound consequential action.", "type": "module", "private": true, + "repository": { + "type": "git", + "url": "git+https://github.com/sntllgnc/CINT.git" + }, + "homepage": "https://github.com/sntllgnc/CINT#readme", + "bugs": { + "url": "https://github.com/sntllgnc/CINT/issues" + }, "bin": { "cint": "./bin/cint.js", "agent-floor": "./bin/agent-floor.js" diff --git a/src/cint/cli.js b/src/cint/cli.js index 4e92100..cfc0ed5 100644 --- a/src/cint/cli.js +++ b/src/cint/cli.js @@ -7,11 +7,11 @@ export const CINT_IDENTITY = Object.freeze({ tagline: "No consequential action without current authority.", mission: "Prevent silent or unauthorized intent from becoming machine action.", invariant: "No consequential action without current decision-bound authority.", - release_state: "REMOTE_R0_REVIEW_CANDIDATE", + release_state: "PUBLIC_R0_SOURCE", public_source_exposure: "YES", - public_default_product: "NO", - public_release: "NO", - remaining_publication_authority: "NONE", + public_default_product: "YES", + public_release: "YES", + remaining_publication_authority: "SOURCE_RELEASE_COMPLETE", adapters: Object.freeze([ "cint.adapter.synthetic-file-patch", "cint.adapter.codex-delegation" diff --git a/tests/cint-cli.test.js b/tests/cint-cli.test.js index 5b1f528..f15513b 100644 --- a/tests/cint-cli.test.js +++ b/tests/cint-cli.test.js @@ -9,11 +9,11 @@ test("CINT CLI reports public review exposure without default-product or release const identity = JSON.parse(output); assert.deepEqual(identity, CINT_IDENTITY); assert.equal(identity.public_display, "SI1 CINT"); - assert.equal(identity.release_state, "REMOTE_R0_REVIEW_CANDIDATE"); + assert.equal(identity.release_state, "PUBLIC_R0_SOURCE"); assert.equal(identity.public_source_exposure, "YES"); - assert.equal(identity.public_default_product, "NO"); - assert.equal(identity.public_release, "NO"); - assert.equal(identity.remaining_publication_authority, "NONE"); + assert.equal(identity.public_default_product, "YES"); + assert.equal(identity.public_release, "YES"); + assert.equal(identity.remaining_publication_authority, "SOURCE_RELEASE_COMPLETE"); }); test("CINT CLI keeps legacy execution behind an explicit command", () => {