Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions packages/storage/src/adapters/sparql-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
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 { 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';
Expand Down Expand Up @@ -706,14 +706,14 @@
// already carries the property.
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Adapter wiring is not verified with a lease-issued proof

What's wrong
The PR's key storage-boundary behavior is that the adapter receives the same lease-owned runtime consumer that the verifier issuer will use. Current tests exercise the runtime directly and only check that the adapter rejects a forged object, which would also pass if the adapter were wired to the wrong private registry. That leaves the changed integration contract unverified.

Example
A regression that accidentally changed the adapter back to a fresh private registry would still reject Object.freeze({}) and the direct runtime tests would still pass, but a handle issued by resolveOwnedSystemRecordRuntimeV1(ownership.lease).issuer would fail through session.applyVerified(handle) instead of being consumed by the adapter's atomic executor.

Suggested direction
Cover the production composition, not only the resolver in isolation: the test should prove the store consumes handles from the same lease-bound runtime it resolves here.

Confidence note
I found direct tests for the lease runtime and for the atomic executor, plus an adapter test that only sends a forged object. I did not find an adapter test that sends a valid handle issued from the lease-bound runtime.

For Agents
Add an adapter-level regression test around SparqlHttpStore.getSystemRecordLaneControllerV1: create a managed lease, resolve its runtime, issue a valid active replacement handle with the same lifecycle binding, pass it to an open session's applyVerified, and assert the path reaches the atomic executor behavior rather than returning capability-lost for an unrecognized registry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated as a missing integration-evidence case rather than an identified runtime defect. Added the exact lease-issued-handle-through-adapter regression to #2154; the current direct runtime, adapter rejection, atomic executor, and live ownership gates remain green.

const { consumer } = resolveOwnedSystemRecordRuntimeV1(
this.ownershipLease,
);
const atomicExecutor = createSystemRecordAtomicApplyExecutorV1({
consumer,
storeId: this,
Expand Down Expand Up @@ -1385,7 +1385,7 @@
async countQuads(graphUri?: string, options?: QueryOptions): Promise<number> {
const sparql = graphUri
? `SELECT (COUNT(*) AS ?c) WHERE { GRAPH <${escapeUri(graphUri)}> { ?s ?p ?o } }`
: `SELECT (COUNT(*) AS ?c) WHERE { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } }`;

Check notice on line 1388 in packages/storage/src/adapters/sparql-http.ts

View workflow job for this annotation

GitHub Actions / SPARQL scalability lint

sparql-scale-lint R2 graph-var-scan

All-variable triple inside GRAPH ?var enumerates every graph × every triple (the #1597 listGraphs-storm shape). Bind the graph (VALUES/exact IRI), bind a term, or use a FILTER EXISTS existence probe. [pre-existing (grandfathered; fix when touched)] To acknowledge: "sparql-scan-allow: R2 -- <why this is bounded>"

Check notice on line 1388 in packages/storage/src/adapters/sparql-http.ts

View workflow job for this annotation

GitHub Actions / SPARQL scalability lint

sparql-scale-lint R1 unscoped-all-var-scan

All-variable triple pattern with no graph scope scans the ENTIRE store. Scope it to an exact named graph, bind at least one term, or add LIMIT (without ORDER BY). [pre-existing (grandfathered; fix when touched)] To acknowledge: "sparql-scan-allow: R1 -- <why this is bounded>"
const r = await this.query(sparql, {
...options,
source: options?.source ?? 'sparql-http.countQuads',
Expand Down
7 changes: 5 additions & 2 deletions packages/storage/src/system-record-materializer-v1.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { types as utilTypes } from 'node:util';

import {
readManagedOxigraphOwnershipSnapshotV1,
type ManagedOxigraphOwnershipLeaseV1,
Expand Down Expand Up @@ -263,12 +265,13 @@ 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. */
/** 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');
Expand Down Expand Up @@ -303,7 +306,7 @@ const snapshotActivation = (activation: unknown): SystemRecordLaneActivationSnap
}

const kinds = readDataField('kinds');
if (!Array.isArray(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);
Expand Down
4 changes: 3 additions & 1 deletion packages/storage/src/system-record-next-state-v1-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
import {
assertAuthenticSystemRecordAppliedSnapshotV1,
assertSystemRecordRootClaimSnapshotV1,
requiresSystemRecordSnapshotRematerializationV1,
type SystemRecordAppliedSnapshotV1,
} from './system-record-state-snapshot-v1-internal.js';
import {
Expand Down Expand Up @@ -231,7 +232,8 @@ export function deriveSystemRecordActiveReplacementV1(input: {
? snapshot.ownedSubjectTable
: Object.freeze([]) as OwnedSubjectTableObjectV1;

if (authority.equalHead) {
if (authority.equalHead && !(snapshot.state === 'present'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Model rematerialization before the equal-head branch instead of negating it at the call site

What's wrong
The change threads rematerialization through the already-applied path as a special-case exception, so equalHead no longer means what its name suggests unless the reader also knows about the snapshot epoch helper. This spreads one state-machine decision across two modules and makes future changes to the equal-head path easier to get wrong or duplicate.

Example
For a present snapshot from an older applied tuple epoch, classifyAuthorityAdvance still returns { outcome: 'advance', equalHead: true }; the caller then has to reinterpret that as not actually reusable via !(snapshot.state === 'present' && requiresSystemRecordSnapshotRematerializationV1(snapshot)).

Suggested direction
Fold epoch freshness into classifyAuthorityAdvance, return a richer outcome such as already-applied versus advance, or make the snapshot model distinguish reusable present state from present-but-needs-rematerialization. That would let this call site stay direct and remove the double-negative special case.

For Agents
Refactor around classifyAuthorityAdvance and the present-snapshot epoch model. Preserve behavior for equal current-epoch heads and prior-epoch rematerialization, but make the classifier or snapshot type express whether the persisted tuple is reusable. Keep the existing rematerialization test proving a prior-epoch equal head advances to a fresh tuple.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Fold rematerialization into the authority classification instead of special-casing equal heads

What's wrong
The new condition bolts an epoch-rematerialization exception onto an already busy control path. Readers now need to understand that equalHead does not mean “already applied”; it means “same head, unless the snapshot tuple is from an older durable epoch.” That weakens the abstraction boundary between snapshot freshness and authority advancement and makes future tuple states likely to add more compound exceptions here.

Example
For a present snapshot with the same head but appliedTupleEpoch !== materializationEpoch, classifyAuthorityAdvance reports equalHead: true; line 235 then says this equal head must not take the already-applied path because it needs rematerialization. That behavior may be right, but the model now says both “equal head” and “not reusable” in separate places.

Suggested direction
Have the classifier or snapshot model expose the actual decision the caller needs, such as equalCurrentHead, rematerializeEqualHead, or canReusePersistedMaterialization, instead of returning equalHead and then negating it with a separate epoch freshness check at the call site.

For Agents
Look at classifyAuthorityAdvance, requiresSystemRecordSnapshotRematerializationV1, and the already-applied branch in deriveSystemRecordActiveReplacementV1. Preserve the prior-epoch rematerialization behavior, but move the freshness decision into the authority/snapshot classification so the main derivation branches on a direct state like alreadyApplied vs rematerializeEqualHead. Keep the existing prior-epoch next-state test passing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated as a maintainability refinement, not a correctness blocker for this default-off runtime foundation. Deferred explicitly to #2155, which now requires the authority decision to expose a direct reusable/rematerialize outcome before activation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Make equal-head rematerialization an explicit state, not a hidden exception

What's wrong
This adds a special-case conditional into the busiest part of the derivation function. The reader has to know that equalHead sometimes means already-applied and sometimes means rematerialize, depending on epoch state owned by a different module. That is exactly the kind of branching growth that will make future transition cases harder to place correctly.

Example
Equal head at the current epoch enters the already-applied branch; equal head from a prior tuple epoch falls through into the normal advance/rematerialization path. That distinction is real, but it is hidden in a compound condition instead of being named by the authority/snapshot classification.

Suggested direction
Reframe the branch so the top-level flow dispatches on a named derivation state rather than equalHead && !(present && requiresRematerialization(...)).

For Agents
In system-record-next-state-v1-internal.ts, move the prior-epoch equal-head classification into a named model: for example have classifyAuthorityAdvance return advanceKind: 'new-head' | 'rematerialize-equal-head' | 'already-applied-candidate', or compute a clearly named shouldRematerializeEqualHead before branching. Preserve the current already-applied and rematerialization behavior, and keep the existing tests for equal-head prior epoch/current epoch passing.

&& requiresSystemRecordSnapshotRematerializationV1(snapshot))) {
if (snapshot.state !== 'present') {
throw new Error('equal system-record head cannot exist in absent state');
}
Expand Down
32 changes: 32 additions & 0 deletions packages/storage/src/system-record-reservation-gate-v1-internal.ts
Original file line number Diff line number Diff line change
@@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Make the reservation gate own the lease token instead of accepting release metadata

What's wrong
The new gate is supposed to encapsulate runtime accounting, but its interface pushes the exact release identity and byte count back onto the caller. That keeps wrong-owner and partial-release states in the contract, then compensates with runtime checks. A smaller, more maintainable abstraction would make those combinations impossible and remove bookkeeping from the registry caller.

Example
Current shape: the registry manufactures an owner object, calls reservationGate.acquire(reservation.identity, reservation.bytes), then must later repeat the exact pair in reservationGate.release(reservation.identity, reservation.bytes). The new tests explicitly exercise wrong-owner and partial-release attempts, which is a sign the abstraction is exposing invalid states instead of making them unrepresentable.

Suggested direction
Change the gate API from acquire(owner, bytes) / release(owner, bytes) to an opaque lease/token model, for example const token = gate.acquire(bytes); gate.release(token). The registry can still expose a separate opaque reservationIdentity in facts if needed, but the accountant should not require callers to reconstruct its internal release key.

For Agents
Look at system-record-reservation-gate-v1-internal.ts and the reservation calls in system-record-verified-replacement-v1-internal.ts. Preserve the single nonqueued process-wide/accounted-byte behavior, but have acquire return an opaque reservation/token that captures owner/bytes internally, and release by that token or by a token method. Existing reservation lifecycle tests should still prove exact-once release and cross-runtime exclusion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated as a worthwhile API hardening refactor, not a behavioral blocker for the default-off foundation. Deferred explicitly to #2155, which now requires an opaque gate-owned reservation token while preserving exact-once release and process-wide exclusion.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: Return an opaque reservation token instead of exposing owner/byte bookkeeping

What's wrong
The new abstraction is too thin: it extracts accounting state into a separate module but leaves the validity protocol spread across both sides. That makes the boundary more fragile than the original local state because the type system cannot express “release exactly the reservation that was acquired.”

Example
gate.acquire(firstOwner, bytes); gate.release(firstOwner, bytes - 1) is representable through the public internal interface and only fails dynamically. The caller should not need to remember the exact owner/byte pair to release a reservation it just acquired.

Suggested direction
Let the gate own its reservation record and expose a token-based API, or collapse the gate back into the registry if there is only one meaningful caller. Either option removes the primitive protocol between modules.

For Agents
In system-record-reservation-gate-v1-internal.ts and system-record-verified-replacement-v1-internal.ts, make acquire(bytes) return an opaque/branded reservation capability that the gate owns, and release by that token. Keep the one-live-reservation and max-accounted-bytes behavior, but remove the raw owner/bytes round trip from the registry/gate boundary.

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;
},
});
}
50 changes: 50 additions & 0 deletions packages/storage/src/system-record-runtime-v1-internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
isManagedOxigraphOwnershipLeaseV1,
readManagedOxigraphOwnershipSnapshotV1,
type ManagedOxigraphOwnershipLeaseV1,
} from './managed-oxigraph-ownership-v1-internal.js';
import {
createSystemRecordVerifiedReplacementRegistryForRuntimeV1,
type SystemRecordVerifiedReplacementRegistryV1,
} from './system-record-verified-replacement-v1-internal.js';
import { createSystemRecordNonQueuedReservationGateV1 } from './system-record-reservation-gate-v1-internal.js';

/** One nonqueued process-wide gate shared by every authentic managed endpoint. */
const PROCESS_RESERVATION_GATE = createSystemRecordNonQueuedReservationGateV1();

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;
}
30 changes: 23 additions & 7 deletions packages/storage/src/system-record-state-snapshot-v1-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ 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 previousReservedQuads: readonly Readonly<Quad>[];
readonly expectedRootClaimQuads: readonly Readonly<Quad>[];
readonly requiredAbsentReservedSubjects: readonly string[];
Expand Down Expand Up @@ -110,6 +113,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);

Expand All @@ -119,9 +125,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({
Expand Down Expand Up @@ -171,9 +175,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,
Expand All @@ -191,8 +196,12 @@ 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
Comment thread
Jurij89 marked this conversation as resolved.
|| 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');
}
const canonicalTableBytes = canonicalizeOwnedSubjectTableObjectV1(
Expand Down Expand Up @@ -228,7 +237,7 @@ export function decodeSystemRecordAppliedSnapshotV1(input: {
const expectedFirstRead = Object.freeze([
...canonical.record,
...canonical.capacity,
...canonical.epoch,
...canonicalEpochRows,
...canonical.receipt,
]);
assertExactQuadSet(quads, expectedFirstRead, 'reserved state');
Expand All @@ -241,12 +250,19 @@ export function decodeSystemRecordAppliedSnapshotV1(input: {
capacityState: capacity,
receipt,
materializationEpoch: epoch,
appliedTupleEpoch: appliedState.materializationEpoch,
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ 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,
type OwnedSubjectTableObjectV1,
} 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;

Expand Down Expand Up @@ -161,6 +164,11 @@ interface RuntimeReservationV1 {
recoveryOwnership?: object;
}

export interface SystemRecordVerifiedReplacementRegistryDepsV1 {
readonly reservationGate: SystemRecordRuntimeReservationGateV1;
readonly assertAvailable?: () => void;
}

/** Module-private and non-enumerable by construction. Handle identity is the only lookup key. */
const REGISTERED_REPLACEMENTS = new WeakMap<object, RegisteredReplacementV1>();
const AUTHENTIC_VERIFIED_REPLACEMENT_FACTS = new WeakSet<object>();
Expand Down Expand Up @@ -563,20 +571,17 @@ 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 {
export function createSystemRecordVerifiedReplacementRegistryForRuntimeV1(
deps: SystemRecordVerifiedReplacementRegistryDepsV1,
): SystemRecordVerifiedReplacementRegistryV1 {
const registryIdentity = Object.freeze(Object.create(null) as object);
let accountedBytes = 0;
let liveAtomicReservation: RuntimeReservationV1 | null = null;
const { reservationGate } = deps;

const reserveAtomic = (
admittedDeadlineMs: number,
decodedBytes: number,
): RuntimeReservationV1 => {
if (liveAtomicReservation !== null
|| accountedBytes + SYSTEM_RECORD_MAX_ATOMIC_TRANSIENT_BYTES
> SYSTEM_RECORD_MAX_RUNTIME_ACCOUNTED_BYTES) {
throw new Error('system-record atomic transient reservation is already live');
}
deps.assertAvailable?.();
const reservation: RuntimeReservationV1 = {
registryIdentity,
identity: Object.freeze(Object.create(null) as object),
Expand All @@ -590,26 +595,21 @@ export function createSystemRecordVerifiedReplacementRegistryV1(): SystemRecordV
},
phase: 'proof',
};
accountedBytes += reservation.bytes;
liveAtomicReservation = reservation;
reservationGate.acquire(reservation.identity, reservation.bytes);
return reservation;
};

const releaseReservation = (reservation: RuntimeReservationV1): void => {
if (reservation.registryIdentity !== registryIdentity || reservation.phase === 'released') {
throw new Error('system-record atomic transient reservation was already released');
}
if (liveAtomicReservation !== reservation || 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;
accountedBytes -= reservation.bytes;
liveAtomicReservation = null;
};

const registeredHandle = (handle: unknown): RegisteredReplacementV1 => {
Expand Down Expand Up @@ -875,6 +875,17 @@ export function createSystemRecordVerifiedReplacementRegistryV1(): SystemRecordV
return Object.freeze({ issuer, consumer });
}

/**
* 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 createSystemRecordVerifiedReplacementRegistryForRuntimeV1({
reservationGate: createSystemRecordNonQueuedReservationGateV1(),
});
}

function retainedVerifiedFactsBytes(
head: AgentProfileActiveHeadObjectV1,
authority: AgentProfileVerifiedAuthoritySummaryV1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,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(
Expand Down
Loading
Loading