Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
466 changes: 394 additions & 72 deletions packages/agent/src/finalization-handler.ts

Large diffs are not rendered by default.

212 changes: 212 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,212 @@
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 {
readGraphKnowledgeAssetReceiptProvenanceV1,
readLocallyTrustedKnowledgeAssetControlEnvelope,
type KnowledgeAssetWorkspaceHead,
} from '@origintrail-official/dkg-publisher';
import { ethers } from 'ethers';
import {
VerifiedGraphScopedFinalizationEvidenceCodec,
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;
onChainContextGraphId: bigint;
subGraphName?: string;
}

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 when the mutable SWM controls were recorded locally
* after authenticated envelope admission. For a chain-confirmed public CG, the
* on-chain access policy is also sufficient: public reads do not depend on a
* publisher peer identity or allow-list. Private CGs continue to require the
* authenticated local sidecar and fail closed when it is unavailable.
*/
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 ?predicate ?object WHERE {
GRAPH <${metaGraph}> { <${safeUal}> ?predicate ?object }
}`,
{ source: 'agent.finalization.recoverReceiptBackedEvidence' },
);
if (
candidate.type !== 'bindings'
|| candidate.bindings.some((row) => (
row['predicate'] === undefined || row['object'] === undefined
))
) {
return { status: 'unavailable', reason: 'stored receipt metadata is incomplete' };
}
const receiptProvenance = readGraphKnowledgeAssetReceiptProvenanceV1(
candidate.bindings.map((row) => ({
predicate: row['predicate']!,
object: row['object']!,
})),
);
if (!receiptProvenance) {
return { status: 'unavailable', reason: 'stored receipt claim is invalid' };
}
const { transactionHash } = receiptProvenance;

try {
const [resolution, rootCount, trustedControls] = await Promise.all([
resolver.call(input.chain, transactionHash),
rootCountReader.call(input.chain, input.kaId),
readLocallyTrustedKnowledgeAssetControlEnvelope(
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' };
}
let controls = trustedControls;
if (!controls) {
const getAccessPolicy = input.chain.getContextGraphAccessPolicy;
const isActiveOnChain = input.chain.isContextGraphActiveOnChain;
if (
input.onChainContextGraphId <= 0n
|| !isActiveOnChain
|| !getAccessPolicy
) {
return {
status: 'unavailable',
reason: 'authenticated local SWM controls and active on-chain public policy are unavailable',
};
}
const activeOnChain = await isActiveOnChain.call(
input.chain,
input.onChainContextGraphId,
);
if (!activeOnChain) {
return {
status: 'unavailable',
reason: 'on-chain context graph is not active',
};
}
const onChainAccessPolicy = Number(await getAccessPolicy.call(
Comment thread
branarakic marked this conversation as resolved.
input.chain,
input.onChainContextGraphId,
));
if (onChainAccessPolicy !== 0) {
return {
status: 'unavailable',
reason: 'authenticated local SWM controls are required for a non-public context graph',
};
}
controls = {
accessPolicy: 'public',
allowedPeers: [],
// Public access does not authorize through this value. Preserve an
// explicit non-empty sentinel rather than trusting peer-supplied SWM.
publisherPeerId: 'unknown',
};
}
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