Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
004cccb
fix(agent): repair durable VM metadata from canonical receipt
Aug 9, 2026
513eacd
fix(agent): authenticate receipt-backed metadata repair
Aug 9, 2026
d8c57cf
fix(agent): repair public VM metadata from chain truth
Aug 9, 2026
adfbb85
fix(agent): promote exact public SWM from chain inventory
Aug 9, 2026
1c8ba22
refactor(agent): centralize trusted control recovery
Aug 9, 2026
19edcec
fix(agent): make public VM recovery fail closed
Aug 9, 2026
eac18d3
test(agent): cover receiptless public KA updates
Aug 9, 2026
c302466
fix(agent): fail closed in receipt recovery
Aug 9, 2026
9e8b34f
refactor(publisher): type trusted control writes
Aug 9, 2026
8f9592d
refactor(publisher): make typed controls canonical
Aug 9, 2026
a4e4b4e
refactor(test): extract graph reconcile fixtures
Aug 9, 2026
3aa4fe3
fix(agent): reconcile selected RFC64 public VM
Aug 9, 2026
e6b5872
Merge remote-tracking branch 'origin/codex/10.0.14-cg298-binding-hotf…
Aug 9, 2026
d515bc3
Merge branch 'codex/10.0.14-cg298-binding-hotfix' of https://github.c…
Aug 9, 2026
7a14940
test(agent): cover selected VM reconcile lifecycle
Aug 9, 2026
5d4f443
refactor(agent): unify public finalized materialization
Aug 9, 2026
8798952
fix(agent): validate selected VM cursor binding
Aug 9, 2026
4a99e41
Merge branch 'codex/10.0.14-cg298-binding-hotfix' of https://github.c…
Aug 9, 2026
6e8c1b1
feat(agent): gate startup subscription rehydration
Aug 9, 2026
1196a91
feat(cli): expose effective sync lifecycle gate
Aug 9, 2026
56960f3
test(agent): use composed VM reconcile fixture
Aug 9, 2026
c34a70e
fix(agent): fence selected VM cursors by deployment
Aug 9, 2026
7807c95
fix(agent): resolve chain discovery name hashes
Aug 9, 2026
e69ea72
fix(agent): preserve selected VM binding failures
Aug 9, 2026
e39dbc8
refactor(agent): keep VM cleanup out of binding state
Aug 9, 2026
50bab7d
test(cli): cover daemon subscription rehydration gate
Aug 9, 2026
93a1a3d
refactor(chain): simplify name-hash resolver layers
Aug 9, 2026
5fe8370
test(node-ui): expect deployment cursor schema
Aug 9, 2026
fcb65ed
test(cli): expect current deployment cursor schema
Aug 9, 2026
167c992
refactor(agent): isolate selected VM cursor storage
Aug 10, 2026
0b263b5
fix(agent): harden subscription and CG discovery boundaries
Aug 10, 2026
dc73c76
Merge remote-tracking branch 'origin/testnet-canary' into codex/vm-me…
Aug 10, 2026
8402782
refactor(agent): normalize discovered context graph bindings
Aug 10, 2026
9f43c0d
fix(agent): fence selected VM reconcile lifecycle
Aug 10, 2026
4f3eb80
refactor(chain): own public finalization authority fence
Aug 10, 2026
26ecbd6
fix: verify graph-scoped update receipts during VM repair
Aug 10, 2026
229590c
test: cover selected VM cursor V31 upgrade
Aug 10, 2026
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
117 changes: 93 additions & 24 deletions packages/agent/src/finalization-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import {
type ParsedGraphScopedFinalization,
type VerifiedGraphScopedFinalizationEvidence,
} from './finalization-graph-envelope.js';
import { recoverReceiptBackedGraphScopedEvidence } from './receipt-backed-graph-scoped-evidence.js';
import { protobufScalarToBigInt, protobufScalarToNumber } from './protobuf-scalars.js';

