From 7a8bfa8314d955ea4ad41c478820158103123573 Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Fri, 7 Aug 2026 12:36:27 +0200 Subject: [PATCH 1/4] feat(storage): centralize system-record runtime authority --- packages/storage/src/adapters/sparql-http.ts | 19 +-- ...stem-record-lane-activation-v1-internal.ts | 139 ++++++++++++++++++ .../src/system-record-materializer-v1.ts | 107 +++----------- ...record-verified-replacement-v1-internal.ts | 120 +++++++++++++-- ...tem-record-capability-discovery-v1.test.ts | 26 +++- ...ord-control-barrier-integration-v1.test.ts | 15 +- ...cord-managed-mutation-admission-v1.test.ts | 11 +- ...d-materialization-epoch-adapter-v1.test.ts | 8 +- ...tem-record-verified-replacement-v1.test.ts | 117 +++++++++++++++ 9 files changed, 439 insertions(+), 123 deletions(-) create mode 100644 packages/storage/src/system-record-lane-activation-v1-internal.ts diff --git a/packages/storage/src/adapters/sparql-http.ts b/packages/storage/src/adapters/sparql-http.ts index 463998c6b..294b0adfb 100644 --- a/packages/storage/src/adapters/sparql-http.ts +++ b/packages/storage/src/adapters/sparql-http.ts @@ -69,7 +69,7 @@ import { type SystemRecordLaneExecutionBindingV1, } from '../system-record-materializer-v1.js'; import { createSystemRecordAtomicApplyExecutorV1 } from '../system-record-atomic-apply-executor-v1-internal.js'; -import { createSystemRecordVerifiedReplacementRegistryV1 } from '../system-record-verified-replacement-v1-internal.js'; +import { resolveOwnedSystemRecordVerifiedReplacementRuntimeV1 } from '../system-record-verified-replacement-v1-internal.js'; import { OwnedManagedHttpClient } from './managed-http-client.js'; import { rotateSystemRecordMaterializationEpochV1 } from '../system-record-materialization-epoch-v1-internal.js'; import { UnsupportedTripleStoreCapabilityError } from '../unsupported-capability-error.js'; @@ -587,14 +587,14 @@ export class SparqlHttpStore implements TripleStore { if (this.systemRecordLane === undefined) { try { - // Pair issuer and consumer in one private registry, but retain only the - // consumer at this storage boundary. The issuer is intentionally not a - // store property, option, facade member, or export. B3 deliberately - // discards it, leaving this production lane default-unused; the later - // structured-verifier stack must move registry creation to its private - // composition closure and hand this boundary the SAME consumer. Until - // then every caller-authored object fails before inspection/mutation. - const { consumer } = createSystemRecordVerifiedReplacementRegistryV1(); + // Resolve the ownership-lease runtime and retain only its consumer at + // the storage boundary. Every adapter for this lease receives the same + // registry, and all authentic leases share one process-wide accountant. + // The issuer remains outside the store and has no production caller in + // this default-unused stack; the later lifecycle verifier captures it. + const { consumer, activationReader } = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + this.ownershipLease, + ); const atomicExecutor = createSystemRecordAtomicApplyExecutorV1({ consumer, storeId: this, @@ -605,6 +605,7 @@ export class SparqlHttpStore implements TripleStore { const owner = createSystemRecordLaneControllerV1({ lease: this.ownershipLease, handoff: this.buildChildHandoff(this.supervisorHandoff), + activationReader, executor: { applyVerified: (proof, childGeneration) => this.executeSystemRecordApplyLegacy(proof, childGeneration), diff --git a/packages/storage/src/system-record-lane-activation-v1-internal.ts b/packages/storage/src/system-record-lane-activation-v1-internal.ts new file mode 100644 index 000000000..9129e8a5d --- /dev/null +++ b/packages/storage/src/system-record-lane-activation-v1-internal.ts @@ -0,0 +1,139 @@ +import { types as utilTypes } from 'node:util'; + +declare const SYSTEM_RECORD_LANE_ACTIVATION_BRAND: unique symbol; + +/** Empty process-local authority. Its descriptor exists only in the private table. */ +export type OwnedSystemRecordLaneActivationV1 = { + readonly [SYSTEM_RECORD_LANE_ACTIVATION_BRAND]: 'owned-system-record-lane-activation-v1'; +}; + +export interface SystemRecordLaneActivationDescriptorV1 { + readonly networkId: string; + readonly kinds: readonly ['agents']; + readonly mode: 'shadow' | 'authoritative'; +} + +export interface SystemRecordLaneActivationIssuerV1 { + issue(descriptor: SystemRecordLaneActivationDescriptorV1): OwnedSystemRecordLaneActivationV1; +} + +export interface SystemRecordLaneActivationReaderV1 { + read(activation: unknown): SystemRecordLaneActivationDescriptorV1; +} + +export interface SystemRecordLaneActivationRegistryV1 { + readonly issuer: SystemRecordLaneActivationIssuerV1; + readonly reader: SystemRecordLaneActivationReaderV1; +} + +interface RegisteredActivationV1 { + readonly registryIdentity: object; + readonly descriptor: SystemRecordLaneActivationDescriptorV1; +} + +const REGISTERED_ACTIVATIONS = new WeakMap(); +const NETWORK_ID_PATTERN_V1 = /^[A-Za-z0-9._:-]+$/; +const MAX_NETWORK_ID_BYTES_V1 = 128; +const UTF8 = new TextEncoder(); + +/** Snapshot the closed activation record without invoking caller accessors or iterators. */ +export function snapshotSystemRecordLaneActivationDescriptorV1( + activation: unknown, +): SystemRecordLaneActivationDescriptorV1 { + if ( + activation === null || + typeof activation !== 'object' || + Array.isArray(activation) || + utilTypes.isProxy(activation) || + ![Object.prototype, null].includes(Object.getPrototypeOf(activation)) + ) { + throw new Error('system-record lane activation must be a plain data object'); + } + + const expected = ['kinds', 'mode', 'networkId']; + const ownKeys = Reflect.ownKeys(activation); + if ( + ownKeys.length !== expected.length || + ownKeys.some((key) => typeof key !== 'string') || + [...(ownKeys as string[])].sort().some((key, index) => key !== expected[index]) + ) { + throw new Error('system-record lane activation has unknown or missing fields'); + } + + const readDataField = (key: string): unknown => { + const field = Object.getOwnPropertyDescriptor(activation, key); + if (!field?.enumerable || !Object.prototype.hasOwnProperty.call(field, 'value')) { + throw new Error('system-record lane activation fields must be enumerable data properties'); + } + return field.value; + }; + + const networkId = readDataField('networkId'); + if ( + typeof networkId !== 'string' || + networkId.length === 0 || + UTF8.encode(networkId).byteLength > MAX_NETWORK_ID_BYTES_V1 || + !NETWORK_ID_PATTERN_V1.test(networkId) + ) { + throw new Error('system-record lane activation networkId is not canonical'); + } + + const kinds = readDataField('kinds'); + if (!Array.isArray(kinds) || utilTypes.isProxy(kinds)) { + throw new Error('system-record lane activation kinds must be the closed [agents] tuple'); + } + const kindKeys = Reflect.ownKeys(kinds); + const length = Object.getOwnPropertyDescriptor(kinds, 'length'); + const first = Object.getOwnPropertyDescriptor(kinds, '0'); + if ( + kindKeys.length !== 2 || + !kindKeys.includes('length') || + !kindKeys.includes('0') || + length?.value !== 1 || + length.enumerable || + !first?.enumerable || + !Object.prototype.hasOwnProperty.call(first, 'value') || + first.value !== 'agents' + ) { + throw new Error('system-record lane activation kinds must be the closed [agents] tuple'); + } + + const mode = readDataField('mode'); + if (mode !== 'shadow' && mode !== 'authoritative') { + throw new Error('system-record lane activation mode is invalid'); + } + + return Object.freeze({ networkId, kinds: Object.freeze(['agents'] as const), mode }); +} + +/** + * Create one non-interchangeable issuer/reader pair. Production captures the reader in + * the controller and the issuer in the later lifecycle owner; neither belongs in config. + */ +export function createSystemRecordLaneActivationRegistryV1( + assertAvailable?: () => void, +): SystemRecordLaneActivationRegistryV1 { + const registryIdentity = Object.freeze(Object.create(null) as object); + const issuer: SystemRecordLaneActivationIssuerV1 = Object.freeze({ + issue(value: SystemRecordLaneActivationDescriptorV1): OwnedSystemRecordLaneActivationV1 { + assertAvailable?.(); + const descriptor = snapshotSystemRecordLaneActivationDescriptorV1(value); + const handle = Object.freeze(Object.create(null) as object) as OwnedSystemRecordLaneActivationV1; + REGISTERED_ACTIVATIONS.set(handle, { registryIdentity, descriptor }); + return handle; + }, + }); + const reader: SystemRecordLaneActivationReaderV1 = Object.freeze({ + read(activation: unknown): SystemRecordLaneActivationDescriptorV1 { + if (activation === null || typeof activation !== 'object') { + throw new Error('system-record lane activation capability is invalid'); + } + const registered = REGISTERED_ACTIVATIONS.get(activation); + if (registered?.registryIdentity !== registryIdentity) { + throw new Error('system-record lane activation capability is invalid or belongs to another runtime'); + } + return registered.descriptor; + }, + }); + return Object.freeze({ issuer, reader }); +} diff --git a/packages/storage/src/system-record-materializer-v1.ts b/packages/storage/src/system-record-materializer-v1.ts index 4d565600d..a0b37a9eb 100644 --- a/packages/storage/src/system-record-materializer-v1.ts +++ b/packages/storage/src/system-record-materializer-v1.ts @@ -9,6 +9,11 @@ import type { SystemRecordAtomicRecoveryResolutionV1, SystemRecordAtomicRecoveryRuntimeV1, } from './system-record-atomic-apply-executor-v1-internal.js'; +import { + snapshotSystemRecordLaneActivationDescriptorV1, + type OwnedSystemRecordLaneActivationV1, + type SystemRecordLaneActivationReaderV1, +} from './system-record-lane-activation-v1-internal.js'; /** * System-record V1 lane controller (#2052 Stack B2). @@ -30,21 +35,8 @@ import type { * Public contract * ------------------------------------------------------------------ */ -/** - * Activation descriptor. Carries no authority of its own: it names WHICH - * `(network, kind)` set to enable, while the right to enable anything at all - * comes from the ownership lease captured at controller construction. - */ -export interface SystemRecordLaneActivationV1 { - readonly networkId: string; - /** V1 accepts only the fixed `agents` kind; `ontology` is Stack E. */ - readonly kinds: readonly ['agents']; - /** - * Pre-activation shadow mode keeps the legacy lane authoritative: V1 rows are - * materialized and charged, but legacy RDF is never deleted. - */ - readonly mode: 'shadow' | 'authoritative'; -} +/** Lifecycle-issued process-local authority; no public descriptor can mint it. */ +export type SystemRecordLaneActivationV1 = OwnedSystemRecordLaneActivationV1; export type SystemRecordApplyOutcomeV1 = | { readonly outcome: 'applied'; readonly stateRevision: string; readonly appliedStateDigest: string } @@ -205,6 +197,8 @@ export interface SystemRecordLaneControllerDepsV1 { readonly lease: ManagedOxigraphOwnershipLeaseV1; readonly handoff: SystemRecordChildHandoffV1; readonly executor: SystemRecordTransactionExecutorV1; + /** Production reader for the opaque lifecycle-issued activation capability. */ + readonly activationReader?: SystemRecordLaneActivationReaderV1; /** * Required, not optional. An optional barrier is one that gets forgotten: * this capability shipped once with a barrier implemented, exported and @@ -253,77 +247,6 @@ interface SystemRecordLaneActivationSnapshotV1 { readonly mode: 'shadow' | 'authoritative'; } -const NETWORK_ID_PATTERN_V1 = /^[A-Za-z0-9._:-]+$/; -const MAX_NETWORK_ID_BYTES_V1 = 128; -const UTF8 = new TextEncoder(); - -/** Snapshot the closed activation record without invoking caller accessors or iterators. */ -const snapshotActivation = (activation: unknown): SystemRecordLaneActivationSnapshotV1 => { - if ( - activation === null || - typeof activation !== 'object' || - Array.isArray(activation) || - ![Object.prototype, null].includes(Object.getPrototypeOf(activation)) - ) { - throw new Error('system-record lane activation must be a plain data object'); - } - - const expected = ['kinds', 'mode', 'networkId']; - const ownKeys = Reflect.ownKeys(activation); - if ( - ownKeys.length !== expected.length || - ownKeys.some((key) => typeof key !== 'string') || - [...(ownKeys as string[])].sort().some((key, index) => key !== expected[index]) - ) { - throw new Error('system-record lane activation has unknown or missing fields'); - } - - const readDataField = (key: string): unknown => { - const field = Object.getOwnPropertyDescriptor(activation, key); - if (!field?.enumerable || !Object.prototype.hasOwnProperty.call(field, 'value')) { - throw new Error('system-record lane activation fields must be enumerable data properties'); - } - return field.value; - }; - - const networkId = readDataField('networkId'); - if ( - typeof networkId !== 'string' || - networkId.length === 0 || - UTF8.encode(networkId).byteLength > MAX_NETWORK_ID_BYTES_V1 || - !NETWORK_ID_PATTERN_V1.test(networkId) - ) { - throw new Error('system-record lane activation networkId is not canonical'); - } - - const kinds = readDataField('kinds'); - if (!Array.isArray(kinds)) { - throw new Error('system-record lane activation kinds must be the closed [agents] tuple'); - } - const kindKeys = Reflect.ownKeys(kinds); - const length = Object.getOwnPropertyDescriptor(kinds, 'length'); - const first = Object.getOwnPropertyDescriptor(kinds, '0'); - if ( - kindKeys.length !== 2 || - !kindKeys.includes('length') || - !kindKeys.includes('0') || - length?.value !== 1 || - length.enumerable || - !first?.enumerable || - !Object.prototype.hasOwnProperty.call(first, 'value') || - first.value !== 'agents' - ) { - throw new Error('system-record lane activation kinds must be the closed [agents] tuple'); - } - - const mode = readDataField('mode'); - if (mode !== 'shadow' && mode !== 'authoritative') { - throw new Error('system-record lane activation mode is invalid'); - } - - return Object.freeze({ networkId, kind: 'agents', mode }); -}; - const descriptorOf = (activation: SystemRecordLaneActivationSnapshotV1): string => `${activation.networkId}|${activation.kind}|${activation.mode}`; @@ -562,8 +485,16 @@ class SystemRecordLaneSession { /* -------------------------------------------------------------- */ - async open(activation: SystemRecordLaneActivationV1): Promise { - const activationSnapshot = snapshotActivation(activation); + async open(activation: unknown): Promise { + const descriptor = this.deps.activationReader + ? this.deps.activationReader.read(activation) + : activation; + const parsed = snapshotSystemRecordLaneActivationDescriptorV1(descriptor); + const activationSnapshot: SystemRecordLaneActivationSnapshotV1 = Object.freeze({ + networkId: parsed.networkId, + kind: 'agents', + mode: parsed.mode, + }); const wanted = descriptorOf(activationSnapshot); this.assertNotTerminal(); diff --git a/packages/storage/src/system-record-verified-replacement-v1-internal.ts b/packages/storage/src/system-record-verified-replacement-v1-internal.ts index b85c5fae1..ca88ff68e 100644 --- a/packages/storage/src/system-record-verified-replacement-v1-internal.ts +++ b/packages/storage/src/system-record-verified-replacement-v1-internal.ts @@ -35,6 +35,16 @@ import { type OwnedSubjectTableObjectV1, } from '@origintrail-official/dkg-core/system-record-v1'; +import { + isManagedOxigraphOwnershipLeaseV1, + readManagedOxigraphOwnershipSnapshotV1, + type ManagedOxigraphOwnershipLeaseV1, +} from './managed-oxigraph-ownership-v1-internal.js'; +import { + createSystemRecordLaneActivationRegistryV1, + type SystemRecordLaneActivationIssuerV1, + type SystemRecordLaneActivationReaderV1, +} from './system-record-lane-activation-v1-internal.js'; import type { Quad } from './triple-store.js'; declare const VERIFIED_REPLACEMENT_HANDLE_BRAND: unique symbol; @@ -139,6 +149,8 @@ export type SystemRecordAtomicChargeCategoryV1 = export interface SystemRecordVerifiedReplacementRegistryV1 { readonly issuer: SystemRecordVerifiedReplacementIssuerV1; readonly consumer: SystemRecordVerifiedReplacementConsumerV1; + readonly activationIssuer: SystemRecordLaneActivationIssuerV1; + readonly activationReader: SystemRecordLaneActivationReaderV1; } interface RegisteredReplacementV1 { @@ -153,6 +165,7 @@ type RuntimeReservationPhaseV1 = 'proof' | 'facts' | 'recovery' | 'released'; interface RuntimeReservationV1 { readonly registryIdentity: object; + readonly accountantIdentity: object; readonly identity: object; readonly bytes: number; readonly admittedDeadlineMs: number; @@ -161,6 +174,17 @@ interface RuntimeReservationV1 { recoveryOwnership?: object; } +interface SystemRecordRuntimeAccountantV1 { + readonly identity: object; + accountedBytes: number; + liveAtomicReservation: RuntimeReservationV1 | null; +} + +interface SystemRecordVerifiedReplacementRegistryDepsV1 { + readonly accountant: SystemRecordRuntimeAccountantV1; + readonly assertAvailable?: () => void; +} + /** Module-private and non-enumerable by construction. Handle identity is the only lookup key. */ const REGISTERED_REPLACEMENTS = new WeakMap(); const AUTHENTIC_VERIFIED_REPLACEMENT_FACTS = new WeakSet(); @@ -172,6 +196,23 @@ const ATOMIC_CHARGE_CATEGORIES = new Set([ 'prepared', ]); +const createSystemRecordRuntimeAccountantV1 = (): SystemRecordRuntimeAccountantV1 => ({ + identity: Object.freeze(Object.create(null) as object), + accountedBytes: 0, + liveAtomicReservation: null, +}); + +/** + * Production reservations are process-wide, not per adapter or per ownership lease. + * Test-only registries created directly below receive an isolated accountant so suites + * cannot leak mutable process state into one another. + */ +const PRODUCTION_RUNTIME_ACCOUNTANT = createSystemRecordRuntimeAccountantV1(); +const PRODUCTION_REGISTRIES = new WeakMap< + ManagedOxigraphOwnershipLeaseV1, + SystemRecordVerifiedReplacementRegistryV1 +>(); + /** Refuse structural facts even when they embed a separately valid authority capability. */ export function assertAuthenticSystemRecordVerifiedReplacementFactsV1( value: unknown, @@ -563,22 +604,26 @@ function bindingsEqual( * Create one non-interchangeable issuer/consumer pair. Only the consumer half belongs * in the storage executor; only the issuer half belongs in the verifier closure. */ -export function createSystemRecordVerifiedReplacementRegistryV1(): SystemRecordVerifiedReplacementRegistryV1 { +function createSystemRecordVerifiedReplacementRegistryWithDepsV1( + deps: SystemRecordVerifiedReplacementRegistryDepsV1, +): SystemRecordVerifiedReplacementRegistryV1 { const registryIdentity = Object.freeze(Object.create(null) as object); - let accountedBytes = 0; - let liveAtomicReservation: RuntimeReservationV1 | null = null; + const { accountant } = deps; + const activation = createSystemRecordLaneActivationRegistryV1(deps.assertAvailable); const reserveAtomic = ( admittedDeadlineMs: number, decodedBytes: number, ): RuntimeReservationV1 => { - if (liveAtomicReservation !== null - || accountedBytes + SYSTEM_RECORD_MAX_ATOMIC_TRANSIENT_BYTES + deps.assertAvailable?.(); + if (accountant.liveAtomicReservation !== null + || accountant.accountedBytes + SYSTEM_RECORD_MAX_ATOMIC_TRANSIENT_BYTES > SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES) { throw new Error('system-record atomic transient reservation is already live'); } const reservation: RuntimeReservationV1 = { registryIdentity, + accountantIdentity: accountant.identity, identity: Object.freeze(Object.create(null) as object), bytes: SYSTEM_RECORD_MAX_ATOMIC_TRANSIENT_BYTES, admittedDeadlineMs, @@ -590,8 +635,8 @@ export function createSystemRecordVerifiedReplacementRegistryV1(): SystemRecordV }, phase: 'proof', }; - accountedBytes += reservation.bytes; - liveAtomicReservation = reservation; + accountant.accountedBytes += reservation.bytes; + accountant.liveAtomicReservation = reservation; return reservation; }; @@ -599,7 +644,9 @@ export function createSystemRecordVerifiedReplacementRegistryV1(): SystemRecordV if (reservation.registryIdentity !== registryIdentity || reservation.phase === 'released') { throw new Error('system-record atomic transient reservation was already released'); } - if (liveAtomicReservation !== reservation || accountedBytes !== reservation.bytes) { + if (reservation.accountantIdentity !== accountant.identity + || accountant.liveAtomicReservation !== reservation + || accountant.accountedBytes < reservation.bytes) { throw new Error('system-record atomic transient accountant state is inconsistent'); } reservation.phase = 'released'; @@ -608,8 +655,8 @@ export function createSystemRecordVerifiedReplacementRegistryV1(): SystemRecordV reservation.charges.response = 0; reservation.charges.prepared = 0; reservation.recoveryOwnership = undefined; - accountedBytes -= reservation.bytes; - liveAtomicReservation = null; + accountant.accountedBytes -= reservation.bytes; + accountant.liveAtomicReservation = null; }; const registeredHandle = (handle: unknown): RegisteredReplacementV1 => { @@ -872,7 +919,58 @@ export function createSystemRecordVerifiedReplacementRegistryV1(): SystemRecordV }, }); - return Object.freeze({ issuer, consumer }); + return Object.freeze({ + issuer, + consumer, + activationIssuer: activation.issuer, + activationReader: activation.reader, + }); +} + +/** + * Isolated registry for storage-internal tests and pure transaction composition. + * Production code must resolve the ownership-lease runtime below so every managed + * adapter and future lifecycle verifier shares one process-wide accountant. + */ +export function createSystemRecordVerifiedReplacementRegistryV1(): SystemRecordVerifiedReplacementRegistryV1 { + return createSystemRecordVerifiedReplacementRegistryWithDepsV1({ + accountant: createSystemRecordRuntimeAccountantV1(), + }); +} + +/** + * Resolve the single runtime bound to an authentic daemon ownership lease. + * + * A persisted option, copied object, or structural look-alike cannot create a runtime. + * The returned pair is intentionally internal to the package: storage retains only the + * consumer while the later agent lifecycle captures the issuer in its verifier closure. + */ +export function resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + lease: ManagedOxigraphOwnershipLeaseV1, +): SystemRecordVerifiedReplacementRegistryV1 { + if (!isManagedOxigraphOwnershipLeaseV1(lease)) { + throw new Error('system-record runtime requires an authentic managed Oxigraph ownership lease'); + } + const ownership = readManagedOxigraphOwnershipSnapshotV1(lease); + if (ownership?.queryEndpoint === undefined || ownership.updateEndpoint === undefined) { + throw new Error('system-record runtime requires an endpoint-bound managed Oxigraph ownership lease'); + } + const existing = PRODUCTION_REGISTRIES.get(lease); + if (existing !== undefined) return existing; + + const runtime = createSystemRecordVerifiedReplacementRegistryWithDepsV1({ + accountant: PRODUCTION_RUNTIME_ACCOUNTANT, + assertAvailable: () => { + const snapshot = readManagedOxigraphOwnershipSnapshotV1(lease); + if (!snapshot?.ready || snapshot.terminal + || snapshot.queryEndpoint !== ownership.queryEndpoint + || snapshot.updateEndpoint !== ownership.updateEndpoint) { + throw new Error('system-record runtime ownership lease is not ready'); + } + }, + }); + PRODUCTION_REGISTRIES.set(lease, runtime); + return runtime; } function retainedVerifiedFactsBytes( diff --git a/packages/storage/test/system-record-capability-discovery-v1.test.ts b/packages/storage/test/system-record-capability-discovery-v1.test.ts index 32a842e37..c63140de7 100644 --- a/packages/storage/test/system-record-capability-discovery-v1.test.ts +++ b/packages/storage/test/system-record-capability-discovery-v1.test.ts @@ -12,6 +12,7 @@ import { type ManagedOxigraphSupervisorHandoffV1, } from '../src/managed-oxigraph-ownership-v1-internal.js'; import { __resetSystemRecordControllerRegistrationForTests } from '../src/system-record-materializer-v1.js'; +import { resolveOwnedSystemRecordVerifiedReplacementRuntimeV1 } from '../src/system-record-verified-replacement-v1-internal.js'; import { createTripleStore, type TripleStore } from '../src/triple-store.js'; const QUERY_ENDPOINT = 'http://127.0.0.1:1/query'; @@ -95,6 +96,18 @@ describe('system-record V1 capability discovery', () => { await store.close().catch(() => undefined); }); + it('refuses a caller-authored activation descriptor on the production controller', async () => { + const store = await build(managedOptions()); + const controller = store.getSystemRecordLaneControllerV1?.(); + expect(controller).toBeDefined(); + await expect(controller!.open({ + networkId: 'testnet', + kinds: ['agents'], + mode: 'shadow', + })).rejects.toThrow(/activation capability/); + await store.close(); + }); + it.each([ ['wrong query path', UPDATE_ENDPOINT, UPDATE_ENDPOINT, undefined], ['wrong update path', QUERY_ENDPOINT, QUERY_ENDPOINT, undefined], @@ -149,6 +162,13 @@ describe('system-record V1 capability discovery', () => { const firstStore = await build(managedOptions()); const retired = firstStore.getSystemRecordLaneControllerV1?.(); expect(retired).toBeDefined(); + const activation = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + ownership.lease, + ).activationIssuer.issue({ + networkId: 'testnet', + kinds: ['agents'], + mode: 'shadow', + }); await firstStore.close(); // SparqlHttpStore.close rotates a reusable lifecycle generation. The @@ -161,11 +181,7 @@ describe('system-record V1 capability discovery', () => { const replacementStore = await build(managedOptions()); expect(replacementStore.getSystemRecordLaneControllerV1?.()).toBeDefined(); - await expect(retired!.open({ - networkId: 'testnet', - kinds: ['agents'], - mode: 'shadow', - })).rejects.toThrow(/terminal/); + await expect(retired!.open(activation)).rejects.toThrow(/terminal/); await replacementStore.close(); }); diff --git a/packages/storage/test/system-record-control-barrier-integration-v1.test.ts b/packages/storage/test/system-record-control-barrier-integration-v1.test.ts index f9dcf915e..ceecd23d9 100644 --- a/packages/storage/test/system-record-control-barrier-integration-v1.test.ts +++ b/packages/storage/test/system-record-control-barrier-integration-v1.test.ts @@ -15,6 +15,7 @@ import { __resetSystemRecordControllerRegistrationForTests, type SystemRecordLaneActivationV1, } from '../src/system-record-materializer-v1.js'; +import { resolveOwnedSystemRecordVerifiedReplacementRuntimeV1 } from '../src/system-record-verified-replacement-v1-internal.js'; import { createTripleStore, type TripleStore } from '../src/triple-store.js'; let QUERY_ENDPOINT: string; @@ -102,6 +103,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => let supervisor: RecordingSupervisor; let gated: GatedFetch; let store: TripleStore; + let activation: unknown; beforeAll(async () => { managedServer = createServer((req, res) => { @@ -139,6 +141,9 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => __resetSystemRecordControllerRegistrationForTests(); ownership = createManagedOxigraphOwnershipControllerV1(QUERY_ENDPOINT, UPDATE_ENDPOINT); ownership.bindReadyGeneration(); + activation = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + ownership.lease, + ).activationIssuer.issue(ACTIVATION); supervisor = new RecordingSupervisor(); gated = new GatedFetch(); epoch = null; @@ -178,7 +183,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => await gated.firstEntry; expect(gated.entered).toBe(1); - const opening = controller!.open(ACTIVATION); + const opening = controller!.open(activation); await drainTurns(); // THE assertion. Without the barrier this reads ['stop', 'start'] here: the @@ -201,7 +206,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => // Positive control for the timing claim: the wait above is caused by the // in-flight request, not by the barrier being slow or the open being async. const controller = store.getSystemRecordLaneControllerV1?.(); - const session = await controller!.open(ACTIVATION); + const session = await controller!.open(activation); expect(supervisor.calls).toEqual(['stop', 'start']); expect(session.state).toBe('enabled'); }); @@ -216,7 +221,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => const held = store.query('SELECT ?s WHERE { ?s ?p ?o }'); await gated.firstEntry; - const opening = controller!.open(ACTIVATION); + const opening = controller!.open(activation); await drainTurns(10); const queuedDuringSection = store.query('ASK { ?s ?p ?o }'); @@ -232,7 +237,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => const controller = store.getSystemRecordLaneControllerV1?.(); supervisor.failAt = 'start'; - await expect(controller!.open(ACTIVATION)).rejects.toThrow(/supervisor start failed/); + await expect(controller!.open(activation)).rejects.toThrow(/supervisor start failed/); await expect(store.insert([{ subject: 'urn:test:s', predicate: 'urn:test:p', @@ -245,7 +250,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => it('disposes an opened controller on store close and releases registration once', async () => { const controller = store.getSystemRecordLaneControllerV1?.(); - await controller!.open(ACTIVATION); + await controller!.open(activation); supervisor.calls.length = 0; await store.close(); diff --git a/packages/storage/test/system-record-managed-mutation-admission-v1.test.ts b/packages/storage/test/system-record-managed-mutation-admission-v1.test.ts index 97424ad14..ab54de6cb 100644 --- a/packages/storage/test/system-record-managed-mutation-admission-v1.test.ts +++ b/packages/storage/test/system-record-managed-mutation-admission-v1.test.ts @@ -10,6 +10,7 @@ import { } from '../src/managed-oxigraph-ownership-v1-internal.js'; import { __resetSystemRecordControllerRegistrationForTests } from '../src/system-record-materializer-v1.js'; import { externalStorePriorityScheduler } from '../src/store-priority-scheduler.js'; +import { resolveOwnedSystemRecordVerifiedReplacementRuntimeV1 } from '../src/system-record-verified-replacement-v1-internal.js'; let QUERY_ENDPOINT: string; let UPDATE_ENDPOINT: string; @@ -115,7 +116,9 @@ describe('managed Oxigraph mutation admission V1', () => { async function activate(): Promise { const controller = store.getSystemRecordLaneControllerV1?.(); expect(controller).toBeDefined(); - await controller!.open({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow' }); + await controller!.open(resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + ownership.lease, + ).activationIssuer.issue({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow' })); } function holdAgentsExclusive(): { @@ -331,11 +334,13 @@ describe('managed Oxigraph mutation admission V1', () => { it('restores the zero-metadata scheduler fast path after a successful disable', async () => { const controller = store.getSystemRecordLaneControllerV1?.(); expect(controller).toBeDefined(); - const session = await controller!.open({ + const session = await controller!.open(resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + ownership.lease, + ).activationIssuer.issue({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow', - }); + })); await session.close('disable'); const before = externalStorePriorityScheduler.snapshot; diff --git a/packages/storage/test/system-record-materialization-epoch-adapter-v1.test.ts b/packages/storage/test/system-record-materialization-epoch-adapter-v1.test.ts index 9190f13a4..589eb299a 100644 --- a/packages/storage/test/system-record-materialization-epoch-adapter-v1.test.ts +++ b/packages/storage/test/system-record-materialization-epoch-adapter-v1.test.ts @@ -7,6 +7,7 @@ import { createManagedOxigraphOwnershipControllerV1, } from '../src/managed-oxigraph-ownership-v1-internal.js'; import { __resetSystemRecordControllerRegistrationForTests } from '../src/system-record-materializer-v1.js'; +import { resolveOwnedSystemRecordVerifiedReplacementRuntimeV1 } from '../src/system-record-verified-replacement-v1-internal.js'; let server: Server; let queryEndpoint: string; @@ -78,12 +79,15 @@ describe('sparql-http managed epoch handoff', () => { const store = new SparqlHttpStore(options); const controller = store.getSystemRecordLaneControllerV1(); expect(controller).toBeDefined(); + const activation = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + ownership.lease, + ).activationIssuer.issue({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow' }); - const first = await controller!.open({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow' }); + const first = await controller!.open(activation); expect(epoch).toBe('1'); await first.close('disable'); expect(epoch).toBe('2'); - const second = await controller!.open({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow' }); + const second = await controller!.open(activation); expect(epoch).toBe('3'); expect(requests.map((request) => request.path)).toEqual([ '/query', '/update', '/query', diff --git a/packages/storage/test/system-record-verified-replacement-v1.test.ts b/packages/storage/test/system-record-verified-replacement-v1.test.ts index b691c368c..2dbe7c480 100644 --- a/packages/storage/test/system-record-verified-replacement-v1.test.ts +++ b/packages/storage/test/system-record-verified-replacement-v1.test.ts @@ -22,8 +22,10 @@ import { } from '@origintrail-official/dkg-core/system-record-v1'; import { describe, expect, it } from 'vitest'; +import { createManagedOxigraphOwnershipControllerV1 } from '../src/managed-oxigraph-ownership-v1-internal.js'; import { createSystemRecordVerifiedReplacementRegistryV1, + resolveOwnedSystemRecordVerifiedReplacementRuntimeV1, type SystemRecordActiveReplacementIssueV1, type SystemRecordVerifiedReplacementLaneBindingV1, } from '../src/system-record-verified-replacement-v1-internal.js'; @@ -301,6 +303,120 @@ describe('system-record verified replacement V1', () => { expect(registry.issuer.issueActive(input)).toBeDefined(); }); + it('resolves one runtime per authentic lease under one process-wide reservation', () => { + const firstOwnership = createManagedOxigraphOwnershipControllerV1( + 'http://127.0.0.1:7878/query', + 'http://127.0.0.1:7878/update', + ); + const secondOwnership = createManagedOxigraphOwnershipControllerV1( + 'http://127.0.0.1:7879/query', + 'http://127.0.0.1:7879/update', + ); + firstOwnership.bindReadyGeneration(); + secondOwnership.bindReadyGeneration(); + + const first = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(firstOwnership.lease); + const firstAgain = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(firstOwnership.lease); + const second = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(secondOwnership.lease); + expect(firstAgain).toBe(first); + expect(second).not.toBe(first); + expect(() => resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + Object.freeze(Object.create(null) as object) as typeof firstOwnership.lease, + )).toThrow(/authentic managed Oxigraph ownership lease/); + + const activation = first.activationIssuer.issue({ + networkId: 'testnet', + kinds: ['agents'], + mode: 'shadow', + }); + expect(Object.isFrozen(activation)).toBe(true); + expect(Object.getPrototypeOf(activation)).toBeNull(); + expect(Reflect.ownKeys(activation)).toEqual([]); + expect(first.activationReader.read(activation)).toEqual({ + networkId: 'testnet', + kinds: ['agents'], + mode: 'shadow', + }); + expect(() => second.activationReader.read(activation)).toThrow(/another runtime/); + expect(() => first.activationReader.read({ + networkId: 'testnet', + kinds: ['agents'], + mode: 'shadow', + })).toThrow(/activation capability/); + + const { input } = fixture(); + const firstHandle = first.issuer.issueActive(input); + expect(() => second.issuer.issueActive(input)).toThrow(/reservation is already live/); + first.consumer.release(firstHandle); + + const secondHandle = second.issuer.issueActive(input); + second.consumer.release(secondHandle); + }); + + it('keeps ownership liveness outside persisted runtime configuration', () => { + const ownership = createManagedOxigraphOwnershipControllerV1( + 'http://127.0.0.1:7880/query', + 'http://127.0.0.1:7880/update', + ); + const runtime = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(ownership.lease); + const { input } = fixture(); + + expect(() => runtime.issuer.issueActive(input)).toThrow(/ownership lease is not ready/); + expect(() => runtime.activationIssuer.issue({ + networkId: 'testnet', + kinds: ['agents'], + mode: 'shadow', + })).toThrow(/ownership lease is not ready/); + ownership.bindReadyGeneration(); + const handle = runtime.issuer.issueActive(input); + runtime.consumer.release(handle); + ownership.invalidate('shutdown'); + expect(() => runtime.issuer.issueActive(input)).toThrow(/ownership lease is not ready/); + expect(() => runtime.activationIssuer.issue({ + networkId: 'testnet', + kinds: ['agents'], + mode: 'shadow', + })).toThrow(/ownership lease is not ready/); + }); + + it('refuses diagnostic leases that do not prove the managed listener endpoints', () => { + const diagnostic = createManagedOxigraphOwnershipControllerV1(); + diagnostic.bindReadyGeneration(); + expect(() => resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + diagnostic.lease, + )).toThrow(/endpoint-bound managed Oxigraph ownership lease/); + }); + + it('holds the process reservation across recovery ownership until settlement', async () => { + const firstOwnership = createManagedOxigraphOwnershipControllerV1( + 'http://127.0.0.1:7881/query', + 'http://127.0.0.1:7881/update', + ); + const secondOwnership = createManagedOxigraphOwnershipControllerV1( + 'http://127.0.0.1:7882/query', + 'http://127.0.0.1:7882/update', + ); + firstOwnership.bindReadyGeneration(); + secondOwnership.bindReadyGeneration(); + const first = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(firstOwnership.lease); + const second = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(secondOwnership.lease); + const { input, bindings } = fixture(); + const facts = first.consumer.consume(first.issuer.issueActive(input), bindings); + const ownership = Object.freeze(Object.create(null) as object); + let settle!: () => void; + const completion = new Promise((resolve) => { + settle = resolve; + }); + + first.consumer.transferToRecovery(facts, ownership, completion); + expect(() => second.issuer.issueActive(input)).toThrow(/reservation is already live/); + settle(); + await completion; + await Promise.resolve(); + const next = second.issuer.issueActive(input); + second.consumer.release(next); + }); + it('discards only a live unconsumed proof and refuses aliases after consumption', () => { const registry = createSystemRecordVerifiedReplacementRegistryV1(); const { input, bindings } = fixture(); @@ -636,5 +752,6 @@ describe('system-record verified replacement V1', () => { it('is not exported from the storage package barrel', async () => { const storage = await import('../src/index.js'); expect('createSystemRecordVerifiedReplacementRegistryV1' in storage).toBe(false); + expect('resolveOwnedSystemRecordVerifiedReplacementRuntimeV1' in storage).toBe(false); }); }); From 5015aca9ec5756b3ad40380dde0b253ef7c43b5c Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Fri, 7 Aug 2026 12:43:21 +0200 Subject: [PATCH 2/4] fix(storage): recover prior system-record epochs --- .../system-record-next-state-v1-internal.ts | 4 +- ...ystem-record-state-snapshot-v1-internal.ts | 7 +- .../test/system-record-next-state-v1.test.ts | 68 +++++++++++++++++++ .../system-record-state-snapshot-v1.test.ts | 32 +++++++++ 4 files changed, 107 insertions(+), 4 deletions(-) diff --git a/packages/storage/src/system-record-next-state-v1-internal.ts b/packages/storage/src/system-record-next-state-v1-internal.ts index 0a90ed23f..38ac9a0bf 100644 --- a/packages/storage/src/system-record-next-state-v1-internal.ts +++ b/packages/storage/src/system-record-next-state-v1-internal.ts @@ -231,7 +231,9 @@ export function deriveSystemRecordActiveReplacementV1(input: { ? snapshot.ownedSubjectTable : Object.freeze([]) as OwnedSubjectTableObjectV1; - if (authority.equalHead) { + const recoveringPriorEpoch = snapshot.state === 'present' + && snapshot.appliedState.materializationEpoch !== facts.materializationEpoch; + if (authority.equalHead && !recoveringPriorEpoch) { if (snapshot.state !== 'present') { throw new Error('equal system-record head cannot exist in absent state'); } diff --git a/packages/storage/src/system-record-state-snapshot-v1-internal.ts b/packages/storage/src/system-record-state-snapshot-v1-internal.ts index c198215f4..3d4139065 100644 --- a/packages/storage/src/system-record-state-snapshot-v1-internal.ts +++ b/packages/storage/src/system-record-state-snapshot-v1-internal.ts @@ -191,8 +191,9 @@ export function decodeSystemRecordAppliedSnapshotV1(input: { if (appliedState.networkId !== networkId || appliedState.stableKeyHash !== stableKeyHash || claims.networkId !== networkId || claims.stableKeyHash !== stableKeyHash || capacity.networkId !== networkId || receipt.networkId !== networkId - || receipt.stableKeyHash !== stableKeyHash || appliedState.materializationEpoch !== epoch - || receipt.materializationEpoch !== epoch) { + || receipt.stableKeyHash !== stableKeyHash + || receipt.materializationEpoch !== appliedState.materializationEpoch + || BigInt(appliedState.materializationEpoch) > BigInt(epoch)) { throw new Error('persisted system-record tuple crosses its network, key, or epoch binding'); } const canonicalTableBytes = canonicalizeOwnedSubjectTableObjectV1( @@ -228,7 +229,7 @@ export function decodeSystemRecordAppliedSnapshotV1(input: { const expectedFirstRead = Object.freeze([ ...canonical.record, ...canonical.capacity, - ...canonical.epoch, + ...epochRows, ...canonical.receipt, ]); assertExactQuadSet(quads, expectedFirstRead, 'reserved state'); diff --git a/packages/storage/test/system-record-next-state-v1.test.ts b/packages/storage/test/system-record-next-state-v1.test.ts index 23bd45a45..3b376b331 100644 --- a/packages/storage/test/system-record-next-state-v1.test.ts +++ b/packages/storage/test/system-record-next-state-v1.test.ts @@ -169,6 +169,37 @@ describe('system-record active next-state derivation', () => { expect(() => assertAuthenticSystemRecordActiveReplacementCompleteV1(result)).not.toThrow(); }); + it('atomically rematerializes an equal head retained from a prior durable epoch', () => { + const cold = coldReady(); + const snapshot = snapshotAtPriorEpoch(cold, '12'); + const result = expectReady(deriveSystemRecordActiveReplacementV1({ + facts: INITIAL_FACTS, + snapshot, + observedRootClaimQuads: cold.next.rootClaimQuads, + })); + + expect(snapshot).toMatchObject({ + materializationEpoch: EPOCH, + appliedState: { materializationEpoch: '12', stateRevision: '1' }, + }); + expect(result.nextAppliedState).toMatchObject({ + materializationEpoch: EPOCH, + stateRevision: '2', + headDigest: cold.next.appliedState.headDigest, + }); + expect(result.next.capacityState).toMatchObject({ revision: '2', liveRecordCount: '1' }); + expect(result.previousReservedQuads).toHaveLength( + snapshot.previousReservedQuads.length + cold.next.rootClaimQuads.length, + ); + expect(result.previousReservedQuads).toEqual(expect.arrayContaining([ + ...snapshot.previousReservedQuads, + ...cold.next.rootClaimQuads, + ])); + expect(result.nextReservedQuads).not.toEqual(result.previousReservedQuads); + expect(result.success.stateRevision).toBe('2'); + expect(() => assertAuthenticSystemRecordActiveReplacementCompleteV1(result)).not.toThrow(); + }); + it('never acknowledges an equal digest whose canonical persisted tuple disagrees with the head', () => { const cold = coldReady(); const inconsistent = snapshotWithAppliedState(cold, { @@ -442,6 +473,43 @@ function snapshotFrom(value: SystemRecordActiveReplacementCompleteV1): SystemRec }); } +function snapshotAtPriorEpoch( + ready: SystemRecordActiveReplacementReadyV1, + priorEpoch: string, +): SystemRecordAppliedSnapshotV1 { + const appliedState: SystemRecordAppliedStatePresentV1 = { + ...ready.next.appliedState, + materializationEpoch: priorEpoch, + }; + const receipt: SystemRecordMaterializationReceiptV1 = { + ...ready.next.receipt, + materializationEpoch: priorEpoch, + appliedStateDigest: computeSystemRecordAppliedStateDigestV1(appliedState), + }; + const prior = buildSystemRecordReservedStateQuadsV1({ + appliedState, + headVersion: ready.next.headVersion, + ownedSubjectTable: ready.next.ownedSubjectTable, + rootClaimSet: ready.next.rootClaimSet, + capacityState: ready.next.capacityState, + receipt, + }); + const currentEpoch = buildSystemRecordReservedStateQuadsV1({ + appliedState: ready.next.appliedState, + headVersion: ready.next.headVersion, + ownedSubjectTable: ready.next.ownedSubjectTable, + rootClaimSet: ready.next.rootClaimSet, + capacityState: ready.next.capacityState, + receipt: ready.next.receipt, + }).epoch; + return decodeSystemRecordAppliedSnapshotV1({ + networkId: INITIAL.networkId, + stableKeyHash: computeStableKey(INITIAL), + materializationEpoch: EPOCH, + quads: [...prior.record, ...prior.capacity, ...currentEpoch, ...prior.receipt], + }); +} + function snapshotWithAppliedState( ready: SystemRecordActiveReplacementReadyV1, appliedState: SystemRecordAppliedStatePresentV1, diff --git a/packages/storage/test/system-record-state-snapshot-v1.test.ts b/packages/storage/test/system-record-state-snapshot-v1.test.ts index ffc02a3fb..ee3fcaf80 100644 --- a/packages/storage/test/system-record-state-snapshot-v1.test.ts +++ b/packages/storage/test/system-record-state-snapshot-v1.test.ts @@ -88,6 +88,38 @@ describe('system-record reserved-state snapshot decoder', () => { expect(() => assertAuthenticSystemRecordAppliedSnapshotV1({ ...decoded })).toThrow(/exact decoder/); }); + it('decodes an exact prior-epoch tuple against the current durable epoch', () => { + const prior = tuple({ appliedState: state({ materializationEpoch: '1' }) }); + const currentEpoch = tuple().epoch; + const decoded = decodeSystemRecordAppliedSnapshotV1({ + networkId: NETWORK, + stableKeyHash: STABLE_KEY, + materializationEpoch: '2', + quads: [...prior.record, ...prior.capacity, ...currentEpoch, ...prior.receipt], + }); + + expect(decoded).toMatchObject({ + state: 'present', + materializationEpoch: '2', + appliedState: { materializationEpoch: '1' }, + receipt: { materializationEpoch: '1' }, + }); + expect(decoded.previousReservedQuads).toEqual(expect.arrayContaining(currentEpoch)); + expect(decoded.previousReservedQuads).not.toEqual(expect.arrayContaining(prior.epoch)); + }); + + it('rejects a persisted tuple from a future materialization epoch', () => { + const future = tuple({ appliedState: state({ materializationEpoch: '3' }) }); + const currentEpoch = tuple().epoch; + + expect(() => decodeSystemRecordAppliedSnapshotV1({ + networkId: NETWORK, + stableKeyHash: STABLE_KEY, + materializationEpoch: '2', + quads: [...future.record, ...future.capacity, ...currentEpoch, ...future.receipt], + })).toThrow(/epoch binding/); + }); + it('rejects missing, extra, duplicate, malformed, and mismatched-epoch rows', () => { const canonical = tuple(); const all = [...canonical.record, ...canonical.capacity, ...canonical.epoch, ...canonical.receipt]; From 0ba3bb6c9c01917207c7a243b51121444ccee31f Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Fri, 7 Aug 2026 15:05:48 +0200 Subject: [PATCH 3/4] fix(storage): preserve system-record activation contract --- packages/storage/src/adapters/sparql-http.ts | 5 +- ...stem-record-lane-activation-v1-internal.ts | 139 ------------------ .../src/system-record-materializer-v1.ts | 110 +++++++++++--- .../system-record-next-state-v1-internal.ts | 4 +- .../src/system-record-runtime-v1-internal.ts | 83 +++++++++++ ...ystem-record-state-snapshot-v1-internal.ts | 21 ++- ...record-verified-replacement-v1-internal.ts | 121 ++++----------- ...tem-record-capability-discovery-v1.test.ts | 26 +--- ...ord-control-barrier-integration-v1.test.ts | 15 +- ...cord-managed-mutation-admission-v1.test.ts | 11 +- ...d-materialization-epoch-adapter-v1.test.ts | 8 +- ...m-record-materializer-lifecycle-v1.test.ts | 26 ++++ .../test/system-record-next-state-v1.test.ts | 2 + .../system-record-state-snapshot-v1.test.ts | 21 +++ ...tem-record-verified-replacement-v1.test.ts | 50 ++----- 15 files changed, 296 insertions(+), 346 deletions(-) delete mode 100644 packages/storage/src/system-record-lane-activation-v1-internal.ts create mode 100644 packages/storage/src/system-record-runtime-v1-internal.ts diff --git a/packages/storage/src/adapters/sparql-http.ts b/packages/storage/src/adapters/sparql-http.ts index 294b0adfb..34fcb8904 100644 --- a/packages/storage/src/adapters/sparql-http.ts +++ b/packages/storage/src/adapters/sparql-http.ts @@ -69,7 +69,7 @@ import { type SystemRecordLaneExecutionBindingV1, } from '../system-record-materializer-v1.js'; import { createSystemRecordAtomicApplyExecutorV1 } from '../system-record-atomic-apply-executor-v1-internal.js'; -import { resolveOwnedSystemRecordVerifiedReplacementRuntimeV1 } from '../system-record-verified-replacement-v1-internal.js'; +import { resolveOwnedSystemRecordRuntimeV1 } from '../system-record-runtime-v1-internal.js'; import { OwnedManagedHttpClient } from './managed-http-client.js'; import { rotateSystemRecordMaterializationEpochV1 } from '../system-record-materialization-epoch-v1-internal.js'; import { UnsupportedTripleStoreCapabilityError } from '../unsupported-capability-error.js'; @@ -592,7 +592,7 @@ export class SparqlHttpStore implements TripleStore { // registry, and all authentic leases share one process-wide accountant. // The issuer remains outside the store and has no production caller in // this default-unused stack; the later lifecycle verifier captures it. - const { consumer, activationReader } = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + const { consumer } = resolveOwnedSystemRecordRuntimeV1( this.ownershipLease, ); const atomicExecutor = createSystemRecordAtomicApplyExecutorV1({ @@ -605,7 +605,6 @@ export class SparqlHttpStore implements TripleStore { const owner = createSystemRecordLaneControllerV1({ lease: this.ownershipLease, handoff: this.buildChildHandoff(this.supervisorHandoff), - activationReader, executor: { applyVerified: (proof, childGeneration) => this.executeSystemRecordApplyLegacy(proof, childGeneration), diff --git a/packages/storage/src/system-record-lane-activation-v1-internal.ts b/packages/storage/src/system-record-lane-activation-v1-internal.ts deleted file mode 100644 index 9129e8a5d..000000000 --- a/packages/storage/src/system-record-lane-activation-v1-internal.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { types as utilTypes } from 'node:util'; - -declare const SYSTEM_RECORD_LANE_ACTIVATION_BRAND: unique symbol; - -/** Empty process-local authority. Its descriptor exists only in the private table. */ -export type OwnedSystemRecordLaneActivationV1 = { - readonly [SYSTEM_RECORD_LANE_ACTIVATION_BRAND]: 'owned-system-record-lane-activation-v1'; -}; - -export interface SystemRecordLaneActivationDescriptorV1 { - readonly networkId: string; - readonly kinds: readonly ['agents']; - readonly mode: 'shadow' | 'authoritative'; -} - -export interface SystemRecordLaneActivationIssuerV1 { - issue(descriptor: SystemRecordLaneActivationDescriptorV1): OwnedSystemRecordLaneActivationV1; -} - -export interface SystemRecordLaneActivationReaderV1 { - read(activation: unknown): SystemRecordLaneActivationDescriptorV1; -} - -export interface SystemRecordLaneActivationRegistryV1 { - readonly issuer: SystemRecordLaneActivationIssuerV1; - readonly reader: SystemRecordLaneActivationReaderV1; -} - -interface RegisteredActivationV1 { - readonly registryIdentity: object; - readonly descriptor: SystemRecordLaneActivationDescriptorV1; -} - -const REGISTERED_ACTIVATIONS = new WeakMap(); -const NETWORK_ID_PATTERN_V1 = /^[A-Za-z0-9._:-]+$/; -const MAX_NETWORK_ID_BYTES_V1 = 128; -const UTF8 = new TextEncoder(); - -/** Snapshot the closed activation record without invoking caller accessors or iterators. */ -export function snapshotSystemRecordLaneActivationDescriptorV1( - activation: unknown, -): SystemRecordLaneActivationDescriptorV1 { - if ( - activation === null || - typeof activation !== 'object' || - Array.isArray(activation) || - utilTypes.isProxy(activation) || - ![Object.prototype, null].includes(Object.getPrototypeOf(activation)) - ) { - throw new Error('system-record lane activation must be a plain data object'); - } - - const expected = ['kinds', 'mode', 'networkId']; - const ownKeys = Reflect.ownKeys(activation); - if ( - ownKeys.length !== expected.length || - ownKeys.some((key) => typeof key !== 'string') || - [...(ownKeys as string[])].sort().some((key, index) => key !== expected[index]) - ) { - throw new Error('system-record lane activation has unknown or missing fields'); - } - - const readDataField = (key: string): unknown => { - const field = Object.getOwnPropertyDescriptor(activation, key); - if (!field?.enumerable || !Object.prototype.hasOwnProperty.call(field, 'value')) { - throw new Error('system-record lane activation fields must be enumerable data properties'); - } - return field.value; - }; - - const networkId = readDataField('networkId'); - if ( - typeof networkId !== 'string' || - networkId.length === 0 || - UTF8.encode(networkId).byteLength > MAX_NETWORK_ID_BYTES_V1 || - !NETWORK_ID_PATTERN_V1.test(networkId) - ) { - throw new Error('system-record lane activation networkId is not canonical'); - } - - const kinds = readDataField('kinds'); - if (!Array.isArray(kinds) || utilTypes.isProxy(kinds)) { - throw new Error('system-record lane activation kinds must be the closed [agents] tuple'); - } - const kindKeys = Reflect.ownKeys(kinds); - const length = Object.getOwnPropertyDescriptor(kinds, 'length'); - const first = Object.getOwnPropertyDescriptor(kinds, '0'); - if ( - kindKeys.length !== 2 || - !kindKeys.includes('length') || - !kindKeys.includes('0') || - length?.value !== 1 || - length.enumerable || - !first?.enumerable || - !Object.prototype.hasOwnProperty.call(first, 'value') || - first.value !== 'agents' - ) { - throw new Error('system-record lane activation kinds must be the closed [agents] tuple'); - } - - const mode = readDataField('mode'); - if (mode !== 'shadow' && mode !== 'authoritative') { - throw new Error('system-record lane activation mode is invalid'); - } - - return Object.freeze({ networkId, kinds: Object.freeze(['agents'] as const), mode }); -} - -/** - * Create one non-interchangeable issuer/reader pair. Production captures the reader in - * the controller and the issuer in the later lifecycle owner; neither belongs in config. - */ -export function createSystemRecordLaneActivationRegistryV1( - assertAvailable?: () => void, -): SystemRecordLaneActivationRegistryV1 { - const registryIdentity = Object.freeze(Object.create(null) as object); - const issuer: SystemRecordLaneActivationIssuerV1 = Object.freeze({ - issue(value: SystemRecordLaneActivationDescriptorV1): OwnedSystemRecordLaneActivationV1 { - assertAvailable?.(); - const descriptor = snapshotSystemRecordLaneActivationDescriptorV1(value); - const handle = Object.freeze(Object.create(null) as object) as OwnedSystemRecordLaneActivationV1; - REGISTERED_ACTIVATIONS.set(handle, { registryIdentity, descriptor }); - return handle; - }, - }); - const reader: SystemRecordLaneActivationReaderV1 = Object.freeze({ - read(activation: unknown): SystemRecordLaneActivationDescriptorV1 { - if (activation === null || typeof activation !== 'object') { - throw new Error('system-record lane activation capability is invalid'); - } - const registered = REGISTERED_ACTIVATIONS.get(activation); - if (registered?.registryIdentity !== registryIdentity) { - throw new Error('system-record lane activation capability is invalid or belongs to another runtime'); - } - return registered.descriptor; - }, - }); - return Object.freeze({ issuer, reader }); -} diff --git a/packages/storage/src/system-record-materializer-v1.ts b/packages/storage/src/system-record-materializer-v1.ts index a0b37a9eb..0816566c5 100644 --- a/packages/storage/src/system-record-materializer-v1.ts +++ b/packages/storage/src/system-record-materializer-v1.ts @@ -1,3 +1,5 @@ +import { types as utilTypes } from 'node:util'; + import { readManagedOxigraphOwnershipSnapshotV1, type ManagedOxigraphOwnershipLeaseV1, @@ -9,11 +11,6 @@ import type { SystemRecordAtomicRecoveryResolutionV1, SystemRecordAtomicRecoveryRuntimeV1, } from './system-record-atomic-apply-executor-v1-internal.js'; -import { - snapshotSystemRecordLaneActivationDescriptorV1, - type OwnedSystemRecordLaneActivationV1, - type SystemRecordLaneActivationReaderV1, -} from './system-record-lane-activation-v1-internal.js'; /** * System-record V1 lane controller (#2052 Stack B2). @@ -35,8 +32,21 @@ import { * Public contract * ------------------------------------------------------------------ */ -/** Lifecycle-issued process-local authority; no public descriptor can mint it. */ -export type SystemRecordLaneActivationV1 = OwnedSystemRecordLaneActivationV1; +/** + * Activation descriptor. Carries no authority of its own: it names WHICH + * `(network, kind)` set to enable, while the right to enable anything at all + * comes from the ownership lease captured at controller construction. + */ +export interface SystemRecordLaneActivationV1 { + readonly networkId: string; + /** V1 accepts only the fixed `agents` kind; `ontology` is Stack E. */ + readonly kinds: readonly ['agents']; + /** + * Pre-activation shadow mode keeps the legacy lane authoritative: V1 rows are + * materialized and charged, but legacy RDF is never deleted. + */ + readonly mode: 'shadow' | 'authoritative'; +} export type SystemRecordApplyOutcomeV1 = | { readonly outcome: 'applied'; readonly stateRevision: string; readonly appliedStateDigest: string } @@ -197,8 +207,6 @@ export interface SystemRecordLaneControllerDepsV1 { readonly lease: ManagedOxigraphOwnershipLeaseV1; readonly handoff: SystemRecordChildHandoffV1; readonly executor: SystemRecordTransactionExecutorV1; - /** Production reader for the opaque lifecycle-issued activation capability. */ - readonly activationReader?: SystemRecordLaneActivationReaderV1; /** * Required, not optional. An optional barrier is one that gets forgotten: * this capability shipped once with a barrier implemented, exported and @@ -247,6 +255,78 @@ interface SystemRecordLaneActivationSnapshotV1 { readonly mode: 'shadow' | 'authoritative'; } +const NETWORK_ID_PATTERN_V1 = /^[A-Za-z0-9._:-]+$/; +const MAX_NETWORK_ID_BYTES_V1 = 128; +const UTF8 = new TextEncoder(); + +/** Snapshot the closed activation record without invoking caller traps or accessors. */ +const snapshotActivation = (activation: unknown): SystemRecordLaneActivationSnapshotV1 => { + if ( + activation === null || + typeof activation !== 'object' || + Array.isArray(activation) || + utilTypes.isProxy(activation) || + ![Object.prototype, null].includes(Object.getPrototypeOf(activation)) + ) { + throw new Error('system-record lane activation must be a plain data object'); + } + + const expected = ['kinds', 'mode', 'networkId']; + const ownKeys = Reflect.ownKeys(activation); + if ( + ownKeys.length !== expected.length || + ownKeys.some((key) => typeof key !== 'string') || + [...(ownKeys as string[])].sort().some((key, index) => key !== expected[index]) + ) { + throw new Error('system-record lane activation has unknown or missing fields'); + } + + const readDataField = (key: string): unknown => { + const field = Object.getOwnPropertyDescriptor(activation, key); + if (!field?.enumerable || !Object.prototype.hasOwnProperty.call(field, 'value')) { + throw new Error('system-record lane activation fields must be enumerable data properties'); + } + return field.value; + }; + + const networkId = readDataField('networkId'); + if ( + typeof networkId !== 'string' || + networkId.length === 0 || + UTF8.encode(networkId).byteLength > MAX_NETWORK_ID_BYTES_V1 || + !NETWORK_ID_PATTERN_V1.test(networkId) + ) { + throw new Error('system-record lane activation networkId is not canonical'); + } + + const kinds = readDataField('kinds'); + if (!Array.isArray(kinds) || utilTypes.isProxy(kinds)) { + throw new Error('system-record lane activation kinds must be the closed [agents] tuple'); + } + const kindKeys = Reflect.ownKeys(kinds); + const length = Object.getOwnPropertyDescriptor(kinds, 'length'); + const first = Object.getOwnPropertyDescriptor(kinds, '0'); + if ( + kindKeys.length !== 2 || + !kindKeys.includes('length') || + !kindKeys.includes('0') || + length?.value !== 1 || + length.enumerable || + !first?.enumerable || + !Object.prototype.hasOwnProperty.call(first, 'value') || + first.value !== 'agents' + ) { + throw new Error('system-record lane activation kinds must be the closed [agents] tuple'); + } + + const mode = readDataField('mode'); + if (mode !== 'shadow' && mode !== 'authoritative') { + throw new Error('system-record lane activation mode is invalid'); + } + + return Object.freeze({ networkId, kind: 'agents', mode }); +}; + const descriptorOf = (activation: SystemRecordLaneActivationSnapshotV1): string => `${activation.networkId}|${activation.kind}|${activation.mode}`; @@ -485,16 +565,8 @@ class SystemRecordLaneSession { /* -------------------------------------------------------------- */ - async open(activation: unknown): Promise { - const descriptor = this.deps.activationReader - ? this.deps.activationReader.read(activation) - : activation; - const parsed = snapshotSystemRecordLaneActivationDescriptorV1(descriptor); - const activationSnapshot: SystemRecordLaneActivationSnapshotV1 = Object.freeze({ - networkId: parsed.networkId, - kind: 'agents', - mode: parsed.mode, - }); + async open(activation: SystemRecordLaneActivationV1): Promise { + const activationSnapshot = snapshotActivation(activation); const wanted = descriptorOf(activationSnapshot); this.assertNotTerminal(); diff --git a/packages/storage/src/system-record-next-state-v1-internal.ts b/packages/storage/src/system-record-next-state-v1-internal.ts index 38ac9a0bf..779869bc1 100644 --- a/packages/storage/src/system-record-next-state-v1-internal.ts +++ b/packages/storage/src/system-record-next-state-v1-internal.ts @@ -231,9 +231,7 @@ export function deriveSystemRecordActiveReplacementV1(input: { ? snapshot.ownedSubjectTable : Object.freeze([]) as OwnedSubjectTableObjectV1; - const recoveringPriorEpoch = snapshot.state === 'present' - && snapshot.appliedState.materializationEpoch !== facts.materializationEpoch; - if (authority.equalHead && !recoveringPriorEpoch) { + if (authority.equalHead && !(snapshot.state === 'present' && snapshot.requiresRematerialization)) { if (snapshot.state !== 'present') { throw new Error('equal system-record head cannot exist in absent state'); } diff --git a/packages/storage/src/system-record-runtime-v1-internal.ts b/packages/storage/src/system-record-runtime-v1-internal.ts new file mode 100644 index 000000000..de9f5d86d --- /dev/null +++ b/packages/storage/src/system-record-runtime-v1-internal.ts @@ -0,0 +1,83 @@ +import { + SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES, +} from '@origintrail-official/dkg-core/system-record-v1'; + +import { + isManagedOxigraphOwnershipLeaseV1, + readManagedOxigraphOwnershipSnapshotV1, + type ManagedOxigraphOwnershipLeaseV1, +} from './managed-oxigraph-ownership-v1-internal.js'; +import { + createSystemRecordVerifiedReplacementRegistryForRuntimeV1, + type SystemRecordRuntimeReservationGateV1, + type SystemRecordVerifiedReplacementRegistryV1, +} from './system-record-verified-replacement-v1-internal.js'; + +interface SystemRecordRuntimeReservationStateV1 { + liveOwner: object | null; + accountedBytes: number; +} + +/** One nonqueued process-wide gate shared by every authentic managed endpoint. */ +const PROCESS_RESERVATION_STATE: SystemRecordRuntimeReservationStateV1 = { + liveOwner: null, + accountedBytes: 0, +}; + +const PROCESS_RESERVATION_GATE: SystemRecordRuntimeReservationGateV1 = Object.freeze({ + acquire(owner: object, bytes: number): void { + if (!Number.isSafeInteger(bytes) || bytes <= 0 + || PROCESS_RESERVATION_STATE.liveOwner !== null + || PROCESS_RESERVATION_STATE.accountedBytes + bytes + > SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES) { + throw new Error('system-record atomic transient reservation is already live'); + } + PROCESS_RESERVATION_STATE.liveOwner = owner; + PROCESS_RESERVATION_STATE.accountedBytes += bytes; + }, + release(owner: object, bytes: number): void { + if (PROCESS_RESERVATION_STATE.liveOwner !== owner + || PROCESS_RESERVATION_STATE.accountedBytes !== bytes) { + throw new Error('system-record atomic transient accountant state is inconsistent'); + } + PROCESS_RESERVATION_STATE.liveOwner = null; + PROCESS_RESERVATION_STATE.accountedBytes = 0; + }, +}); + +const OWNED_RUNTIMES = new WeakMap< + ManagedOxigraphOwnershipLeaseV1, + SystemRecordVerifiedReplacementRegistryV1 +>(); + +/** + * Resolve the single proof runtime bound to an authentic daemon ownership lease. + * Persisted options and structural look-alikes cannot mint this authority. + */ +export function resolveOwnedSystemRecordRuntimeV1( + lease: ManagedOxigraphOwnershipLeaseV1, +): SystemRecordVerifiedReplacementRegistryV1 { + if (!isManagedOxigraphOwnershipLeaseV1(lease)) { + throw new Error('system-record runtime requires an authentic managed Oxigraph ownership lease'); + } + const ownership = readManagedOxigraphOwnershipSnapshotV1(lease); + if (ownership?.queryEndpoint === undefined || ownership.updateEndpoint === undefined) { + throw new Error('system-record runtime requires an endpoint-bound managed Oxigraph ownership lease'); + } + const existing = OWNED_RUNTIMES.get(lease); + if (existing !== undefined) return existing; + + const runtime = createSystemRecordVerifiedReplacementRegistryForRuntimeV1({ + reservationGate: PROCESS_RESERVATION_GATE, + assertAvailable: () => { + const snapshot = readManagedOxigraphOwnershipSnapshotV1(lease); + if (!snapshot?.ready || snapshot.terminal + || snapshot.queryEndpoint !== ownership.queryEndpoint + || snapshot.updateEndpoint !== ownership.updateEndpoint) { + throw new Error('system-record runtime ownership lease is not ready'); + } + }, + }); + OWNED_RUNTIMES.set(lease, runtime); + return runtime; +} diff --git a/packages/storage/src/system-record-state-snapshot-v1-internal.ts b/packages/storage/src/system-record-state-snapshot-v1-internal.ts index 3d4139065..5bcc132e1 100644 --- a/packages/storage/src/system-record-state-snapshot-v1-internal.ts +++ b/packages/storage/src/system-record-state-snapshot-v1-internal.ts @@ -61,7 +61,11 @@ export interface SystemRecordPresentSnapshotV1 { readonly rootClaimSet: SystemRecordRootClaimSetV1; readonly capacityState: SystemRecordCapacityStateV1; readonly receipt: SystemRecordMaterializationReceiptV1; + /** Current durable epoch read from the global epoch row. */ readonly materializationEpoch: string; + /** Epoch bound by the exact persisted applied-state/receipt tuple. */ + readonly appliedTupleEpoch: string; + readonly requiresRematerialization: boolean; readonly previousReservedQuads: readonly Readonly[]; readonly expectedRootClaimQuads: readonly Readonly[]; readonly requiredAbsentReservedSubjects: readonly string[]; @@ -110,6 +114,9 @@ export function decodeSystemRecordAppliedSnapshotV1(input: { if (epoch !== owned.materializationEpoch) { throw new Error('system-record materialization epoch changed during inspection'); } + const canonicalEpochRows = epochRows.filter((quad) => ( + quad.predicate === SYSTEM_RECORD_V1_PREDICATES.materializationEpoch + )); const decodedCapacity = decodeCapacityState(networkId, capacityRows); @@ -119,9 +126,7 @@ export function decodeSystemRecordAppliedSnapshotV1(input: { } const expectedFirstRead = Object.freeze([ ...capacityRows, - ...epochRows.filter((quad) => ( - quad.predicate === SYSTEM_RECORD_V1_PREDICATES.materializationEpoch - )), + ...canonicalEpochRows, ]); assertExactQuadSet(quads, expectedFirstRead, 'absent reserved state'); return markAuthenticSnapshot(Object.freeze({ @@ -171,9 +176,10 @@ export function decodeSystemRecordAppliedSnapshotV1(input: { 'materialization receipt', )); + const appliedStateDigest = computeSystemRecordAppliedStateDigestV1(appliedState); const digests = [ [recordRows, SYSTEM_RECORD_V1_PREDICATES.appliedStateDigest, - computeSystemRecordAppliedStateDigestV1(appliedState), 'applied-state digest'], + appliedStateDigest, 'applied-state digest'], [recordRows, SYSTEM_RECORD_V1_PREDICATES.ownedSubjectTableDigest, appliedState.ownedSubjectTableDigest, 'owned-table digest'], [recordRows, SYSTEM_RECORD_V1_PREDICATES.rootClaimSetDigest, @@ -193,6 +199,9 @@ export function decodeSystemRecordAppliedSnapshotV1(input: { || capacity.networkId !== networkId || receipt.networkId !== networkId || receipt.stableKeyHash !== stableKeyHash || receipt.materializationEpoch !== appliedState.materializationEpoch + || receipt.stateRevision !== appliedState.stateRevision + || receipt.appliedStateDigest !== appliedStateDigest + || receipt.headDigest !== appliedState.headDigest || BigInt(appliedState.materializationEpoch) > BigInt(epoch)) { throw new Error('persisted system-record tuple crosses its network, key, or epoch binding'); } @@ -229,7 +238,7 @@ export function decodeSystemRecordAppliedSnapshotV1(input: { const expectedFirstRead = Object.freeze([ ...canonical.record, ...canonical.capacity, - ...epochRows, + ...canonicalEpochRows, ...canonical.receipt, ]); assertExactQuadSet(quads, expectedFirstRead, 'reserved state'); @@ -242,6 +251,8 @@ export function decodeSystemRecordAppliedSnapshotV1(input: { capacityState: capacity, receipt, materializationEpoch: epoch, + appliedTupleEpoch: appliedState.materializationEpoch, + requiresRematerialization: appliedState.materializationEpoch !== epoch, previousReservedQuads: expectedFirstRead, expectedRootClaimQuads: canonical.rootClaims, requiredAbsentReservedSubjects: Object.freeze([]), diff --git a/packages/storage/src/system-record-verified-replacement-v1-internal.ts b/packages/storage/src/system-record-verified-replacement-v1-internal.ts index ca88ff68e..d5146dea2 100644 --- a/packages/storage/src/system-record-verified-replacement-v1-internal.ts +++ b/packages/storage/src/system-record-verified-replacement-v1-internal.ts @@ -35,16 +35,6 @@ import { type OwnedSubjectTableObjectV1, } from '@origintrail-official/dkg-core/system-record-v1'; -import { - isManagedOxigraphOwnershipLeaseV1, - readManagedOxigraphOwnershipSnapshotV1, - type ManagedOxigraphOwnershipLeaseV1, -} from './managed-oxigraph-ownership-v1-internal.js'; -import { - createSystemRecordLaneActivationRegistryV1, - type SystemRecordLaneActivationIssuerV1, - type SystemRecordLaneActivationReaderV1, -} from './system-record-lane-activation-v1-internal.js'; import type { Quad } from './triple-store.js'; declare const VERIFIED_REPLACEMENT_HANDLE_BRAND: unique symbol; @@ -149,8 +139,6 @@ export type SystemRecordAtomicChargeCategoryV1 = export interface SystemRecordVerifiedReplacementRegistryV1 { readonly issuer: SystemRecordVerifiedReplacementIssuerV1; readonly consumer: SystemRecordVerifiedReplacementConsumerV1; - readonly activationIssuer: SystemRecordLaneActivationIssuerV1; - readonly activationReader: SystemRecordLaneActivationReaderV1; } interface RegisteredReplacementV1 { @@ -165,7 +153,6 @@ type RuntimeReservationPhaseV1 = 'proof' | 'facts' | 'recovery' | 'released'; interface RuntimeReservationV1 { readonly registryIdentity: object; - readonly accountantIdentity: object; readonly identity: object; readonly bytes: number; readonly admittedDeadlineMs: number; @@ -174,14 +161,13 @@ interface RuntimeReservationV1 { recoveryOwnership?: object; } -interface SystemRecordRuntimeAccountantV1 { - readonly identity: object; - accountedBytes: number; - liveAtomicReservation: RuntimeReservationV1 | null; +export interface SystemRecordRuntimeReservationGateV1 { + acquire(owner: object, bytes: number): void; + release(owner: object, bytes: number): void; } -interface SystemRecordVerifiedReplacementRegistryDepsV1 { - readonly accountant: SystemRecordRuntimeAccountantV1; +export interface SystemRecordVerifiedReplacementRegistryDepsV1 { + readonly reservationGate: SystemRecordRuntimeReservationGateV1; readonly assertAvailable?: () => void; } @@ -196,23 +182,6 @@ const ATOMIC_CHARGE_CATEGORIES = new Set([ 'prepared', ]); -const createSystemRecordRuntimeAccountantV1 = (): SystemRecordRuntimeAccountantV1 => ({ - identity: Object.freeze(Object.create(null) as object), - accountedBytes: 0, - liveAtomicReservation: null, -}); - -/** - * Production reservations are process-wide, not per adapter or per ownership lease. - * Test-only registries created directly below receive an isolated accountant so suites - * cannot leak mutable process state into one another. - */ -const PRODUCTION_RUNTIME_ACCOUNTANT = createSystemRecordRuntimeAccountantV1(); -const PRODUCTION_REGISTRIES = new WeakMap< - ManagedOxigraphOwnershipLeaseV1, - SystemRecordVerifiedReplacementRegistryV1 ->(); - /** Refuse structural facts even when they embed a separately valid authority capability. */ export function assertAuthenticSystemRecordVerifiedReplacementFactsV1( value: unknown, @@ -604,26 +573,19 @@ function bindingsEqual( * Create one non-interchangeable issuer/consumer pair. Only the consumer half belongs * in the storage executor; only the issuer half belongs in the verifier closure. */ -function createSystemRecordVerifiedReplacementRegistryWithDepsV1( +export function createSystemRecordVerifiedReplacementRegistryForRuntimeV1( deps: SystemRecordVerifiedReplacementRegistryDepsV1, ): SystemRecordVerifiedReplacementRegistryV1 { const registryIdentity = Object.freeze(Object.create(null) as object); - const { accountant } = deps; - const activation = createSystemRecordLaneActivationRegistryV1(deps.assertAvailable); + const { reservationGate } = deps; const reserveAtomic = ( admittedDeadlineMs: number, decodedBytes: number, ): RuntimeReservationV1 => { deps.assertAvailable?.(); - if (accountant.liveAtomicReservation !== null - || accountant.accountedBytes + SYSTEM_RECORD_MAX_ATOMIC_TRANSIENT_BYTES - > SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES) { - throw new Error('system-record atomic transient reservation is already live'); - } const reservation: RuntimeReservationV1 = { registryIdentity, - accountantIdentity: accountant.identity, identity: Object.freeze(Object.create(null) as object), bytes: SYSTEM_RECORD_MAX_ATOMIC_TRANSIENT_BYTES, admittedDeadlineMs, @@ -635,8 +597,7 @@ function createSystemRecordVerifiedReplacementRegistryWithDepsV1( }, phase: 'proof', }; - accountant.accountedBytes += reservation.bytes; - accountant.liveAtomicReservation = reservation; + reservationGate.acquire(reservation.identity, reservation.bytes); return reservation; }; @@ -644,19 +605,13 @@ function createSystemRecordVerifiedReplacementRegistryWithDepsV1( if (reservation.registryIdentity !== registryIdentity || reservation.phase === 'released') { throw new Error('system-record atomic transient reservation was already released'); } - if (reservation.accountantIdentity !== accountant.identity - || accountant.liveAtomicReservation !== reservation - || accountant.accountedBytes < reservation.bytes) { - throw new Error('system-record atomic transient accountant state is inconsistent'); - } + reservationGate.release(reservation.identity, reservation.bytes); reservation.phase = 'released'; reservation.charges.decoded = 0; reservation.charges.request = 0; reservation.charges.response = 0; reservation.charges.prepared = 0; reservation.recoveryOwnership = undefined; - accountant.accountedBytes -= reservation.bytes; - accountant.liveAtomicReservation = null; }; const registeredHandle = (handle: unknown): RegisteredReplacementV1 => { @@ -919,12 +874,7 @@ function createSystemRecordVerifiedReplacementRegistryWithDepsV1( }, }); - return Object.freeze({ - issuer, - consumer, - activationIssuer: activation.issuer, - activationReader: activation.reader, - }); + return Object.freeze({ issuer, consumer }); } /** @@ -933,44 +883,31 @@ function createSystemRecordVerifiedReplacementRegistryWithDepsV1( * adapter and future lifecycle verifier shares one process-wide accountant. */ export function createSystemRecordVerifiedReplacementRegistryV1(): SystemRecordVerifiedReplacementRegistryV1 { - return createSystemRecordVerifiedReplacementRegistryWithDepsV1({ - accountant: createSystemRecordRuntimeAccountantV1(), + return createSystemRecordVerifiedReplacementRegistryForRuntimeV1({ + reservationGate: createIsolatedReservationGateV1(), }); } -/** - * Resolve the single runtime bound to an authentic daemon ownership lease. - * - * A persisted option, copied object, or structural look-alike cannot create a runtime. - * The returned pair is intentionally internal to the package: storage retains only the - * consumer while the later agent lifecycle captures the issuer in its verifier closure. - */ -export function resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( - lease: ManagedOxigraphOwnershipLeaseV1, -): SystemRecordVerifiedReplacementRegistryV1 { - if (!isManagedOxigraphOwnershipLeaseV1(lease)) { - throw new Error('system-record runtime requires an authentic managed Oxigraph ownership lease'); - } - const ownership = readManagedOxigraphOwnershipSnapshotV1(lease); - if (ownership?.queryEndpoint === undefined || ownership.updateEndpoint === undefined) { - throw new Error('system-record runtime requires an endpoint-bound managed Oxigraph ownership lease'); - } - const existing = PRODUCTION_REGISTRIES.get(lease); - if (existing !== undefined) return existing; - - const runtime = createSystemRecordVerifiedReplacementRegistryWithDepsV1({ - accountant: PRODUCTION_RUNTIME_ACCOUNTANT, - assertAvailable: () => { - const snapshot = readManagedOxigraphOwnershipSnapshotV1(lease); - if (!snapshot?.ready || snapshot.terminal - || snapshot.queryEndpoint !== ownership.queryEndpoint - || snapshot.updateEndpoint !== ownership.updateEndpoint) { - throw new Error('system-record runtime ownership lease is not ready'); +function createIsolatedReservationGateV1(): SystemRecordRuntimeReservationGateV1 { + let liveOwner: object | null = null; + let accountedBytes = 0; + return Object.freeze({ + acquire(owner: object, bytes: number): void { + if (liveOwner !== null + || accountedBytes + bytes > SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES) { + throw new Error('system-record atomic transient reservation is already live'); + } + liveOwner = owner; + accountedBytes += bytes; + }, + release(owner: object, bytes: number): void { + if (liveOwner !== owner || accountedBytes < bytes) { + throw new Error('system-record atomic transient accountant state is inconsistent'); } + liveOwner = null; + accountedBytes -= bytes; }, }); - PRODUCTION_REGISTRIES.set(lease, runtime); - return runtime; } function retainedVerifiedFactsBytes( diff --git a/packages/storage/test/system-record-capability-discovery-v1.test.ts b/packages/storage/test/system-record-capability-discovery-v1.test.ts index c63140de7..32a842e37 100644 --- a/packages/storage/test/system-record-capability-discovery-v1.test.ts +++ b/packages/storage/test/system-record-capability-discovery-v1.test.ts @@ -12,7 +12,6 @@ import { type ManagedOxigraphSupervisorHandoffV1, } from '../src/managed-oxigraph-ownership-v1-internal.js'; import { __resetSystemRecordControllerRegistrationForTests } from '../src/system-record-materializer-v1.js'; -import { resolveOwnedSystemRecordVerifiedReplacementRuntimeV1 } from '../src/system-record-verified-replacement-v1-internal.js'; import { createTripleStore, type TripleStore } from '../src/triple-store.js'; const QUERY_ENDPOINT = 'http://127.0.0.1:1/query'; @@ -96,18 +95,6 @@ describe('system-record V1 capability discovery', () => { await store.close().catch(() => undefined); }); - it('refuses a caller-authored activation descriptor on the production controller', async () => { - const store = await build(managedOptions()); - const controller = store.getSystemRecordLaneControllerV1?.(); - expect(controller).toBeDefined(); - await expect(controller!.open({ - networkId: 'testnet', - kinds: ['agents'], - mode: 'shadow', - })).rejects.toThrow(/activation capability/); - await store.close(); - }); - it.each([ ['wrong query path', UPDATE_ENDPOINT, UPDATE_ENDPOINT, undefined], ['wrong update path', QUERY_ENDPOINT, QUERY_ENDPOINT, undefined], @@ -162,13 +149,6 @@ describe('system-record V1 capability discovery', () => { const firstStore = await build(managedOptions()); const retired = firstStore.getSystemRecordLaneControllerV1?.(); expect(retired).toBeDefined(); - const activation = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( - ownership.lease, - ).activationIssuer.issue({ - networkId: 'testnet', - kinds: ['agents'], - mode: 'shadow', - }); await firstStore.close(); // SparqlHttpStore.close rotates a reusable lifecycle generation. The @@ -181,7 +161,11 @@ describe('system-record V1 capability discovery', () => { const replacementStore = await build(managedOptions()); expect(replacementStore.getSystemRecordLaneControllerV1?.()).toBeDefined(); - await expect(retired!.open(activation)).rejects.toThrow(/terminal/); + await expect(retired!.open({ + networkId: 'testnet', + kinds: ['agents'], + mode: 'shadow', + })).rejects.toThrow(/terminal/); await replacementStore.close(); }); diff --git a/packages/storage/test/system-record-control-barrier-integration-v1.test.ts b/packages/storage/test/system-record-control-barrier-integration-v1.test.ts index ceecd23d9..f9dcf915e 100644 --- a/packages/storage/test/system-record-control-barrier-integration-v1.test.ts +++ b/packages/storage/test/system-record-control-barrier-integration-v1.test.ts @@ -15,7 +15,6 @@ import { __resetSystemRecordControllerRegistrationForTests, type SystemRecordLaneActivationV1, } from '../src/system-record-materializer-v1.js'; -import { resolveOwnedSystemRecordVerifiedReplacementRuntimeV1 } from '../src/system-record-verified-replacement-v1-internal.js'; import { createTripleStore, type TripleStore } from '../src/triple-store.js'; let QUERY_ENDPOINT: string; @@ -103,7 +102,6 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => let supervisor: RecordingSupervisor; let gated: GatedFetch; let store: TripleStore; - let activation: unknown; beforeAll(async () => { managedServer = createServer((req, res) => { @@ -141,9 +139,6 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => __resetSystemRecordControllerRegistrationForTests(); ownership = createManagedOxigraphOwnershipControllerV1(QUERY_ENDPOINT, UPDATE_ENDPOINT); ownership.bindReadyGeneration(); - activation = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( - ownership.lease, - ).activationIssuer.issue(ACTIVATION); supervisor = new RecordingSupervisor(); gated = new GatedFetch(); epoch = null; @@ -183,7 +178,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => await gated.firstEntry; expect(gated.entered).toBe(1); - const opening = controller!.open(activation); + const opening = controller!.open(ACTIVATION); await drainTurns(); // THE assertion. Without the barrier this reads ['stop', 'start'] here: the @@ -206,7 +201,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => // Positive control for the timing claim: the wait above is caused by the // in-flight request, not by the barrier being slow or the open being async. const controller = store.getSystemRecordLaneControllerV1?.(); - const session = await controller!.open(activation); + const session = await controller!.open(ACTIVATION); expect(supervisor.calls).toEqual(['stop', 'start']); expect(session.state).toBe('enabled'); }); @@ -221,7 +216,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => const held = store.query('SELECT ?s WHERE { ?s ?p ?o }'); await gated.firstEntry; - const opening = controller!.open(activation); + const opening = controller!.open(ACTIVATION); await drainTurns(10); const queuedDuringSection = store.query('ASK { ?s ?p ?o }'); @@ -237,7 +232,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => const controller = store.getSystemRecordLaneControllerV1?.(); supervisor.failAt = 'start'; - await expect(controller!.open(activation)).rejects.toThrow(/supervisor start failed/); + await expect(controller!.open(ACTIVATION)).rejects.toThrow(/supervisor start failed/); await expect(store.insert([{ subject: 'urn:test:s', predicate: 'urn:test:p', @@ -250,7 +245,7 @@ describe('system-record lane control barrier (real adapter + scheduler)', () => it('disposes an opened controller on store close and releases registration once', async () => { const controller = store.getSystemRecordLaneControllerV1?.(); - await controller!.open(activation); + await controller!.open(ACTIVATION); supervisor.calls.length = 0; await store.close(); diff --git a/packages/storage/test/system-record-managed-mutation-admission-v1.test.ts b/packages/storage/test/system-record-managed-mutation-admission-v1.test.ts index ab54de6cb..97424ad14 100644 --- a/packages/storage/test/system-record-managed-mutation-admission-v1.test.ts +++ b/packages/storage/test/system-record-managed-mutation-admission-v1.test.ts @@ -10,7 +10,6 @@ import { } from '../src/managed-oxigraph-ownership-v1-internal.js'; import { __resetSystemRecordControllerRegistrationForTests } from '../src/system-record-materializer-v1.js'; import { externalStorePriorityScheduler } from '../src/store-priority-scheduler.js'; -import { resolveOwnedSystemRecordVerifiedReplacementRuntimeV1 } from '../src/system-record-verified-replacement-v1-internal.js'; let QUERY_ENDPOINT: string; let UPDATE_ENDPOINT: string; @@ -116,9 +115,7 @@ describe('managed Oxigraph mutation admission V1', () => { async function activate(): Promise { const controller = store.getSystemRecordLaneControllerV1?.(); expect(controller).toBeDefined(); - await controller!.open(resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( - ownership.lease, - ).activationIssuer.issue({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow' })); + await controller!.open({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow' }); } function holdAgentsExclusive(): { @@ -334,13 +331,11 @@ describe('managed Oxigraph mutation admission V1', () => { it('restores the zero-metadata scheduler fast path after a successful disable', async () => { const controller = store.getSystemRecordLaneControllerV1?.(); expect(controller).toBeDefined(); - const session = await controller!.open(resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( - ownership.lease, - ).activationIssuer.issue({ + const session = await controller!.open({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow', - })); + }); await session.close('disable'); const before = externalStorePriorityScheduler.snapshot; diff --git a/packages/storage/test/system-record-materialization-epoch-adapter-v1.test.ts b/packages/storage/test/system-record-materialization-epoch-adapter-v1.test.ts index 589eb299a..9190f13a4 100644 --- a/packages/storage/test/system-record-materialization-epoch-adapter-v1.test.ts +++ b/packages/storage/test/system-record-materialization-epoch-adapter-v1.test.ts @@ -7,7 +7,6 @@ import { createManagedOxigraphOwnershipControllerV1, } from '../src/managed-oxigraph-ownership-v1-internal.js'; import { __resetSystemRecordControllerRegistrationForTests } from '../src/system-record-materializer-v1.js'; -import { resolveOwnedSystemRecordVerifiedReplacementRuntimeV1 } from '../src/system-record-verified-replacement-v1-internal.js'; let server: Server; let queryEndpoint: string; @@ -79,15 +78,12 @@ describe('sparql-http managed epoch handoff', () => { const store = new SparqlHttpStore(options); const controller = store.getSystemRecordLaneControllerV1(); expect(controller).toBeDefined(); - const activation = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( - ownership.lease, - ).activationIssuer.issue({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow' }); - const first = await controller!.open(activation); + const first = await controller!.open({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow' }); expect(epoch).toBe('1'); await first.close('disable'); expect(epoch).toBe('2'); - const second = await controller!.open(activation); + const second = await controller!.open({ networkId: 'testnet', kinds: ['agents'], mode: 'shadow' }); expect(epoch).toBe('3'); expect(requests.map((request) => request.path)).toEqual([ '/query', '/update', '/query', diff --git a/packages/storage/test/system-record-materializer-lifecycle-v1.test.ts b/packages/storage/test/system-record-materializer-lifecycle-v1.test.ts index e52b9401a..c83b935c1 100644 --- a/packages/storage/test/system-record-materializer-lifecycle-v1.test.ts +++ b/packages/storage/test/system-record-materializer-lifecycle-v1.test.ts @@ -540,6 +540,32 @@ describe('system-record lane session lifecycle V1', () => { expect(handoff.calls).toEqual([]); }); + it('rejects proxied activation records and kinds before invoking traps', async () => { + const controller = build(); + let objectTrapCalls = 0; + const activation = new Proxy({ ...ACTIVATION }, { + getPrototypeOf: () => { + objectTrapCalls += 1; + throw new Error('activation proxy trap ran'); + }, + }); + await expect(controller.open(activation)).rejects.toThrow(/plain data object/); + expect(objectTrapCalls).toBe(0); + + let kindsTrapCalls = 0; + const kinds = new Proxy(['agents'], { + ownKeys: () => { + kindsTrapCalls += 1; + throw new Error('kinds proxy trap ran'); + }, + }); + await expect(controller.open({ ...ACTIVATION, kinds } as never)).rejects.toThrow( + /closed \[agents\] tuple/, + ); + expect(kindsTrapCalls).toBe(0); + expect(handoff.calls).toEqual([]); + }); + it('rejects unknown activation fields and a non-closed kinds tuple', async () => { const controller = build(); await expect(controller.open({ ...ACTIVATION, extra: true } as never)).rejects.toThrow( diff --git a/packages/storage/test/system-record-next-state-v1.test.ts b/packages/storage/test/system-record-next-state-v1.test.ts index 3b376b331..c8f79ca08 100644 --- a/packages/storage/test/system-record-next-state-v1.test.ts +++ b/packages/storage/test/system-record-next-state-v1.test.ts @@ -180,6 +180,8 @@ describe('system-record active next-state derivation', () => { expect(snapshot).toMatchObject({ materializationEpoch: EPOCH, + appliedTupleEpoch: '12', + requiresRematerialization: true, appliedState: { materializationEpoch: '12', stateRevision: '1' }, }); expect(result.nextAppliedState).toMatchObject({ diff --git a/packages/storage/test/system-record-state-snapshot-v1.test.ts b/packages/storage/test/system-record-state-snapshot-v1.test.ts index ee3fcaf80..f1635af60 100644 --- a/packages/storage/test/system-record-state-snapshot-v1.test.ts +++ b/packages/storage/test/system-record-state-snapshot-v1.test.ts @@ -79,6 +79,8 @@ describe('system-record reserved-state snapshot decoder', () => { appliedState: { stableKeyHash: STABLE_KEY, stateRevision: '4' }, capacityState: { revision: '7' }, materializationEpoch: '2', + appliedTupleEpoch: '2', + requiresRematerialization: false, }); if (decoded.state !== 'present') throw new Error('expected present state'); expect(decoded.expectedRootClaimQuads).toEqual(canonical.rootClaims); @@ -101,6 +103,8 @@ describe('system-record reserved-state snapshot decoder', () => { expect(decoded).toMatchObject({ state: 'present', materializationEpoch: '2', + appliedTupleEpoch: '1', + requiresRematerialization: true, appliedState: { materializationEpoch: '1' }, receipt: { materializationEpoch: '1' }, }); @@ -120,6 +124,18 @@ describe('system-record reserved-state snapshot decoder', () => { })).toThrow(/epoch binding/); }); + it('rejects split applied-state and receipt epochs', () => { + const prior = tuple({ appliedState: state({ materializationEpoch: '1' }) }); + const current = tuple(); + + expect(() => decodeSystemRecordAppliedSnapshotV1({ + networkId: NETWORK, + stableKeyHash: STABLE_KEY, + materializationEpoch: '2', + quads: [...prior.record, ...prior.capacity, ...current.epoch, ...current.receipt], + })).toThrow(/epoch binding/); + }); + it('rejects missing, extra, duplicate, malformed, and mismatched-epoch rows', () => { const canonical = tuple(); const all = [...canonical.record, ...canonical.capacity, ...canonical.epoch, ...canonical.receipt]; @@ -167,6 +183,11 @@ describe('system-record reserved-state snapshot decoder', () => { predicate: canonical.capacity[0].predicate, object: canonical.capacity[0].object, }])).toThrow(/fixed canonical RDF schema/); + expect(() => decode([...all, { + ...canonical.epoch[0], + predicate: SYSTEM_RECORD_V1_PREDICATES.root, + object: ROOT, + }])).toThrow(/fixed canonical RDF schema/); expect(() => decode(all.map((quad) => quad === canonical.record[0] ? { ...quad, object: '"not-json"^^' } : quad))).toThrow(); diff --git a/packages/storage/test/system-record-verified-replacement-v1.test.ts b/packages/storage/test/system-record-verified-replacement-v1.test.ts index 2dbe7c480..bebfb1c04 100644 --- a/packages/storage/test/system-record-verified-replacement-v1.test.ts +++ b/packages/storage/test/system-record-verified-replacement-v1.test.ts @@ -25,10 +25,10 @@ import { describe, expect, it } from 'vitest'; import { createManagedOxigraphOwnershipControllerV1 } from '../src/managed-oxigraph-ownership-v1-internal.js'; import { createSystemRecordVerifiedReplacementRegistryV1, - resolveOwnedSystemRecordVerifiedReplacementRuntimeV1, type SystemRecordActiveReplacementIssueV1, type SystemRecordVerifiedReplacementLaneBindingV1, } from '../src/system-record-verified-replacement-v1-internal.js'; +import { resolveOwnedSystemRecordRuntimeV1 } from '../src/system-record-runtime-v1-internal.js'; interface Vectors { readonly variants: { @@ -315,35 +315,15 @@ describe('system-record verified replacement V1', () => { firstOwnership.bindReadyGeneration(); secondOwnership.bindReadyGeneration(); - const first = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(firstOwnership.lease); - const firstAgain = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(firstOwnership.lease); - const second = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(secondOwnership.lease); + const first = resolveOwnedSystemRecordRuntimeV1(firstOwnership.lease); + const firstAgain = resolveOwnedSystemRecordRuntimeV1(firstOwnership.lease); + const second = resolveOwnedSystemRecordRuntimeV1(secondOwnership.lease); expect(firstAgain).toBe(first); expect(second).not.toBe(first); - expect(() => resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + expect(() => resolveOwnedSystemRecordRuntimeV1( Object.freeze(Object.create(null) as object) as typeof firstOwnership.lease, )).toThrow(/authentic managed Oxigraph ownership lease/); - const activation = first.activationIssuer.issue({ - networkId: 'testnet', - kinds: ['agents'], - mode: 'shadow', - }); - expect(Object.isFrozen(activation)).toBe(true); - expect(Object.getPrototypeOf(activation)).toBeNull(); - expect(Reflect.ownKeys(activation)).toEqual([]); - expect(first.activationReader.read(activation)).toEqual({ - networkId: 'testnet', - kinds: ['agents'], - mode: 'shadow', - }); - expect(() => second.activationReader.read(activation)).toThrow(/another runtime/); - expect(() => first.activationReader.read({ - networkId: 'testnet', - kinds: ['agents'], - mode: 'shadow', - })).toThrow(/activation capability/); - const { input } = fixture(); const firstHandle = first.issuer.issueActive(input); expect(() => second.issuer.issueActive(input)).toThrow(/reservation is already live/); @@ -358,31 +338,21 @@ describe('system-record verified replacement V1', () => { 'http://127.0.0.1:7880/query', 'http://127.0.0.1:7880/update', ); - const runtime = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(ownership.lease); + const runtime = resolveOwnedSystemRecordRuntimeV1(ownership.lease); const { input } = fixture(); expect(() => runtime.issuer.issueActive(input)).toThrow(/ownership lease is not ready/); - expect(() => runtime.activationIssuer.issue({ - networkId: 'testnet', - kinds: ['agents'], - mode: 'shadow', - })).toThrow(/ownership lease is not ready/); ownership.bindReadyGeneration(); const handle = runtime.issuer.issueActive(input); runtime.consumer.release(handle); ownership.invalidate('shutdown'); expect(() => runtime.issuer.issueActive(input)).toThrow(/ownership lease is not ready/); - expect(() => runtime.activationIssuer.issue({ - networkId: 'testnet', - kinds: ['agents'], - mode: 'shadow', - })).toThrow(/ownership lease is not ready/); }); it('refuses diagnostic leases that do not prove the managed listener endpoints', () => { const diagnostic = createManagedOxigraphOwnershipControllerV1(); diagnostic.bindReadyGeneration(); - expect(() => resolveOwnedSystemRecordVerifiedReplacementRuntimeV1( + expect(() => resolveOwnedSystemRecordRuntimeV1( diagnostic.lease, )).toThrow(/endpoint-bound managed Oxigraph ownership lease/); }); @@ -398,8 +368,8 @@ describe('system-record verified replacement V1', () => { ); firstOwnership.bindReadyGeneration(); secondOwnership.bindReadyGeneration(); - const first = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(firstOwnership.lease); - const second = resolveOwnedSystemRecordVerifiedReplacementRuntimeV1(secondOwnership.lease); + const first = resolveOwnedSystemRecordRuntimeV1(firstOwnership.lease); + const second = resolveOwnedSystemRecordRuntimeV1(secondOwnership.lease); const { input, bindings } = fixture(); const facts = first.consumer.consume(first.issuer.issueActive(input), bindings); const ownership = Object.freeze(Object.create(null) as object); @@ -752,6 +722,6 @@ describe('system-record verified replacement V1', () => { it('is not exported from the storage package barrel', async () => { const storage = await import('../src/index.js'); expect('createSystemRecordVerifiedReplacementRegistryV1' in storage).toBe(false); - expect('resolveOwnedSystemRecordVerifiedReplacementRuntimeV1' in storage).toBe(false); + expect('resolveOwnedSystemRecordRuntimeV1' in storage).toBe(false); }); }); From 10925a70f2307bfe91936c9ebabf9d62c2dcc5b2 Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Fri, 7 Aug 2026 15:43:39 +0200 Subject: [PATCH 4/4] fix(storage): close runtime authority review gaps --- .../system-record-next-state-v1-internal.ts | 4 +- ...tem-record-reservation-gate-v1-internal.ts | 32 ++++++++ .../src/system-record-runtime-v1-internal.ts | 37 +-------- ...ystem-record-state-snapshot-v1-internal.ts | 8 +- ...record-verified-replacement-v1-internal.ts | 34 ++------ .../test/system-record-next-state-v1.test.ts | 1 - .../system-record-state-snapshot-v1.test.ts | 78 ++++++++++++++++--- ...tem-record-verified-replacement-v1.test.ts | 18 +++++ 8 files changed, 132 insertions(+), 80 deletions(-) create mode 100644 packages/storage/src/system-record-reservation-gate-v1-internal.ts diff --git a/packages/storage/src/system-record-next-state-v1-internal.ts b/packages/storage/src/system-record-next-state-v1-internal.ts index 779869bc1..da44c0754 100644 --- a/packages/storage/src/system-record-next-state-v1-internal.ts +++ b/packages/storage/src/system-record-next-state-v1-internal.ts @@ -51,6 +51,7 @@ import { import { assertAuthenticSystemRecordAppliedSnapshotV1, assertSystemRecordRootClaimSnapshotV1, + requiresSystemRecordSnapshotRematerializationV1, type SystemRecordAppliedSnapshotV1, } from './system-record-state-snapshot-v1-internal.js'; import { @@ -231,7 +232,8 @@ export function deriveSystemRecordActiveReplacementV1(input: { ? snapshot.ownedSubjectTable : Object.freeze([]) as OwnedSubjectTableObjectV1; - if (authority.equalHead && !(snapshot.state === 'present' && snapshot.requiresRematerialization)) { + if (authority.equalHead && !(snapshot.state === 'present' + && requiresSystemRecordSnapshotRematerializationV1(snapshot))) { if (snapshot.state !== 'present') { throw new Error('equal system-record head cannot exist in absent state'); } diff --git a/packages/storage/src/system-record-reservation-gate-v1-internal.ts b/packages/storage/src/system-record-reservation-gate-v1-internal.ts new file mode 100644 index 000000000..068aa2fab --- /dev/null +++ b/packages/storage/src/system-record-reservation-gate-v1-internal.ts @@ -0,0 +1,32 @@ +import { + SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES, +} from '@origintrail-official/dkg-core/system-record-v1'; + +export interface SystemRecordRuntimeReservationGateV1 { + acquire(owner: object, bytes: number): void; + release(owner: object, bytes: number): void; +} + +/** One exact, nonqueued reservation with no partial-release state. */ +export function createSystemRecordNonQueuedReservationGateV1(): SystemRecordRuntimeReservationGateV1 { + let liveOwner: object | null = null; + let accountedBytes = 0; + return Object.freeze({ + acquire(owner: object, bytes: number): void { + if (!Number.isSafeInteger(bytes) || bytes <= 0 + || liveOwner !== null + || accountedBytes + bytes > SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES) { + throw new Error('system-record atomic transient reservation is already live'); + } + liveOwner = owner; + accountedBytes += bytes; + }, + release(owner: object, bytes: number): void { + if (liveOwner !== owner || accountedBytes !== bytes) { + throw new Error('system-record atomic transient accountant state is inconsistent'); + } + liveOwner = null; + accountedBytes = 0; + }, + }); +} diff --git a/packages/storage/src/system-record-runtime-v1-internal.ts b/packages/storage/src/system-record-runtime-v1-internal.ts index de9f5d86d..b0aedb563 100644 --- a/packages/storage/src/system-record-runtime-v1-internal.ts +++ b/packages/storage/src/system-record-runtime-v1-internal.ts @@ -1,7 +1,3 @@ -import { - SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES, -} from '@origintrail-official/dkg-core/system-record-v1'; - import { isManagedOxigraphOwnershipLeaseV1, readManagedOxigraphOwnershipSnapshotV1, @@ -9,41 +5,12 @@ import { } from './managed-oxigraph-ownership-v1-internal.js'; import { createSystemRecordVerifiedReplacementRegistryForRuntimeV1, - type SystemRecordRuntimeReservationGateV1, type SystemRecordVerifiedReplacementRegistryV1, } from './system-record-verified-replacement-v1-internal.js'; - -interface SystemRecordRuntimeReservationStateV1 { - liveOwner: object | null; - accountedBytes: number; -} +import { createSystemRecordNonQueuedReservationGateV1 } from './system-record-reservation-gate-v1-internal.js'; /** One nonqueued process-wide gate shared by every authentic managed endpoint. */ -const PROCESS_RESERVATION_STATE: SystemRecordRuntimeReservationStateV1 = { - liveOwner: null, - accountedBytes: 0, -}; - -const PROCESS_RESERVATION_GATE: SystemRecordRuntimeReservationGateV1 = Object.freeze({ - acquire(owner: object, bytes: number): void { - if (!Number.isSafeInteger(bytes) || bytes <= 0 - || PROCESS_RESERVATION_STATE.liveOwner !== null - || PROCESS_RESERVATION_STATE.accountedBytes + bytes - > SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES) { - throw new Error('system-record atomic transient reservation is already live'); - } - PROCESS_RESERVATION_STATE.liveOwner = owner; - PROCESS_RESERVATION_STATE.accountedBytes += bytes; - }, - release(owner: object, bytes: number): void { - if (PROCESS_RESERVATION_STATE.liveOwner !== owner - || PROCESS_RESERVATION_STATE.accountedBytes !== bytes) { - throw new Error('system-record atomic transient accountant state is inconsistent'); - } - PROCESS_RESERVATION_STATE.liveOwner = null; - PROCESS_RESERVATION_STATE.accountedBytes = 0; - }, -}); +const PROCESS_RESERVATION_GATE = createSystemRecordNonQueuedReservationGateV1(); const OWNED_RUNTIMES = new WeakMap< ManagedOxigraphOwnershipLeaseV1, diff --git a/packages/storage/src/system-record-state-snapshot-v1-internal.ts b/packages/storage/src/system-record-state-snapshot-v1-internal.ts index 5bcc132e1..204f075c9 100644 --- a/packages/storage/src/system-record-state-snapshot-v1-internal.ts +++ b/packages/storage/src/system-record-state-snapshot-v1-internal.ts @@ -65,7 +65,6 @@ export interface SystemRecordPresentSnapshotV1 { readonly materializationEpoch: string; /** Epoch bound by the exact persisted applied-state/receipt tuple. */ readonly appliedTupleEpoch: string; - readonly requiresRematerialization: boolean; readonly previousReservedQuads: readonly Readonly[]; readonly expectedRootClaimQuads: readonly Readonly[]; readonly requiredAbsentReservedSubjects: readonly string[]; @@ -252,13 +251,18 @@ export function decodeSystemRecordAppliedSnapshotV1(input: { receipt, materializationEpoch: epoch, appliedTupleEpoch: appliedState.materializationEpoch, - requiresRematerialization: appliedState.materializationEpoch !== epoch, previousReservedQuads: expectedFirstRead, expectedRootClaimQuads: canonical.rootClaims, requiredAbsentReservedSubjects: Object.freeze([]), })); } +export function requiresSystemRecordSnapshotRematerializationV1( + snapshot: SystemRecordPresentSnapshotV1, +): boolean { + return snapshot.appliedTupleEpoch !== snapshot.materializationEpoch; +} + export function assertAuthenticSystemRecordAppliedSnapshotV1( value: unknown, ): asserts value is SystemRecordAppliedSnapshotV1 { diff --git a/packages/storage/src/system-record-verified-replacement-v1-internal.ts b/packages/storage/src/system-record-verified-replacement-v1-internal.ts index d5146dea2..b42dbcc52 100644 --- a/packages/storage/src/system-record-verified-replacement-v1-internal.ts +++ b/packages/storage/src/system-record-verified-replacement-v1-internal.ts @@ -28,7 +28,6 @@ import { parseCanonicalOwnedSubjectTableObjectV1, SYSTEM_RECORD_MAX_ATOMIC_TRANSIENT_BYTES, SYSTEM_RECORD_MAX_PROJECTION_BYTES, - SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES, type AgentProfileActiveHeadObjectV1, type AgentProfileVerifiedAuthoritySummaryV1, type NetworkIdV1, @@ -36,6 +35,10 @@ import { } from '@origintrail-official/dkg-core/system-record-v1'; import type { Quad } from './triple-store.js'; +import { + createSystemRecordNonQueuedReservationGateV1, + type SystemRecordRuntimeReservationGateV1, +} from './system-record-reservation-gate-v1-internal.js'; declare const VERIFIED_REPLACEMENT_HANDLE_BRAND: unique symbol; @@ -161,11 +164,6 @@ interface RuntimeReservationV1 { recoveryOwnership?: object; } -export interface SystemRecordRuntimeReservationGateV1 { - acquire(owner: object, bytes: number): void; - release(owner: object, bytes: number): void; -} - export interface SystemRecordVerifiedReplacementRegistryDepsV1 { readonly reservationGate: SystemRecordRuntimeReservationGateV1; readonly assertAvailable?: () => void; @@ -884,29 +882,7 @@ export function createSystemRecordVerifiedReplacementRegistryForRuntimeV1( */ export function createSystemRecordVerifiedReplacementRegistryV1(): SystemRecordVerifiedReplacementRegistryV1 { return createSystemRecordVerifiedReplacementRegistryForRuntimeV1({ - reservationGate: createIsolatedReservationGateV1(), - }); -} - -function createIsolatedReservationGateV1(): SystemRecordRuntimeReservationGateV1 { - let liveOwner: object | null = null; - let accountedBytes = 0; - return Object.freeze({ - acquire(owner: object, bytes: number): void { - if (liveOwner !== null - || accountedBytes + bytes > SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES) { - throw new Error('system-record atomic transient reservation is already live'); - } - liveOwner = owner; - accountedBytes += bytes; - }, - release(owner: object, bytes: number): void { - if (liveOwner !== owner || accountedBytes < bytes) { - throw new Error('system-record atomic transient accountant state is inconsistent'); - } - liveOwner = null; - accountedBytes -= bytes; - }, + reservationGate: createSystemRecordNonQueuedReservationGateV1(), }); } diff --git a/packages/storage/test/system-record-next-state-v1.test.ts b/packages/storage/test/system-record-next-state-v1.test.ts index c8f79ca08..c0e175a67 100644 --- a/packages/storage/test/system-record-next-state-v1.test.ts +++ b/packages/storage/test/system-record-next-state-v1.test.ts @@ -181,7 +181,6 @@ describe('system-record active next-state derivation', () => { expect(snapshot).toMatchObject({ materializationEpoch: EPOCH, appliedTupleEpoch: '12', - requiresRematerialization: true, appliedState: { materializationEpoch: '12', stateRevision: '1' }, }); expect(result.nextAppliedState).toMatchObject({ diff --git a/packages/storage/test/system-record-state-snapshot-v1.test.ts b/packages/storage/test/system-record-state-snapshot-v1.test.ts index f1635af60..eca8d397e 100644 --- a/packages/storage/test/system-record-state-snapshot-v1.test.ts +++ b/packages/storage/test/system-record-state-snapshot-v1.test.ts @@ -1,26 +1,32 @@ import { describe, expect, it } from 'vitest'; +import { escapeSparqlLiteral } from '@origintrail-official/dkg-core'; import { canonicalizeOwnedSubjectTableObjectV1, + canonicalizeSystemRecordMaterializationReceiptV1, computeOwnedSubjectTableDigestV1, computeSystemRecordAccountedBytesV1, computeSystemRecordAppliedStateDigestV1, + computeSystemRecordMaterializationReceiptDigestV1, computeSystemRecordRootClaimSetDigestV1, computeSystemRecordStableKeyHashV1, SYSTEM_RECORD_MAX_ATOMIC_RESERVED_INSPECTION_RESPONSE_BYTES, type OwnedSubjectTableObjectV1, type SystemRecordAppliedStatePresentV1, type SystemRecordCapacityStateV1, + type SystemRecordMaterializationReceiptV1, } from '@origintrail-official/dkg-core/system-record-v1'; import { buildSystemRecordReservedStateQuadsV1, systemRecordRootClaimSubjectV1, + SYSTEM_RECORD_V1_JSON_DATATYPE, SYSTEM_RECORD_V1_PREDICATES, } from '../src/system-record-rdf-schema-v1-internal.js'; import { assertAuthenticSystemRecordAppliedSnapshotV1, assertSystemRecordRootClaimSnapshotV1, decodeSystemRecordAppliedSnapshotV1, + requiresSystemRecordSnapshotRematerializationV1, } from '../src/system-record-state-snapshot-v1-internal.js'; const NETWORK = 'otp:20430' as const; @@ -80,9 +86,9 @@ describe('system-record reserved-state snapshot decoder', () => { capacityState: { revision: '7' }, materializationEpoch: '2', appliedTupleEpoch: '2', - requiresRematerialization: false, }); if (decoded.state !== 'present') throw new Error('expected present state'); + expect(requiresSystemRecordSnapshotRematerializationV1(decoded)).toBe(false); expect(decoded.expectedRootClaimQuads).toEqual(canonical.rootClaims); expect(decoded.previousReservedQuads).toHaveLength(12); expect(Object.isFrozen(decoded)).toBe(true); @@ -104,10 +110,11 @@ describe('system-record reserved-state snapshot decoder', () => { state: 'present', materializationEpoch: '2', appliedTupleEpoch: '1', - requiresRematerialization: true, appliedState: { materializationEpoch: '1' }, receipt: { materializationEpoch: '1' }, }); + if (decoded.state !== 'present') throw new Error('expected present state'); + expect(requiresSystemRecordSnapshotRematerializationV1(decoded)).toBe(true); expect(decoded.previousReservedQuads).toEqual(expect.arrayContaining(currentEpoch)); expect(decoded.previousReservedQuads).not.toEqual(expect.arrayContaining(prior.epoch)); }); @@ -136,6 +143,31 @@ describe('system-record reserved-state snapshot decoder', () => { })).toThrow(/epoch binding/); }); + it('rejects split receipt revision, applied-state digest, and head digest bindings', () => { + const appliedState = state(); + const canonical = tuple({ appliedState }); + const canonicalReceipt = receiptFor(appliedState); + const mismatches = [ + { ...canonicalReceipt, stateRevision: '3' }, + { ...canonicalReceipt, appliedStateDigest: `0x${'cc'.repeat(32)}` }, + { ...canonicalReceipt, headDigest: `0x${'dd'.repeat(32)}` }, + ] as const; + + for (const receipt of mismatches) { + expect(() => decodeSystemRecordAppliedSnapshotV1({ + networkId: NETWORK, + stableKeyHash: STABLE_KEY, + materializationEpoch: '2', + quads: [ + ...canonical.record, + ...canonical.capacity, + ...canonical.epoch, + ...canonicalReceiptQuads(canonical.receipt, receipt), + ], + })).toThrow(/epoch binding/); + } + }); + it('rejects missing, extra, duplicate, malformed, and mismatched-epoch rows', () => { const canonical = tuple(); const all = [...canonical.record, ...canonical.capacity, ...canonical.epoch, ...canonical.receipt]; @@ -388,19 +420,41 @@ function tuple(overrides: { projectionBytes: '8192', projectionQuads: '6', }, - receipt: { - objectType: 'system-record-materialization-receipt', - kind: 'agents', - networkId: NETWORK, - stableKeyHash: STABLE_KEY, - stateRevision: appliedState.stateRevision, - appliedStateDigest, - headDigest: appliedState.headDigest, - materializationEpoch: appliedState.materializationEpoch, - }, + receipt: receiptFor(appliedState, appliedStateDigest), }); } +function receiptFor( + appliedState: SystemRecordAppliedStatePresentV1, + appliedStateDigest = computeSystemRecordAppliedStateDigestV1(appliedState), +): Parameters[0]['receipt'] { + return { + objectType: 'system-record-materialization-receipt', + kind: 'agents', + networkId: NETWORK, + stableKeyHash: STABLE_KEY, + stateRevision: appliedState.stateRevision, + appliedStateDigest, + headDigest: appliedState.headDigest, + materializationEpoch: appliedState.materializationEpoch, + }; +} + +function canonicalReceiptQuads( + template: ReturnType['receipt'], + receipt: SystemRecordMaterializationReceiptV1, +): ReturnType['receipt'] { + const bytes = canonicalizeSystemRecordMaterializationReceiptV1(receipt); + const json = Buffer.from(bytes).toString('utf8'); + const digest = computeSystemRecordMaterializationReceiptDigestV1(receipt); + return Object.freeze(template.map((quad) => Object.freeze({ + ...quad, + object: quad.predicate === SYSTEM_RECORD_V1_PREDICATES.receipt + ? `"${escapeSparqlLiteral(json)}"^^<${SYSTEM_RECORD_V1_JSON_DATATYPE}>` + : `"${digest}"`, + }))); +} + function state( overrides: Partial = {}, ): SystemRecordAppliedStatePresentV1 { diff --git a/packages/storage/test/system-record-verified-replacement-v1.test.ts b/packages/storage/test/system-record-verified-replacement-v1.test.ts index bebfb1c04..033846664 100644 --- a/packages/storage/test/system-record-verified-replacement-v1.test.ts +++ b/packages/storage/test/system-record-verified-replacement-v1.test.ts @@ -15,6 +15,7 @@ import { computeOwnedSubjectTableDigestV1, digestSystemRecordBytesV1, SYSTEM_RECORD_DIGEST_DOMAINS_V1, + SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES, type AgentProfileActiveHeadObjectV1, type AgentProfileVerifiedAuthoritySummaryV1, type NetworkIdV1, @@ -23,6 +24,7 @@ import { import { describe, expect, it } from 'vitest'; import { createManagedOxigraphOwnershipControllerV1 } from '../src/managed-oxigraph-ownership-v1-internal.js'; +import { createSystemRecordNonQueuedReservationGateV1 } from '../src/system-record-reservation-gate-v1-internal.js'; import { createSystemRecordVerifiedReplacementRegistryV1, type SystemRecordActiveReplacementIssueV1, @@ -288,6 +290,22 @@ describe('system-record verified replacement V1', () => { expect(() => registry.consumer.consume(handle, bindings)).toThrow(/already consumed/); }); + it('keeps one exact nonqueued reservation after wrong-owner or partial release attempts', () => { + const gate = createSystemRecordNonQueuedReservationGateV1(); + const firstOwner = Object.freeze(Object.create(null) as object); + const secondOwner = Object.freeze(Object.create(null) as object); + const bytes = SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES; + + gate.acquire(firstOwner, bytes); + expect(() => gate.acquire(secondOwner, 1)).toThrow(/reservation is already live/); + expect(() => gate.release(secondOwner, bytes)).toThrow(/accountant state is inconsistent/); + expect(() => gate.release(firstOwner, bytes - 1)).toThrow(/accountant state is inconsistent/); + expect(() => gate.acquire(secondOwner, 1)).toThrow(/reservation is already live/); + gate.release(firstOwner, bytes); + expect(() => gate.acquire(secondOwner, bytes)).not.toThrow(); + gate.release(secondOwner, bytes); + }); + it('owns one nonqueued atomic reservation and releases handle or facts exactly once', () => { const registry = createSystemRecordVerifiedReplacementRegistryV1(); const { input, bindings } = fixture();