diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index b389d65..2bc6220 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -2735,3 +2735,39 @@ test_integrity_note: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` + +## 2026-10-01 — Check-subject context B1 fixture-only provenance boundary + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-check-subject-context-canonical-E-S-A + given: Independently framed captured engine, subject, and adapter observations + when: B1 canonicalizes and parses the bounded UTF-8/LF v1 payload + then: Exact framed bytes and digest bind the three observations without treating product or engine content as framed bytes + - id: SCN-check-subject-context-fixture-authority + given: An issued fixture harness or copied, foreign, expired, and reused variants + when: Fixture context admission or inspection is requested + then: Only the issued single-use WeakMap authority succeeds, and its result remains nonpromotable, non-native, and receipt-ineligible + - id: SCN-check-subject-context-schema-and-native-refusal + given: Readable schema-2 legacy observations and no registered parent issuer + when: A required-schema-3/current-schema-2 or otherwise registered v1 admission is requested + then: The incompatibility or missing-composition refusal occurs with no ControlPlane, database, or write path + observable_outcomes: + - Exact public namespace allowlists and independent E/S/A preimage bytes + - Refusal of selectors before parsing, V2-shaped fields, parent/native/loaded-engine claims, issued-object clones, drift, and repository-head relabeling + - Fixture-only classification with no native candidate or receipt-like promotion surface + - "V2 dependency-manifest, compatibility-declaration, snapshot-policy, and frozen-input-manifest bindings remain reserved for a separately reviewed version rather than being reinterpreted as V1" + determinism_controls: + - Fixed captured observations, independently assembled UTF-8/LF frames, private WeakMap authorities, and no network, provider, database, ControlPlane, retry, or sleep + anti_cheat_rationale: + prevents: + - Minting registered authority through caller JSON, roots, callbacks, readers, verifiers, or selectors + - Reinterpreting V1 fixture observations as a loaded engine, native parent binding, or V2 snapshot/dependency attestation + - Promoting an isolated fixture result into a native candidate or PASS receipt + - Inferring actual product, engine, or adapter bytes from repository HEAD or a mutable active pointer + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/src/kernel/check-subject-context.mjs b/src/kernel/check-subject-context.mjs new file mode 100644 index 0000000..184dd8c --- /dev/null +++ b/src/kernel/check-subject-context.mjs @@ -0,0 +1,188 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { TorchError } from './errors.mjs'; +import { + canonicalManagedSubjectMetadataBytesV1, + parseManagedSubjectMetadataV1, +} from './managed-subject-metadata.mjs'; + +export const CHECK_SUBJECT_CONTEXT_PROTOCOL_VERSION = 1; + +const harnesses = new WeakMap(); +const contexts = new WeakMap(); +const MUTATIONS = new Set([ + 'engine-observation-drift', 'adapter-observation-drift', 'stale-subject-observation', + 'dirty-subject-claim', 'unknown-field', 'mixed-field', 'head-relabel', +]); + +function fail(message, code, details) { + throw new TorchError(message, { code, details }); +} + +function exactObject(value, allowed, label) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + fail(`${label} must be an object`, 'CHECK_SUBJECT_CONTEXT_INPUT_INVALID'); + } + for (const key of Object.keys(value)) { + if (!allowed.has(key)) fail(`${label} contains an unsupported field`, 'CHECK_SUBJECT_CONTEXT_INPUT_INVALID', { field: key }); + } +} + +function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex'); +} + +function frame(label, value) { + const bytes = Buffer.from(value, 'utf8'); + return Buffer.concat([Buffer.from(`${label}:${bytes.length}\n`, 'ascii'), bytes, Buffer.from('\n', 'ascii')]); +} + +function fixtureObservations() { + return { + schema: 'torch.dev/managed-subject-metadata/v1alpha1', + protocolVersion: 1, + engine: { moduleId: 'candidate-engine', interfaceVersion: 1, byteLength: 4096, sha256: '1'.repeat(64) }, + subject: { + projectId: 'fixture-project', productCommit: '2'.repeat(40), cleanTreeDigest: '3'.repeat(64), + storageSchemaVersion: 2, storageSchemaDigest: '4'.repeat(64), + }, + adapter: { + moduleId: 'fixture-adapter', interfaceVersion: 1, byteLength: 2048, sha256: '5'.repeat(64), + controlPlaneSchemaVersion: 2, compatibleStorageSchemaVersion: 2, compatibleStorageSchemaDigest: '4'.repeat(64), + }, + }; +} + +function assertHarness(harness) { + const state = harnesses.get(harness); + if (!state) fail('Fixture authority is not issued by this module', 'CHECK_SUBJECT_CONTEXT_FIXTURE_AUTH_REQUIRED'); + if (state.closed) fail('Fixture authority has expired', 'CHECK_SUBJECT_CONTEXT_FIXTURE_AUTH_EXPIRED'); + return state; +} + +function assertContext(context) { + const state = contexts.get(context); + if (!state) fail('Fixture context is not issued by this module', 'CHECK_SUBJECT_CONTEXT_FIXTURE_AUTH_REQUIRED'); + if (state.closed) fail('Fixture context has expired', 'CHECK_SUBJECT_CONTEXT_FIXTURE_AUTH_EXPIRED'); + return state; +} + +function mutate(state, kind) { + const metadata = parseManagedSubjectMetadataV1(state.bytes); + if (kind === 'engine-observation-drift') return canonicalManagedSubjectMetadataBytesV1({ + ...metadata, engine: { ...metadata.engine, sha256: '6'.repeat(64) }, + }); + if (kind === 'adapter-observation-drift') return canonicalManagedSubjectMetadataBytesV1({ + ...metadata, adapter: { ...metadata.adapter, sha256: '7'.repeat(64) }, + }); + if (kind === 'stale-subject-observation') return canonicalManagedSubjectMetadataBytesV1({ + ...metadata, subject: { ...metadata.subject, productCommit: '8'.repeat(40) }, + }); + if (kind === 'dirty-subject-claim') return Buffer.concat([state.bytes, frame('S-subject-state', 'dirty')]); + if (kind === 'unknown-field') return Buffer.concat([state.bytes, frame('unexpected-field', 'value')]); + if (kind === 'mixed-field') return Buffer.concat([state.bytes, frame('S-snapshot-policy-digest', '9'.repeat(64))]); + if (kind === 'head-relabel') return Buffer.concat([state.bytes, frame('repository-head', 'a'.repeat(40))]); + return canonicalManagedSubjectMetadataBytesV1(metadata); +} + +/** Same E/S/A bytes as the data-only metadata module; the digest never implies a loaded or native engine. */ +export function canonicalCheckSubjectEsaBytesV1(value) { + return canonicalManagedSubjectMetadataBytesV1(value); +} + +export function checkSubjectEsaDigestV1(value) { + const bytes = Buffer.isBuffer(value) || value instanceof Uint8Array + ? Buffer.from(value) + : canonicalCheckSubjectEsaBytesV1(value); + parseManagedSubjectMetadataV1(bytes); + return sha256(bytes); +} + +/** Issues an opaque, fixture-only authority. Callers may supply captured bytes, never roots, readers, or verifiers. */ +export function createCheckSubjectFixtureHarnessV1(input = {}) { + exactObject(input, new Set(['metadataBytes']), 'Fixture harness input'); + const candidateBytes = input.metadataBytes === undefined + ? canonicalManagedSubjectMetadataBytesV1(fixtureObservations()) + : input.metadataBytes; + parseManagedSubjectMetadataV1(candidateBytes); + const bytes = Buffer.from(candidateBytes); + const harness = Object.freeze({ + schema: 'torch.dev/check-subject-fixture-authority/v1alpha1', fixtureId: `fixture-${randomUUID()}`, + }); + harnesses.set(harness, { bytes, digest: sha256(bytes), used: false, closed: false }); + return harness; +} + +/** Enumerated test-only mutation hook; it cannot target registered metadata or expose a handle. */ +export function applyCheckSubjectFixtureMutationV1(input = {}) { + exactObject(input, new Set(['harness', 'kind']), 'Fixture mutation input'); + const state = assertHarness(input.harness); + if (state.used || !MUTATIONS.has(input.kind)) { + fail('Fixture mutation is unsupported', 'CHECK_SUBJECT_CONTEXT_INPUT_INVALID', { kind: input.kind }); + } + state.bytes = mutate(state, input.kind); + return Object.freeze({ applied: input.kind }); +} + +/** Admits only an issued single-use fixture and always marks its result untrusted and nonpromotable. */ +export function admitFixtureCheckSubjectContextV1(input = {}) { + exactObject(input, new Set(['harness']), 'Fixture admission input'); + const state = assertHarness(input.harness); + if (state.used) fail('Fixture authority is single-use', 'CHECK_SUBJECT_CONTEXT_FIXTURE_AUTH_REUSED'); + const metadata = parseManagedSubjectMetadataV1(state.bytes); + if (sha256(state.bytes) !== state.digest) { + fail('Fixture observations drifted after issue', 'CHECK_SUBJECT_CONTEXT_FIXTURE_DRIFT'); + } + state.used = true; + const context = Object.freeze({ + schema: 'torch.dev/check-subject-context/v1alpha1', + classification: 'fixture-untrusted', + promotion: 'nonpromotable', + nativeEligible: false, + receiptEligible: false, + esaDigest: state.digest, + metadata, + }); + contexts.set(context, state); + return context; +} + +export function inspectFixtureCheckSubjectContextV1(input = {}) { + exactObject(input, new Set(['context']), 'Fixture inspection input'); + const state = assertContext(input.context); + return Object.freeze({ + schema: input.context.schema, + classification: 'fixture-untrusted', + promotion: 'nonpromotable', + nativeEligible: false, + receiptEligible: false, + esaDigest: state.digest, + metadata: input.context.metadata, + }); +} + +export function closeCheckSubjectFixtureHarnessV1(input = {}) { + exactObject(input, new Set(['harness']), 'Fixture close input'); + const state = assertHarness(input.harness); + state.closed = true; + return Object.freeze({ closed: true }); +} + +/** + * B1 deliberately has no reachable registered parent issuer. It may parse untrusted bytes for a + * compatibility refusal, but it never opens a control plane, reads a database, or returns authority. + */ +export function admitRegisteredCheckSubjectContextV1(input = {}) { + exactObject(input, new Set(['metadataBytes', 'requiredStorageSchemaVersion', 'currentStorageSchemaVersion']), 'Registered admission input'); + if (input.metadataBytes !== undefined) parseManagedSubjectMetadataV1(input.metadataBytes); + const required = input.requiredStorageSchemaVersion; + const current = input.currentStorageSchemaVersion; + if (required !== undefined || current !== undefined) { + if (!Number.isSafeInteger(required) || required < 1 || !Number.isSafeInteger(current) || current < 1) { + fail('Registered schema observations are invalid', 'CHECK_SUBJECT_CONTEXT_INPUT_INVALID'); + } + if (required !== current) { + fail('Registered schema is incompatible with the required schema', 'CHECK_SUBJECT_CONTEXT_SCHEMA_INCOMPATIBLE', { required, current }); + } + } + fail('No registered parent composition is available in B1', 'CHECK_SUBJECT_CONTEXT_MISSING_COMPOSITION'); +} diff --git a/src/kernel/managed-subject-metadata.mjs b/src/kernel/managed-subject-metadata.mjs new file mode 100644 index 0000000..6f098d9 --- /dev/null +++ b/src/kernel/managed-subject-metadata.mjs @@ -0,0 +1,208 @@ +import { TorchError } from './errors.mjs'; + +export const MANAGED_SUBJECT_METADATA_PROTOCOL_VERSION = 1; +export const MANAGED_SUBJECT_METADATA_MAX_FRAME_BYTES = 1024 * 1024; + +const SCHEMA = 'torch.dev/managed-subject-metadata/v1alpha1'; +const SHA256 = /^[0-9a-f]{64}$/; +const COMMIT = /^[0-9a-f]{40}$/; +const IDENTIFIER = /^[a-z0-9][a-z0-9._/-]{0,127}$/; +const FRAME_LABELS = Object.freeze([ + 'schema', 'protocol-version', + 'E-module-id', 'E-interface-version', 'E-byte-length', 'E-sha256', + 'S-project-id', 'S-product-commit', 'S-clean-tree-digest', 'S-storage-schema-version', 'S-storage-schema-digest', + 'A-module-id', 'A-interface-version', 'A-byte-length', 'A-sha256', + 'A-control-plane-schema-version', 'A-compatible-storage-schema-version', 'A-compatible-storage-schema-digest', +]); + +function fail(message, code, details) { + throw new TorchError(message, { code, details }); +} + +function exactObject(value, allowed, label) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + fail(`${label} must be an object`, 'MANAGED_SUBJECT_METADATA_INVALID'); + } + for (const key of Object.keys(value)) { + if (!allowed.has(key)) fail(`${label} contains an unsupported field`, 'MANAGED_SUBJECT_METADATA_INVALID', { field: key }); + } +} + +function boundedString(value, field, pattern = null) { + if (typeof value !== 'string' || !value || Buffer.byteLength(value, 'utf8') > 256 || (pattern && !pattern.test(value))) { + fail(`${field} is invalid`, 'MANAGED_SUBJECT_METADATA_INVALID', { field }); + } + return value; +} + +function positiveInteger(value, field) { + if (!Number.isSafeInteger(value) || value < 1) { + fail(`${field} must be a positive safe integer`, 'MANAGED_SUBJECT_METADATA_INVALID', { field }); + } + return value; +} + +function byteLength(value, field) { + if (!Number.isSafeInteger(value) || value < 0) { + fail(`${field} must be a non-negative safe integer`, 'MANAGED_SUBJECT_METADATA_INVALID', { field }); + } + return value; +} + +function freezeMetadata(metadata) { + return Object.freeze({ + schema: metadata.schema, + protocolVersion: metadata.protocolVersion, + engine: Object.freeze({ ...metadata.engine }), + subject: Object.freeze({ ...metadata.subject }), + adapter: Object.freeze({ ...metadata.adapter }), + }); +} + +function frame(label, value) { + const bytes = Buffer.from(value, 'utf8'); + return Buffer.concat([Buffer.from(`${label}:${bytes.length}\n`, 'ascii'), bytes, Buffer.from('\n', 'ascii')]); +} + +function decodeUtf8(bytes, field) { + try { + return new TextDecoder('utf-8', { fatal: true }).decode(bytes); + } catch { + fail(`${field} is not valid UTF-8`, 'MANAGED_SUBJECT_METADATA_INVALID', { field }); + } +} + +function capturedBytes(value) { + if (!(Buffer.isBuffer(value) || value instanceof Uint8Array)) { + fail('Metadata must be captured bytes', 'MANAGED_SUBJECT_METADATA_INVALID'); + } + const bytes = Buffer.from(value); + if (!bytes.length || bytes.length > MANAGED_SUBJECT_METADATA_MAX_FRAME_BYTES) { + fail('Metadata frame exceeds the v1 bound', 'MANAGED_SUBJECT_METADATA_FRAME_TOO_LARGE'); + } + return bytes; +} + +function parseFrames(bytes) { + const fields = new Map(); + let offset = 0; + while (offset < bytes.length) { + const headerEnd = bytes.indexOf(0x0a, offset); + if (headerEnd === -1) fail('Metadata frame header is incomplete', 'MANAGED_SUBJECT_METADATA_INVALID'); + const header = bytes.subarray(offset, headerEnd).toString('ascii'); + const match = /^([A-Za-z0-9-]+):([0-9]+)$/.exec(header); + if (!match || (match[2].length > 1 && match[2].startsWith('0'))) { + fail('Metadata frame header is malformed', 'MANAGED_SUBJECT_METADATA_INVALID'); + } + const [, label, lengthText] = match; + const length = Number(lengthText); + if (!Number.isSafeInteger(length) || length > MANAGED_SUBJECT_METADATA_MAX_FRAME_BYTES) { + fail('Metadata frame length is invalid', 'MANAGED_SUBJECT_METADATA_INVALID'); + } + const valueStart = headerEnd + 1; + const valueEnd = valueStart + length; + if (valueEnd >= bytes.length || bytes[valueEnd] !== 0x0a || fields.has(label)) { + fail('Metadata frame body is malformed', 'MANAGED_SUBJECT_METADATA_INVALID'); + } + fields.set(label, decodeUtf8(bytes.subarray(valueStart, valueEnd), label)); + offset = valueEnd + 1; + } + if (fields.size !== FRAME_LABELS.length || FRAME_LABELS.some((label) => !fields.has(label))) { + fail('Metadata frame fields are unsupported or incomplete', 'MANAGED_SUBJECT_METADATA_INVALID'); + } + return fields; +} + +/** Validates only captured E/S/A observations. It intentionally has no authority or filesystem inputs. */ +export function validateManagedSubjectMetadataV1(value) { + exactObject(value, new Set(['schema', 'protocolVersion', 'engine', 'subject', 'adapter']), 'Metadata'); + if (value.schema !== SCHEMA || value.protocolVersion !== MANAGED_SUBJECT_METADATA_PROTOCOL_VERSION) { + fail('Metadata protocol is unsupported', 'MANAGED_SUBJECT_METADATA_INVALID'); + } + exactObject(value.engine, new Set(['moduleId', 'interfaceVersion', 'byteLength', 'sha256']), 'Engine observation'); + exactObject(value.subject, new Set(['projectId', 'productCommit', 'cleanTreeDigest', 'storageSchemaVersion', 'storageSchemaDigest']), 'Subject observation'); + exactObject(value.adapter, new Set([ + 'moduleId', 'interfaceVersion', 'byteLength', 'sha256', 'controlPlaneSchemaVersion', + 'compatibleStorageSchemaVersion', 'compatibleStorageSchemaDigest', + ]), 'Adapter observation'); + const metadata = { + schema: SCHEMA, + protocolVersion: MANAGED_SUBJECT_METADATA_PROTOCOL_VERSION, + engine: { + moduleId: boundedString(value.engine.moduleId, 'engine.moduleId', IDENTIFIER), + interfaceVersion: positiveInteger(value.engine.interfaceVersion, 'engine.interfaceVersion'), + byteLength: byteLength(value.engine.byteLength, 'engine.byteLength'), + sha256: boundedString(value.engine.sha256, 'engine.sha256', SHA256), + }, + subject: { + projectId: boundedString(value.subject.projectId, 'subject.projectId', IDENTIFIER), + productCommit: boundedString(value.subject.productCommit, 'subject.productCommit', COMMIT), + cleanTreeDigest: boundedString(value.subject.cleanTreeDigest, 'subject.cleanTreeDigest', SHA256), + storageSchemaVersion: positiveInteger(value.subject.storageSchemaVersion, 'subject.storageSchemaVersion'), + storageSchemaDigest: boundedString(value.subject.storageSchemaDigest, 'subject.storageSchemaDigest', SHA256), + }, + adapter: { + moduleId: boundedString(value.adapter.moduleId, 'adapter.moduleId', IDENTIFIER), + interfaceVersion: positiveInteger(value.adapter.interfaceVersion, 'adapter.interfaceVersion'), + byteLength: byteLength(value.adapter.byteLength, 'adapter.byteLength'), + sha256: boundedString(value.adapter.sha256, 'adapter.sha256', SHA256), + controlPlaneSchemaVersion: positiveInteger(value.adapter.controlPlaneSchemaVersion, 'adapter.controlPlaneSchemaVersion'), + compatibleStorageSchemaVersion: positiveInteger(value.adapter.compatibleStorageSchemaVersion, 'adapter.compatibleStorageSchemaVersion'), + compatibleStorageSchemaDigest: boundedString(value.adapter.compatibleStorageSchemaDigest, 'adapter.compatibleStorageSchemaDigest', SHA256), + }, + }; + return freezeMetadata(metadata); +} + +/** Canonical bounded UTF-8/LF bytes for independent engine, subject, and adapter observations. */ +export function canonicalManagedSubjectMetadataBytesV1(value) { + const metadata = validateManagedSubjectMetadataV1(value); + const fields = [ + ['schema', metadata.schema], ['protocol-version', String(metadata.protocolVersion)], + ['E-module-id', metadata.engine.moduleId], ['E-interface-version', String(metadata.engine.interfaceVersion)], + ['E-byte-length', String(metadata.engine.byteLength)], ['E-sha256', metadata.engine.sha256], + ['S-project-id', metadata.subject.projectId], ['S-product-commit', metadata.subject.productCommit], + ['S-clean-tree-digest', metadata.subject.cleanTreeDigest], ['S-storage-schema-version', String(metadata.subject.storageSchemaVersion)], + ['S-storage-schema-digest', metadata.subject.storageSchemaDigest], + ['A-module-id', metadata.adapter.moduleId], ['A-interface-version', String(metadata.adapter.interfaceVersion)], + ['A-byte-length', String(metadata.adapter.byteLength)], ['A-sha256', metadata.adapter.sha256], + ['A-control-plane-schema-version', String(metadata.adapter.controlPlaneSchemaVersion)], + ['A-compatible-storage-schema-version', String(metadata.adapter.compatibleStorageSchemaVersion)], + ['A-compatible-storage-schema-digest', metadata.adapter.compatibleStorageSchemaDigest], + ]; + const bytes = Buffer.concat(fields.map(([label, field]) => frame(label, field))); + if (bytes.length > MANAGED_SUBJECT_METADATA_MAX_FRAME_BYTES) { + fail('Metadata frame exceeds the v1 bound', 'MANAGED_SUBJECT_METADATA_FRAME_TOO_LARGE'); + } + return bytes; +} + +/** Parses captured bytes only; accepted v1 bytes describe untrusted observations, never native authority. */ +export function parseManagedSubjectMetadataV1(value) { + const bytes = capturedBytes(value); + const fields = parseFrames(bytes); + const metadata = validateManagedSubjectMetadataV1({ + schema: fields.get('schema'), + protocolVersion: Number(fields.get('protocol-version')), + engine: { + moduleId: fields.get('E-module-id'), interfaceVersion: Number(fields.get('E-interface-version')), + byteLength: Number(fields.get('E-byte-length')), sha256: fields.get('E-sha256'), + }, + subject: { + projectId: fields.get('S-project-id'), productCommit: fields.get('S-product-commit'), + cleanTreeDigest: fields.get('S-clean-tree-digest'), storageSchemaVersion: Number(fields.get('S-storage-schema-version')), + storageSchemaDigest: fields.get('S-storage-schema-digest'), + }, + adapter: { + moduleId: fields.get('A-module-id'), interfaceVersion: Number(fields.get('A-interface-version')), + byteLength: Number(fields.get('A-byte-length')), sha256: fields.get('A-sha256'), + controlPlaneSchemaVersion: Number(fields.get('A-control-plane-schema-version')), + compatibleStorageSchemaVersion: Number(fields.get('A-compatible-storage-schema-version')), + compatibleStorageSchemaDigest: fields.get('A-compatible-storage-schema-digest'), + }, + }); + if (!Buffer.from(canonicalManagedSubjectMetadataBytesV1(metadata)).equals(bytes)) { + fail('Metadata bytes are not canonical v1 observations', 'MANAGED_SUBJECT_METADATA_INVALID'); + } + return metadata; +} diff --git a/test/fleet/check-subject-context.test.mjs b/test/fleet/check-subject-context.test.mjs new file mode 100644 index 0000000..0913e9b --- /dev/null +++ b/test/fleet/check-subject-context.test.mjs @@ -0,0 +1,153 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; +import { + MANAGED_SUBJECT_METADATA_MAX_FRAME_BYTES, + MANAGED_SUBJECT_METADATA_PROTOCOL_VERSION, + canonicalManagedSubjectMetadataBytesV1, + parseManagedSubjectMetadataV1, + validateManagedSubjectMetadataV1, +} from '../../src/kernel/managed-subject-metadata.mjs'; +import { + CHECK_SUBJECT_CONTEXT_PROTOCOL_VERSION, + admitFixtureCheckSubjectContextV1, + admitRegisteredCheckSubjectContextV1, + applyCheckSubjectFixtureMutationV1, + canonicalCheckSubjectEsaBytesV1, + checkSubjectEsaDigestV1, + closeCheckSubjectFixtureHarnessV1, + createCheckSubjectFixtureHarnessV1, + inspectFixtureCheckSubjectContextV1, +} from '../../src/kernel/check-subject-context.mjs'; + +const observations = Object.freeze({ + schema: 'torch.dev/managed-subject-metadata/v1alpha1', + protocolVersion: 1, + engine: Object.freeze({ moduleId: 'candidate-engine', interfaceVersion: 1, byteLength: 4096, sha256: '1'.repeat(64) }), + subject: Object.freeze({ + projectId: 'fixture-project', productCommit: '2'.repeat(40), cleanTreeDigest: '3'.repeat(64), + storageSchemaVersion: 2, storageSchemaDigest: '4'.repeat(64), + }), + adapter: Object.freeze({ + moduleId: 'fixture-adapter', interfaceVersion: 1, byteLength: 2048, sha256: '5'.repeat(64), + controlPlaneSchemaVersion: 2, compatibleStorageSchemaVersion: 2, compatibleStorageSchemaDigest: '4'.repeat(64), + }), +}); + +function frame(label, value) { + const bytes = Buffer.from(value, 'utf8'); + return Buffer.from(`${label}:${bytes.length}\n${value}\n`, 'utf8'); +} + +function independentlyFramedBytes(value = observations) { + return Buffer.concat([ + frame('schema', value.schema), frame('protocol-version', String(value.protocolVersion)), + frame('E-module-id', value.engine.moduleId), frame('E-interface-version', String(value.engine.interfaceVersion)), + frame('E-byte-length', String(value.engine.byteLength)), frame('E-sha256', value.engine.sha256), + frame('S-project-id', value.subject.projectId), frame('S-product-commit', value.subject.productCommit), + frame('S-clean-tree-digest', value.subject.cleanTreeDigest), frame('S-storage-schema-version', String(value.subject.storageSchemaVersion)), + frame('S-storage-schema-digest', value.subject.storageSchemaDigest), + frame('A-module-id', value.adapter.moduleId), frame('A-interface-version', String(value.adapter.interfaceVersion)), + frame('A-byte-length', String(value.adapter.byteLength)), frame('A-sha256', value.adapter.sha256), + frame('A-control-plane-schema-version', String(value.adapter.controlPlaneSchemaVersion)), + frame('A-compatible-storage-schema-version', String(value.adapter.compatibleStorageSchemaVersion)), + frame('A-compatible-storage-schema-digest', value.adapter.compatibleStorageSchemaDigest), + ]); +} + +function expectCode(callback, code) { + assert.throws(callback, (error) => error.code === code); +} + +test('SCN-check-subject-context-canonical-E-S-A: independently framed captured observations bind separate engine, subject, and adapter bytes', async () => { + const metadata = await import('../../src/kernel/managed-subject-metadata.mjs'); + const context = await import('../../src/kernel/check-subject-context.mjs'); + const bytes = independentlyFramedBytes(); + assert.equal(MANAGED_SUBJECT_METADATA_PROTOCOL_VERSION, 1); + assert.equal(CHECK_SUBJECT_CONTEXT_PROTOCOL_VERSION, 1); + assert.deepEqual(Object.keys(metadata), [ + 'MANAGED_SUBJECT_METADATA_MAX_FRAME_BYTES', 'MANAGED_SUBJECT_METADATA_PROTOCOL_VERSION', + 'canonicalManagedSubjectMetadataBytesV1', 'parseManagedSubjectMetadataV1', 'validateManagedSubjectMetadataV1', + ]); + assert.deepEqual(Object.keys(context), [ + 'CHECK_SUBJECT_CONTEXT_PROTOCOL_VERSION', 'admitFixtureCheckSubjectContextV1', 'admitRegisteredCheckSubjectContextV1', + 'applyCheckSubjectFixtureMutationV1', 'canonicalCheckSubjectEsaBytesV1', 'checkSubjectEsaDigestV1', + 'closeCheckSubjectFixtureHarnessV1', 'createCheckSubjectFixtureHarnessV1', 'inspectFixtureCheckSubjectContextV1', + ]); + assert.deepEqual(canonicalManagedSubjectMetadataBytesV1(observations), bytes); + assert.deepEqual(canonicalCheckSubjectEsaBytesV1(observations), bytes); + assert.equal(checkSubjectEsaDigestV1(bytes), createHash('sha256').update(bytes).digest('hex')); + assert.deepEqual(parseManagedSubjectMetadataV1(bytes), validateManagedSubjectMetadataV1(observations)); +}); + +test('SCN-check-subject-context-v1-refusal: parent, native, loaded-engine, V2, selectors, and oversized bytes cannot reinterpret fixture observations', () => { + const bytes = independentlyFramedBytes(); + expectCode(() => canonicalManagedSubjectMetadataBytesV1({ ...observations, parentBinding: 'forged' }), 'MANAGED_SUBJECT_METADATA_INVALID'); + expectCode(() => canonicalManagedSubjectMetadataBytesV1({ ...observations, engine: { ...observations.engine, loadedEngine: true } }), 'MANAGED_SUBJECT_METADATA_INVALID'); + expectCode(() => parseManagedSubjectMetadataV1(Buffer.concat([bytes, frame('S-snapshot-policy-digest', '9'.repeat(64))])), 'MANAGED_SUBJECT_METADATA_INVALID'); + expectCode(() => parseManagedSubjectMetadataV1(Buffer.concat([bytes, frame('native-eligible', 'true')])), 'MANAGED_SUBJECT_METADATA_INVALID'); + expectCode(() => parseManagedSubjectMetadataV1(Buffer.alloc(MANAGED_SUBJECT_METADATA_MAX_FRAME_BYTES + 1)), 'MANAGED_SUBJECT_METADATA_FRAME_TOO_LARGE'); + + let bytesRead = false; + const selectorBeforeParse = { root: '/tmp/not-authority', get metadataBytes() { bytesRead = true; return bytes; } }; + expectCode(() => createCheckSubjectFixtureHarnessV1(selectorBeforeParse), 'CHECK_SUBJECT_CONTEXT_INPUT_INVALID'); + assert.equal(bytesRead, false, 'public selectors must fail before metadata parsing'); + for (const selector of ['root', 'database', 'ddl', 'manifest', 'metadataReader', 'engineVerifier', 'adapterSelector', 'path']) { + let selectorBytesRead = false; + const input = { [selector]: 'forged', get metadataBytes() { selectorBytesRead = true; return bytes; } }; + expectCode(() => admitRegisteredCheckSubjectContextV1(input), 'CHECK_SUBJECT_CONTEXT_INPUT_INVALID'); + assert.equal(selectorBytesRead, false, `${selector} must fail before parsing`); + } +}); + +test('SCN-check-subject-context-fixture-authority: issued-only, single-use fixture admission is clone-, expiry-, and promotion-resistant', () => { + const harness = createCheckSubjectFixtureHarnessV1({ metadataBytes: independentlyFramedBytes() }); + const copiedHarness = Object.freeze({ ...harness }); + expectCode(() => admitFixtureCheckSubjectContextV1({ harness: copiedHarness }), 'CHECK_SUBJECT_CONTEXT_FIXTURE_AUTH_REQUIRED'); + const context = admitFixtureCheckSubjectContextV1({ harness }); + assert.deepEqual({ + classification: context.classification, promotion: context.promotion, + nativeEligible: context.nativeEligible, receiptEligible: context.receiptEligible, + }, { classification: 'fixture-untrusted', promotion: 'nonpromotable', nativeEligible: false, receiptEligible: false }); + assert.equal(Object.hasOwn(context, 'receipt'), false); + expectCode(() => admitFixtureCheckSubjectContextV1({ harness }), 'CHECK_SUBJECT_CONTEXT_FIXTURE_AUTH_REUSED'); + expectCode(() => inspectFixtureCheckSubjectContextV1({ context: Object.freeze({ ...context }) }), 'CHECK_SUBJECT_CONTEXT_FIXTURE_AUTH_REQUIRED'); + expectCode(() => inspectFixtureCheckSubjectContextV1({ context: { schema: context.schema } }), 'CHECK_SUBJECT_CONTEXT_FIXTURE_AUTH_REQUIRED'); + assert.deepEqual(inspectFixtureCheckSubjectContextV1({ context }), context); + closeCheckSubjectFixtureHarnessV1({ harness }); + expectCode(() => inspectFixtureCheckSubjectContextV1({ context }), 'CHECK_SUBJECT_CONTEXT_FIXTURE_AUTH_EXPIRED'); +}); + +test('SCN-check-subject-context-mutation-resistance: independently observed E/S/A drift and noncanonical claims fail closed', () => { + for (const [kind, code] of [ + ['engine-observation-drift', 'CHECK_SUBJECT_CONTEXT_FIXTURE_DRIFT'], + ['adapter-observation-drift', 'CHECK_SUBJECT_CONTEXT_FIXTURE_DRIFT'], + ['stale-subject-observation', 'CHECK_SUBJECT_CONTEXT_FIXTURE_DRIFT'], + ['dirty-subject-claim', 'MANAGED_SUBJECT_METADATA_INVALID'], + ['unknown-field', 'MANAGED_SUBJECT_METADATA_INVALID'], + ['mixed-field', 'MANAGED_SUBJECT_METADATA_INVALID'], + ['head-relabel', 'MANAGED_SUBJECT_METADATA_INVALID'], + ]) { + const harness = createCheckSubjectFixtureHarnessV1({ metadataBytes: independentlyFramedBytes() }); + applyCheckSubjectFixtureMutationV1({ harness, kind }); + expectCode(() => admitFixtureCheckSubjectContextV1({ harness }), code); + closeCheckSubjectFixtureHarnessV1({ harness }); + } +}); + +test('SCN-check-subject-context-schema-and-native-refusal: schema2 is readable legacy evidence, current2-required3 and all registered v1 admission refuse without state access', () => { + const bytes = independentlyFramedBytes(); + assert.equal(parseManagedSubjectMetadataV1(bytes).subject.storageSchemaVersion, 2); + expectCode(() => admitRegisteredCheckSubjectContextV1({ + metadataBytes: bytes, requiredStorageSchemaVersion: 3, currentStorageSchemaVersion: 2, + }), 'CHECK_SUBJECT_CONTEXT_SCHEMA_INCOMPATIBLE'); + expectCode(() => admitRegisteredCheckSubjectContextV1({ + metadataBytes: bytes, requiredStorageSchemaVersion: 2, currentStorageSchemaVersion: 2, + }), 'CHECK_SUBJECT_CONTEXT_MISSING_COMPOSITION'); + expectCode(() => admitRegisteredCheckSubjectContextV1(), 'CHECK_SUBJECT_CONTEXT_MISSING_COMPOSITION'); + const source = `${readFileSync(new URL('../../src/kernel/managed-subject-metadata.mjs', import.meta.url), 'utf8')}\n${readFileSync(new URL('../../src/kernel/check-subject-context.mjs', import.meta.url), 'utf8')}`; + for (const forbidden of ['ControlPlane', 'openControlPlane', 'DatabaseSync', "node:sqlite", '.prepare(']) { + assert.equal(source.includes(forbidden), false, `B1 data-only modules must not invoke ${forbidden}`); + } +});