Follow-up from the review of #2243. This must land together with the storage export gate, not before it — see the sequencing section, which is the reason it was not taken in #2243.
The finding
packages/agent/src/system-records/agent-profile-materializer-bridge-v1-internal.ts declares AgentProfileMaterializerCandidateIssueV1, a structural mirror of storage's SystemRecordVerifiedCandidateIssueV1, and passes it through unknown to runtime.issuer.issueCandidate. The two shapes are kept in agreement by hand.
The consequence is real and this stack has a worked example of it: if storage adds a required field to the candidate issue shape, the agent-side mirror keeps compiling. Detection is runtime-only, via exactRecord's key throw, and only at the moment the two are actually wired — which is D7. conflictForkBaseHeadDigest (added by S2 in this same stack) is exactly such a field.
Ruling: valid, and the remedy is a narrow type-only export
Export the candidate-issue shape from storage — SystemRecordVerifiedCandidateIssueV1 plus the three types its union references (SystemRecordActiveReplacementIssueV1, SystemRecordTombstoneReplacementIssueV1, SystemRecordQuarantineReplacementIssueV1), none of which are currently in the barrel. Not the issuer, not the registry, not a factory. Then have the bridge import it and pin its local mirror against it, so drift fails the agent build at the construction site.
Two suggested alternatives were considered and declined: sharing the issuer type wholesale (breaches the capability boundary) and a storage-owned adapter accepting receiver candidates (inverts the dependency so storage imports agent types — strictly worse than the current direction).
Why this does not breach the isolation contract
D5a's isolation protects capability — who may mint, issue, or construct a proof — not shape. A type-only export grants no capability: it cannot construct anything, cannot reach the private registry, and erases at runtime.
That is not an assertion; it is what the pinning test actually says. packages/storage/test/system-record-verified-replacement-v1.test.ts:886, 'is not exported from the storage package barrel', asserts exactly two things:
expect('createSystemRecordVerifiedReplacementRegistryV1' in storage).toBe(false);
expect('resolveOwnedSystemRecordRuntimeV1' in storage).toBe(false);
Both are runtime factories. Nothing about types, and a type-only export cannot make an in check true. There is also existing precedent with sign-off: the bridge already does import type { SystemRecordApplyOutcomeV1, SystemRecordLaneSessionV1 } from '@origintrail-official/dkg-storage', and SystemRecordLaneSessionV1 is a type-only export of a privileged surface — the bridge calls session.applyVerified(proof) through it.
Preserve this in the code: after the change the issuer, the registry, the proof factory and the bridge factory all stay private, and nothing outside storage gains the ability to mint or transfer a proof. A reader six months out should not have to re-derive why exporting this one type was safe when the surrounding privacy is deliberate.
Sequencing — the reason this is filed rather than taken
An unproven enforcement is worth less than the guard it replaces. This converts a runtime refusal — the exactRecord throw, which is fail-closed and cannot be silently deleted because it is load-bearing code — into a compile-time check. A compile-time check that ships without an export gate can be silently dropped: any later barrel cleanup removes the export, compile-time detection quietly vanishes, and everyone keeps believing it is there.
At #2243's base, storage has no package-export pin at all. The gate (scripts/pack-gate/barrel-value-exports.json, probe-fixture.mjs, type-fixture.ts, verify-pack-exports.mjs, wired as test:package-exports) is being built by the storage exports-map work on chore/2165-storage-exports-map (issue #2165, now closed; the branch is unmerged, 20 ahead / 18 behind the integration base). Landing this export before that manifest exists means it arrives ungoverned, and that work would have to retro-adjudicate a public surface it did not add.
It also races directly: that branch changes exactly the two files this needs to touch — packages/storage/package.json (+14/-0) and packages/storage/src/index.ts (+13/-16, a deliberate narrowing of the barrel).
Onto the exports-map work it lands with its gate; onto #2243 it would land as a claim.
One framing worth keeping: this is an upgrade to detection latency, not a repair of a hole. Nothing is unguarded today — exactRecord refuses at runtime. That is why it does not earn a place in a converging stack, and why it should not be deferred indefinitely either: the hazard activates when D7 wires the bridge, so it must land before that wiring.
Acceptance
- The four types are exported type-only from the storage barrel; no new runtime value appears.
- The bridge imports the storage type and pins its local mirror against it, so a required field added in storage fails the agent build at the construction site.
- The barrel-privacy test above still passes unchanged.
- The export appears in the export gate's manifest, so removing it later fails the gate rather than silently disabling the check.
Related: #2052 (umbrella), #2243, #2165, #2244, #2245, #2246, #2247.
Follow-up from the review of #2243. This must land together with the storage export gate, not before it — see the sequencing section, which is the reason it was not taken in #2243.
The finding
packages/agent/src/system-records/agent-profile-materializer-bridge-v1-internal.tsdeclaresAgentProfileMaterializerCandidateIssueV1, a structural mirror of storage'sSystemRecordVerifiedCandidateIssueV1, and passes it throughunknowntoruntime.issuer.issueCandidate. The two shapes are kept in agreement by hand.The consequence is real and this stack has a worked example of it: if storage adds a required field to the candidate issue shape, the agent-side mirror keeps compiling. Detection is runtime-only, via
exactRecord's key throw, and only at the moment the two are actually wired — which is D7.conflictForkBaseHeadDigest(added by S2 in this same stack) is exactly such a field.Ruling: valid, and the remedy is a narrow type-only export
Export the candidate-issue shape from storage —
SystemRecordVerifiedCandidateIssueV1plus the three types its union references (SystemRecordActiveReplacementIssueV1,SystemRecordTombstoneReplacementIssueV1,SystemRecordQuarantineReplacementIssueV1), none of which are currently in the barrel. Not the issuer, not the registry, not a factory. Then have the bridge import it and pin its local mirror against it, so drift fails the agent build at the construction site.Two suggested alternatives were considered and declined: sharing the issuer type wholesale (breaches the capability boundary) and a storage-owned adapter accepting receiver candidates (inverts the dependency so storage imports agent types — strictly worse than the current direction).
Why this does not breach the isolation contract
D5a's isolation protects capability — who may mint, issue, or construct a proof — not shape. A type-only export grants no capability: it cannot construct anything, cannot reach the private registry, and erases at runtime.
That is not an assertion; it is what the pinning test actually says.
packages/storage/test/system-record-verified-replacement-v1.test.ts:886,'is not exported from the storage package barrel', asserts exactly two things:Both are runtime factories. Nothing about types, and a type-only export cannot make an
incheck true. There is also existing precedent with sign-off: the bridge already doesimport type { SystemRecordApplyOutcomeV1, SystemRecordLaneSessionV1 } from '@origintrail-official/dkg-storage', andSystemRecordLaneSessionV1is a type-only export of a privileged surface — the bridge callssession.applyVerified(proof)through it.Preserve this in the code: after the change the issuer, the registry, the proof factory and the bridge factory all stay private, and nothing outside storage gains the ability to mint or transfer a proof. A reader six months out should not have to re-derive why exporting this one type was safe when the surrounding privacy is deliberate.
Sequencing — the reason this is filed rather than taken
An unproven enforcement is worth less than the guard it replaces. This converts a runtime refusal — the
exactRecordthrow, which is fail-closed and cannot be silently deleted because it is load-bearing code — into a compile-time check. A compile-time check that ships without an export gate can be silently dropped: any later barrel cleanup removes the export, compile-time detection quietly vanishes, and everyone keeps believing it is there.At #2243's base, storage has no package-export pin at all. The gate (
scripts/pack-gate/barrel-value-exports.json,probe-fixture.mjs,type-fixture.ts,verify-pack-exports.mjs, wired astest:package-exports) is being built by the storage exports-map work onchore/2165-storage-exports-map(issue #2165, now closed; the branch is unmerged, 20 ahead / 18 behind the integration base). Landing this export before that manifest exists means it arrives ungoverned, and that work would have to retro-adjudicate a public surface it did not add.It also races directly: that branch changes exactly the two files this needs to touch —
packages/storage/package.json(+14/-0) andpackages/storage/src/index.ts(+13/-16, a deliberate narrowing of the barrel).Onto the exports-map work it lands with its gate; onto #2243 it would land as a claim.
One framing worth keeping: this is an upgrade to detection latency, not a repair of a hole. Nothing is unguarded today —
exactRecordrefuses at runtime. That is why it does not earn a place in a converging stack, and why it should not be deferred indefinitely either: the hazard activates when D7 wires the bridge, so it must land before that wiring.Acceptance
Related: #2052 (umbrella), #2243, #2165, #2244, #2245, #2246, #2247.