-
Notifications
You must be signed in to change notification settings - Fork 10
feat(agent): reconcile selected public VM from chain truth #2184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
branarakic
merged 37 commits into
testnet-canary
from
codex/vm-metadata-provenance-repair
Aug 10, 2026
Merged
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
513eacd
fix(agent): authenticate receipt-backed metadata repair
d8c57cf
fix(agent): repair public VM metadata from chain truth
adfbb85
fix(agent): promote exact public SWM from chain inventory
1c8ba22
refactor(agent): centralize trusted control recovery
19edcec
fix(agent): make public VM recovery fail closed
eac18d3
test(agent): cover receiptless public KA updates
c302466
fix(agent): fail closed in receipt recovery
9e8b34f
refactor(publisher): type trusted control writes
8f9592d
refactor(publisher): make typed controls canonical
a4e4b4e
refactor(test): extract graph reconcile fixtures
3aa4fe3
fix(agent): reconcile selected RFC64 public VM
e6b5872
Merge remote-tracking branch 'origin/codex/10.0.14-cg298-binding-hotf…
d515bc3
Merge branch 'codex/10.0.14-cg298-binding-hotfix' of https://github.c…
7a14940
test(agent): cover selected VM reconcile lifecycle
5d4f443
refactor(agent): unify public finalized materialization
8798952
fix(agent): validate selected VM cursor binding
4a99e41
Merge branch 'codex/10.0.14-cg298-binding-hotfix' of https://github.c…
6e8c1b1
feat(agent): gate startup subscription rehydration
1196a91
feat(cli): expose effective sync lifecycle gate
56960f3
test(agent): use composed VM reconcile fixture
c34a70e
fix(agent): fence selected VM cursors by deployment
7807c95
fix(agent): resolve chain discovery name hashes
e69ea72
fix(agent): preserve selected VM binding failures
e39dbc8
refactor(agent): keep VM cleanup out of binding state
50bab7d
test(cli): cover daemon subscription rehydration gate
93a1a3d
refactor(chain): simplify name-hash resolver layers
5fe8370
test(node-ui): expect deployment cursor schema
fcb65ed
test(cli): expect current deployment cursor schema
167c992
refactor(agent): isolate selected VM cursor storage
0b263b5
fix(agent): harden subscription and CG discovery boundaries
dc73c76
Merge remote-tracking branch 'origin/testnet-canary' into codex/vm-me…
8402782
refactor(agent): normalize discovered context graph bindings
9f43c0d
fix(agent): fence selected VM reconcile lifecycle
4f3eb80
refactor(chain): own public finalization authority fence
26ecbd6
fix: verify graph-scoped update receipts during VM repair
229590c
test: cover selected VM cursor V31 upgrade
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
208 changes: 208 additions & 0 deletions
208
packages/agent/src/receipt-backed-graph-scoped-evidence.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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( | ||
| `SELECT ?tx ?kind WHERE { | ||
| GRAPH <${metaGraph}> { | ||
| <${safeUal}> <${DKG_NS}transactionHash> ?tx ; | ||
| <${DKG_NS}confirmationKind> ?kind . | ||
|
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), | ||
| }; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.