/**
Expand Down Expand Up @@ -1634,47 +1635,57 @@ export class FinalizationHandler {
}
}
if (!trustedAssertionEvidence) {
const recovery = await recoverReceiptBackedGraphScopedEvidence({
store: this.store,
chain: this.chain,
contextGraphId,
scope,
head: workspaceHead!,
merkleRoot,
publisherAddress,
kaId,
subGraphName,
});
if (recovery.status === 'recovered') {
this.log.info(
ctx,
`Chain-reconcile: recovered canonical transaction provenance and authenticated `
+ `local controls for ${scope.ual}`,
);
return this.repairExactGraphScopedVmMetadata({
contextGraphId,
scope,
verifiedQuads: vmVerification.quads,
computedMerkleRoot: vmVerification.merkleRoot,
evidence: recovery.evidence,
privateMerkleRoot,
batchId: kaId,
preserveNewerWorkspaceLifecycle: false,
ctx,
});
}
this.log.info(
ctx,
`Chain-reconcile: exact VM metadata for ${ual} cannot be repaired without `
+ 'transaction provenance; deferring',
+ `trusted receipt provenance (${recovery.reason}); deferring`,
);
return 'verified-vm-metadata-pending';
}
// A confirmed publish may have committed the exact VM graph before its
// graph-scoped metadata survived a crash. Reapply only the metadata tail:
// SWM writers use a different lock, so this recovery path must not delete
// a potentially newer staged assertion.
const outcome = await this.applyVerifiedGraphScopedFinalization({
return this.repairExactGraphScopedVmMetadata({
contextGraphId,
scope,
verifiedQuads: vmVerification.quads,
head,
privateMerkleRoot,
computedMerkleRoot: vmVerification.merkleRoot,
publisherAddress: evidencePublisherAddress,
txHash: trustedAssertionEvidence.transactionHash,
blockNumber: evidenceBlockNumber,
evidence: trustedAssertionEvidence,
privateMerkleRoot,
batchId: kaId,
authorAddress: evidenceAuthorAddress,
materializedVersion,
accessPolicy: trustedAssertionEvidence?.accessPolicy,
allowedPeers: trustedAssertionEvidence?.allowedPeers,
subGraphName,
source: 'chain-reconcile',
contentAlreadyMaterialized: true,
preserveNewerWorkspaceLifecycle,
ctx,
});
if (outcome === 'stale') return 'stale-target';
if (outcome === 'preserved-metadata') {
this.log.info(
ctx,
`Chain-reconcile: retained confirmed metadata for an older same-root assertion ${ual}`,
);
return preserveNewerWorkspaceLifecycle ? 'stale-target' : 'already-confirmed';
}
this.log.info(ctx, `Chain-reconcile: exact VM graph already matches ${ual}; repaired metadata`);
return preserveNewerWorkspaceLifecycle ? 'stale-target' : 'already-confirmed';
}

const swmVerification = await this.verifyExactGraphScopedLayer({
Expand Down Expand Up @@ -1759,6 +1770,64 @@ export class FinalizationHandler {
return preserveNewerWorkspaceLifecycle ? 'stale-target' : 'promoted';
}

private async repairExactGraphScopedVmMetadata(input: {
contextGraphId: string;
scope: ReturnType<typeof createGraphKnowledgeAssetScope>;
verifiedQuads: Quad[];
computedMerkleRoot: Uint8Array;
evidence: TrustedGraphScopedAssertionEvidence;
privateMerkleRoot?: Uint8Array;
batchId: bigint;
preserveNewerWorkspaceLifecycle: boolean;
ctx: OperationContext;
}): Promise<'already-confirmed' | 'stale-target'> {
const { evidence } = input;
const head: GraphScopedMaterializationEnvelope = {
publicTripleCount: evidence.publicTripleCount,
...(evidence.privateMerkleRoot
? { privateMerkleRoot: evidence.privateMerkleRoot }
: {}),
privateTripleCount: evidence.privateTripleCount,
publisherPeerId: evidence.publisherPeerId,
accessPolicy: evidence.accessPolicy,
allowedPeers: [...evidence.allowedPeers],
};
const outcome = await this.applyVerifiedGraphScopedFinalization({
contextGraphId: input.contextGraphId,
scope: input.scope,
verifiedQuads: input.verifiedQuads,
head,
privateMerkleRoot: input.privateMerkleRoot,
computedMerkleRoot: input.computedMerkleRoot,
publisherAddress: evidence.publisherAddress,
txHash: evidence.transactionHash,
blockNumber: evidence.blockNumber,
batchId: input.batchId,
authorAddress: evidence.authorAddress,
materializedVersion: { blockNumber: evidence.blockNumber, txIndex: evidence.txIndex },
accessPolicy: evidence.accessPolicy,
allowedPeers: evidence.allowedPeers,
subGraphName: evidence.subGraphName,
source: 'chain-reconcile',
contentAlreadyMaterialized: true,
ctx: input.ctx,
});
if (outcome === 'stale') return 'stale-target';
if (outcome === 'preserved-metadata') {
this.log.info(
input.ctx,
`Chain-reconcile: retained confirmed metadata for an older same-root assertion `
+ evidence.transactionHash,
);
return input.preserveNewerWorkspaceLifecycle ? 'stale-target' : 'already-confirmed';
}
this.log.info(
input.ctx,
`Chain-reconcile: exact VM graph already matches ${input.scope.ual}; repaired metadata`,
);
return input.preserveNewerWorkspaceLifecycle ? 'stale-target' : 'already-confirmed';
}

/**
* Materialize a graph-scoped assertion after its content and chain binding
* have been verified. Gossip finalization and chain reconciliation deliberately
Expand Down
208 changes: 208 additions & 0 deletions packages/agent/src/receipt-backed-graph-scoped-evidence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { assertSafeIri, contextGraphMetaUri } from '@origintrail-official/dkg-core';
import type { ChainAdapter } from '@origintrail-official/dkg-chain';
import type { Quad, TripleStore } from '@origintrail-official/dkg-storage';
import {
readLocallyTrustedKnowledgeAssetControls,
type KnowledgeAssetWorkspaceHead,
} from '@origintrail-official/dkg-publisher';
import { ethers } from 'ethers';
import {
VerifiedGraphScopedFinalizationEvidenceCodec,
type GraphScopedAccessPolicy,
type VerifiedGraphScopedFinalizationEvidence,
} from './finalization-graph-envelope.js';

const DKG_NS = 'http://dkg.io/ontology/';

export type ReceiptBackedGraphScopedEvidenceRecovery =
| { status: 'recovered'; evidence: VerifiedGraphScopedFinalizationEvidence }
| { status: 'unavailable'; reason: string };

export interface RecoverReceiptBackedGraphScopedEvidenceInput {
store: TripleStore;
chain?: ChainAdapter;
contextGraphId: string;
scope: { ual: string; assertionVersion: string };
head: KnowledgeAssetWorkspaceHead;
merkleRoot: Uint8Array;
publisherAddress: string;
kaId: bigint;
subGraphName?: string;
}

function stripRdfLiteral(value: string | undefined): string | undefined {
if (!value) return undefined;
if (!value.startsWith('"')) return value;
const lexical = /^("(?:\\.|[^"\\])*")/.exec(value)?.[1];
if (!lexical) return undefined;
try {
return JSON.parse(lexical) as string;
} catch {
return undefined;
}
}

function uniqueControlValue(rows: readonly Quad[], predicate: string): string | undefined {
const values = [...new Set(rows
.filter((quad) => quad.predicate === predicate)
.map((quad) => stripRdfLiteral(quad.object))
.filter((value): value is string => value !== undefined && value.length > 0))];
return values.length === 1 ? values[0] : undefined;
}

function trustedAccessEnvelope(rows: readonly Quad[]): {
accessPolicy: GraphScopedAccessPolicy;
allowedPeers: string[];
publisherPeerId: string;
} | undefined {
const accessPolicy = uniqueControlValue(rows, `${DKG_NS}accessPolicy`);
const publisherPeerId = uniqueControlValue(rows, `${DKG_NS}publisherPeerId`);
if (
(accessPolicy !== 'public' && accessPolicy !== 'ownerOnly' && accessPolicy !== 'allowList')
|| !publisherPeerId
) return undefined;
const allowedPeers = [...new Set(rows
.filter((quad) => quad.predicate === `${DKG_NS}allowedPeer`)
.map((quad) => stripRdfLiteral(quad.object))
.filter((value): value is string => value !== undefined && value.length > 0))];
if (
(accessPolicy === 'allowList' && allowedPeers.length === 0)
|| (accessPolicy !== 'allowList' && allowedPeers.length > 0)
) return undefined;
return { accessPolicy, allowedPeers, publisherPeerId };
}

function anchorQuads(input: RecoverReceiptBackedGraphScopedEvidenceInput): Quad[] {
const graph = contextGraphMetaUri(input.contextGraphId);
return [
{
subject: input.scope.ual,
predicate: `${DKG_NS}assertionVersion`,
object: `"${input.scope.assertionVersion}"^^<http://www.w3.org/2001/XMLSchema#integer>`,
graph,
},
{
subject: input.scope.ual,
predicate: `${DKG_NS}merkleRoot`,
object: `"${ethers.hexlify(input.merkleRoot).slice(2)}"`,
graph,
},
];
}

/**
* Recover receipt provenance only when the mutable SWM controls were recorded
* locally after authenticated envelope admission. The chain authenticates the
* KA identity and root; the local sidecar authenticates policy and peer identity.
*/
export async function recoverReceiptBackedGraphScopedEvidence(
input: RecoverReceiptBackedGraphScopedEvidenceInput,
): Promise<ReceiptBackedGraphScopedEvidenceRecovery> {
const resolver = input.chain?.resolveCanonicalFinalizationReceipt;
const rootCountReader = input.chain?.getMerkleRootCount;
if (
!input.chain
|| input.chain.chainId === 'none'
|| !resolver
|| !rootCountReader
|| input.scope.assertionVersion !== '1'
) return { status: 'unavailable', reason: 'canonical receipt recovery is unsupported' };

if (
input.head.kaUal !== input.scope.ual
|| input.head.assertionVersion !== input.scope.assertionVersion
) return { status: 'unavailable', reason: 'workspace head does not match the target assertion' };

let metaGraph: string;
let safeUal: string;
try {
metaGraph = assertSafeIri(contextGraphMetaUri(input.contextGraphId));
safeUal = assertSafeIri(input.scope.ual);
} catch {
return { status: 'unavailable', reason: 'context graph or UAL is not a safe IRI' };
}
const candidate = await input.store.query(
Comment thread
branarakic marked this conversation as resolved.
`SELECT ?tx ?kind WHERE {
GRAPH <${metaGraph}> {
<${safeUal}> <${DKG_NS}transactionHash> ?tx ;
<${DKG_NS}confirmationKind> ?kind .
Comment thread
branarakic marked this conversation as resolved.
Outdated
}
} LIMIT 2`,
{ source: 'agent.finalization.recoverReceiptBackedEvidence' },
);
if (candidate.type !== 'bindings' || candidate.bindings.length !== 1) {
return { status: 'unavailable', reason: 'exactly one receipt claim is required' };
}
const transactionHash = stripRdfLiteral(candidate.bindings[0]?.['tx']);
const confirmationKind = stripRdfLiteral(candidate.bindings[0]?.['kind']);
if (
confirmationKind !== 'transaction'
|| !transactionHash
|| !ethers.isHexString(transactionHash, 32)
) return { status: 'unavailable', reason: 'stored receipt claim is invalid' };

try {
const [resolution, rootCount, trustedRows] = await Promise.all([
resolver.call(input.chain, transactionHash),
rootCountReader.call(input.chain, input.kaId),
readLocallyTrustedKnowledgeAssetControls(
input.store,
metaGraph,
input.scope.ual,
anchorQuads(input),
{ source: 'agent.finalization.recoverReceiptBackedEvidence.controls' },
),
]);
if (resolution.status !== 'confirmed' || rootCount !== 1n) {
return { status: 'unavailable', reason: 'canonical receipt or unique root is unavailable' };
}
const controls = trustedAccessEnvelope(trustedRows);
if (!controls) {
return { status: 'unavailable', reason: 'authenticated local SWM controls are unavailable' };
}
const { receipt } = resolution;
if (
receipt.txHash.toLowerCase() !== transactionHash.toLowerCase()
|| receipt.kaId !== input.kaId
|| receipt.batchId !== input.kaId
|| receipt.startKAId !== input.kaId
|| receipt.endKAId !== input.kaId
|| !ethers.isHexString(receipt.blockHash, 32)
|| !ethers.isAddress(input.publisherAddress)
|| !ethers.isAddress(receipt.publisherAddress)
|| ethers.getAddress(receipt.publisherAddress) !== ethers.getAddress(input.publisherAddress)
|| !Number.isSafeInteger(receipt.blockNumber)
|| receipt.blockNumber < 0
|| !Number.isSafeInteger(receipt.txIndex)
|| receipt.txIndex < 0
|| ethers.hexlify(receipt.merkleRoot).toLowerCase()
!== ethers.hexlify(input.merkleRoot).toLowerCase()
) return { status: 'unavailable', reason: 'canonical receipt does not match the target KA' };

const evidence = VerifiedGraphScopedFinalizationEvidenceCodec.parse({
assertionVersion: input.scope.assertionVersion,
publicQuadsDigest: input.head.publicQuadsDigest,
publicTripleCount: input.head.publicTripleCount,
...(input.head.privateMerkleRoot
? { privateMerkleRoot: input.head.privateMerkleRoot }
: {}),
privateTripleCount: input.head.privateTripleCount,
publisherPeerId: controls.publisherPeerId,
publisherAddress: receipt.publisherAddress,
transactionHash: receipt.txHash,
blockNumber: receipt.blockNumber,
blockHash: receipt.blockHash,
txIndex: receipt.txIndex,
...(receipt.authorAddress ? { authorAddress: receipt.authorAddress } : {}),
accessPolicy: controls.accessPolicy,
allowedPeers: controls.allowedPeers,
...(input.subGraphName ? { subGraphName: input.subGraphName } : {}),
});
return { status: 'recovered', evidence };
} catch (error) {
return {
status: 'unavailable',
reason: error instanceof Error ? error.message : String(error),
};
}
}
Loading
Loading