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
13 changes: 13 additions & 0 deletions .github/workflows/system-record-managed-ownership.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ name: System-record managed ownership evidence
# 3. Reserved internal state is invisible to enumeration, unserved by
# `hasGraph`, and undeletable by generic mutation including an unscoped
# `deleteByPattern`.
# 4. A live ownership-leased store refuses raw update() and mutating/unknown
# query() programs without changing data, while recognized reads survive.
#
# What it does NOT prove:
# - `pinnedPredecessors` is an INVENTORY, not a set of verdicts. No predecessor
Expand All @@ -39,8 +41,12 @@ on:
- 'packages/agent/**'
- 'packages/cli/**'
- 'packages/core/**'
- 'packages/publisher/**'
- 'packages/query/**'
- 'packages/random-sampling/**'
- 'packages/rdf-utils/**'
- 'packages/storage/**'
- 'scripts/check-managed-store-raw-channels.mjs'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'tsconfig.base.json'
Expand All @@ -53,8 +59,12 @@ on:
- 'packages/agent/**'
- 'packages/cli/**'
- 'packages/core/**'
- 'packages/publisher/**'
- 'packages/query/**'
- 'packages/random-sampling/**'
- 'packages/rdf-utils/**'
- 'packages/storage/**'
- 'scripts/check-managed-store-raw-channels.mjs'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'tsconfig.base.json'
Expand Down Expand Up @@ -136,6 +146,9 @@ jobs:
- name: Typecheck the ownership gate harness
run: pnpm typecheck:live:system-record-managed-ownership

- name: First-party managed-store raw-channel architecture
run: pnpm check:managed-store-raw-channels
Comment thread
Jurij89 marked this conversation as resolved.

- name: Storage unit conformance
run: |
pnpm --filter @origintrail-official/dkg-storage exec vitest run \
Expand Down
44 changes: 42 additions & 2 deletions devnet/issue-2052-managed-ownership/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
*/

export const MANAGED_OWNERSHIP_RAW_SCHEMA_VERSION =
'dkg-system-record-managed-ownership-result-v1' as const;
'dkg-system-record-managed-ownership-result-v2' as const;
export const MANAGED_OWNERSHIP_VERDICT_SCHEMA_VERSION =
'dkg-system-record-managed-ownership-verdict-v1' as const;
'dkg-system-record-managed-ownership-verdict-v2' as const;

/**
* A pinned manifest entry: an inventory row, NOT a result.
Expand All @@ -36,12 +36,22 @@ export interface PinnedPredecessorV1 {
* `hasGraph`, undeletable by generic mutation including an unscoped
* `deleteByPattern`, and the seed must be intact afterwards.
*/
export interface RawChannelConformanceV1 {
readonly updateRefusalCode: string | null;
readonly queryMutationRefusalCode: string | null;
readonly queryUnknownRefusalCode: string | null;
readonly mutationQuadCount: number;
readonly recognizedReadServed: boolean;
}

export interface CurrentBinaryConformanceV1 {
readonly enumeratedReservedGraphs: readonly string[];
readonly servedReservedGraphs: readonly string[];
readonly deletedReservedGraphsOnCleanup: readonly string[];
readonly seededQuadCount: number;
readonly expectedQuadCount: number;
/** Live proof that an authentic ownership lease closes both raw write routes. */
readonly rawChannels: RawChannelConformanceV1;
readonly failures: readonly string[];
}

Expand Down Expand Up @@ -328,6 +338,36 @@ export function evaluateManagedOwnership(
pass: (raw.currentBinaryConformance?.failures.length ?? 1) === 0,
detail: raw.currentBinaryConformance?.failures.join('; ') || undefined,
},
{
name: 'leasedRawUpdateRefused',
pass: raw.currentBinaryConformance?.rawChannels.updateRefusalCode
=== 'MANAGED_OXIGRAPH_MUTATION_UNAVAILABLE',
detail: raw.currentBinaryConformance?.rawChannels.updateRefusalCode ?? 'missing refusal',
},
{
name: 'leasedQueryMutationRefused',
pass: raw.currentBinaryConformance?.rawChannels.queryMutationRefusalCode
=== 'MANAGED_OXIGRAPH_MUTATION_UNAVAILABLE',
detail: raw.currentBinaryConformance?.rawChannels.queryMutationRefusalCode ?? 'missing refusal',
},
{
name: 'leasedQueryUnknownSyntaxRefused',
pass: raw.currentBinaryConformance?.rawChannels.queryUnknownRefusalCode
=== 'MANAGED_OXIGRAPH_MUTATION_UNAVAILABLE',
detail: raw.currentBinaryConformance?.rawChannels.queryUnknownRefusalCode ?? 'missing refusal',
},
{
name: 'rawChannelsMutatedNoLiveData',
pass: raw.currentBinaryConformance?.rawChannels.mutationQuadCount === 0,
detail: `${raw.currentBinaryConformance?.rawChannels.mutationQuadCount ?? 'missing'} quads`,
},
{
name: 'recognizedReadSurvivesRawChannelContraction',
pass: raw.currentBinaryConformance?.rawChannels.recognizedReadServed === true,
detail: raw.currentBinaryConformance?.rawChannels.recognizedReadServed
? undefined
: 'prefixed ASK did not succeed',
},
];

return checks;
Expand Down
84 changes: 84 additions & 0 deletions devnet/issue-2052-managed-ownership/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
type LiveHandoffMeasurementV1,
type ManagedOwnershipRawResultV1,
type CurrentBinaryConformanceV1,
type RawChannelConformanceV1,
} from './model.js';

const HERE = dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -148,6 +149,86 @@ async function countQuadsInGraph(endpoint: string, graph: string): Promise<numbe
return Number(json.results?.bindings?.[0]?.c?.value ?? '0');
}

async function captureErrorCode(work: () => Promise<unknown>): Promise<string | null> {
try {
await work();
return null;
} catch (error) {
return typeof error === 'object' && error !== null && 'code' in error
? String((error as { code: unknown }).code)
: null;
}
}

async function probeLeasedRawChannels(
store: TripleStore,
queryEndpoint: string,
): Promise<{ rawChannels: RawChannelConformanceV1; failures: string[] }> {
const rawChannelGraph = 'urn:dkg:gate:leased-raw-channel';
const refusalProbes = [
{
field: 'updateRefusalCode',
label: 'leased update()',
run: () => store.update
? store.update(
`INSERT DATA { GRAPH <${rawChannelGraph}> { <urn:update> <urn:p> "escaped" } }`,
)
: Promise.resolve(),
},
{
field: 'queryMutationRefusalCode',
label: 'leased query() mutation',
run: () => store.query(
`SELECT ?s WHERE { GRAPH <urn:dkg:gate:read-scope> { ?s ?p ?o } }; ` +
`INSERT DATA { GRAPH <${rawChannelGraph}> { <urn:query> <urn:p> "escaped" } }`,
),
},
{
field: 'queryUnknownRefusalCode',
label: 'leased query() unknown',
run: () => store.query('VALUES ?s { <urn:unknown> }'),
},
] as const;

const refusalCodes = new Map<string, string | null>();
for (const probe of refusalProbes) {
refusalCodes.set(probe.field, await captureErrorCode(probe.run));
}

let recognizedReadServed = false;
try {
const read = await store.query(
'PREFIX dkg: <urn:dkg:gate:>\nASK { dkg:missing dkg:p ?o }',
);
recognizedReadServed = read.type === 'boolean' && read.value === false;
} catch {
recognizedReadServed = false;
}

const rawChannels: RawChannelConformanceV1 = {
updateRefusalCode: refusalCodes.get('updateRefusalCode') ?? null,
queryMutationRefusalCode: refusalCodes.get('queryMutationRefusalCode') ?? null,
queryUnknownRefusalCode: refusalCodes.get('queryUnknownRefusalCode') ?? null,
mutationQuadCount: await countQuadsInGraph(queryEndpoint, rawChannelGraph),
recognizedReadServed,
};
const failures: string[] = [];
const expectedRefusal = 'MANAGED_OXIGRAPH_MUTATION_UNAVAILABLE';
for (const probe of refusalProbes) {
const actual = rawChannels[probe.field];
if (actual !== expectedRefusal) {
failures.push(`${probe.label} refusal code was ${String(actual)}`);
}
}
if (rawChannels.mutationQuadCount !== 0) {
failures.push(`raw channels wrote ${rawChannels.mutationQuadCount} live quad(s)`);
}
if (!rawChannels.recognizedReadServed) {
failures.push('recognized prefixed ASK did not survive raw-channel contraction');
}
return { rawChannels, failures };
}

const pidAlive = (pid: number): boolean => {
try {
process.kill(pid, 0);
Expand Down Expand Up @@ -704,6 +785,8 @@ async function main(): Promise<void> {
// carries no pass/fail field to be misread.
{
const failures: string[] = [];
const rawProbe = await probeLeasedRawChannels(full, server.queryEndpoint);
failures.push(...rawProbe.failures);

// The deletion probe below is destructive, so seed immediately before it.
// This is also why the old per-entry loop could not work: the first row's
Expand Down Expand Up @@ -831,6 +914,7 @@ async function main(): Promise<void> {
deletedReservedGraphsOnCleanup: deleted,
seededQuadCount,
expectedQuadCount: fixture.expectedQuadCount,
rawChannels: rawProbe.rawChannels,
failures,
};
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"test:live:system-record-managed-ownership:generate": "pnpm -r --filter @origintrail-official/dkg... --filter '!@origintrail-official/dkg-evm-module' run build && node --import tsx devnet/issue-2052-managed-ownership/run.ts",
"test:live:system-record-managed-ownership:verify": "node --import tsx devnet/issue-2052-managed-ownership/verify.ts",
"typecheck:live:system-record-managed-ownership": "pnpm --filter @devnet/issue-2052-managed-ownership typecheck",
"check:managed-store-raw-channels": "node --import tsx scripts/check-managed-store-raw-channels.mjs",
"test:watch": "vitest --config vitest.config.ts",
"test:coverage": "turbo test:coverage",
"bench": "pnpm --filter @origintrail-official/dkg-storage build && esbench --config esbench.config.mjs",
Expand Down
13 changes: 6 additions & 7 deletions packages/agent/src/dkg-agent-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ import {
pickNetworkTunables,
isSparqlUpdateOperation,
} from '@origintrail-official/dkg-core';
import { GraphManager, PrivateContentStore, SystemRecordLaneForwarderV1, createTripleStore, isExternalBackend, structuredMutationMightMutate, structuredMutationTouchedGraphs, type TripleStore, type TripleStoreConfig, type Quad, type LargeLiteralStorageConfig, type QueryOptions } from '@origintrail-official/dkg-storage';
import { GraphManager, PrivateContentStore, SystemRecordLaneForwarderV1, captureStructuredMutationEffects, createTripleStore, isExternalBackend, type TripleStore, type TripleStoreConfig, type Quad, type LargeLiteralStorageConfig, type QueryOptions } from '@origintrail-official/dkg-storage';
import { emptyRpcUsageWindow, EVMChainAdapter, NoChainAdapter, enrichEvmError, buildKnowledgeAssetUal, type EVMAdapterConfig, type ChainAdapter, type CreateContextGraphParams, type CreateOnChainContextGraphParams, type CreateOnChainContextGraphResult, type TxResult, type V10PublishingConvictionAccountInfo, type RpcUsageWindow } from '@origintrail-official/dkg-chain';
import {
DKGPublisher, PublishHandler, SharedMemoryHandler, UpdateHandler, ChainEventPoller, AccessHandler, AccessClient,
Expand Down Expand Up @@ -578,14 +578,13 @@ export function createListContextGraphsCacheInvalidatingStore(
: undefined,
structuredMutation: innerStore.structuredMutation
? (mutation, options) => {
// Capture scope before the first await so caller-side mutation cannot
// redirect cache invalidation after the backend has committed.
const targetGraphs = [...structuredMutationTouchedGraphs(mutation)];
const mightMutate = structuredMutationMightMutate(mutation);
const effects = captureStructuredMutationEffects(mutation);
return invalidateAfterMutation(
() => innerStore.structuredMutation!(mutation, options),
() => mightMutate,
() => targetGraphs.forEach((graph) => markProjectionDirty?.(undefined, graph)),
() => effects !== undefined,
() => effects?.touchedGraphs.forEach(
(graph) => markProjectionDirty?.(undefined, graph),
),
);
}
: undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/agent/src/generic-sql-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,7 @@ async function createMssqlClient(
}
request.input(name, value);
}
// dkg-raw-channel-non-store: optional mssql Request, not an RDF TripleStore.
Comment thread
Jurij89 marked this conversation as resolved.
const result = await request.query(sql);
return result.recordset ?? [];
},
Expand Down
74 changes: 73 additions & 1 deletion packages/agent/test/replace-subject-agent-wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
CHANGELOG_GRAPH,
OxigraphStore,
Expand Down Expand Up @@ -150,4 +150,76 @@ describe('#1863 replaceSubject through the agent store wrapper', () => {
proj.markDirtyForGraph('urn:dkg:publisher:control-plane');
expect(entries.has('urn:dkg:publisher:control-plane')).toBe(false);
});

it('invalidates structured mutation effects after success without decoding them in Agent', async () => {
let release!: () => void;
const inFlight = new Promise<void>((resolve) => { release = resolve; });
const options = { source: 'agent.test.structured-mutation-effects' };
let inner!: TripleStore;
const structuredMutation = vi.fn(function (
this: TripleStore,
_mutation: unknown,
receivedOptions: unknown,
) {
expect(this).toBe(inner);
expect(receivedOptions).toBe(options);
return inFlight;
});
inner = { structuredMutation } as unknown as TripleStore;
const invalidate = vi.fn();
const markProjectionDirty = vi.fn();
const store = createListContextGraphsCacheInvalidatingStore(
inner,
invalidate,
markProjectionDirty,
);
const mutation = {
kind: 'copy-subject-projection' as const,
input: {
sourceGraphUris: ['urn:test:source'],
targetGraphUri: 'urn:test:target',
roots: ['urn:test:root'],
descendantSuffix: '/',
excludedPredicates: [],
},
};

const pending = store.structuredMutation!(mutation, options);
mutation.input.targetGraphUri = 'urn:test:redirected';
expect(invalidate).not.toHaveBeenCalled();
release();
await pending;

expect(invalidate).toHaveBeenCalledOnce();
expect(markProjectionDirty).toHaveBeenCalledOnce();
expect(markProjectionDirty).toHaveBeenCalledWith(undefined, 'urn:test:target');
});

it('does not invalidate structured mutation failures or structural no-ops', async () => {
const invalidate = vi.fn();
const markProjectionDirty = vi.fn();
const inner = {
structuredMutation: vi.fn(async () => undefined),
} as unknown as TripleStore;
const store = createListContextGraphsCacheInvalidatingStore(
inner,
invalidate,
markProjectionDirty,
);

await store.structuredMutation!({
kind: 'delete-subjects',
input: { graphUri: 'urn:test:target', subjects: [] },
});
expect(invalidate).not.toHaveBeenCalled();
expect(markProjectionDirty).not.toHaveBeenCalled();

inner.structuredMutation = vi.fn(async () => { throw new Error('commit failed'); });
await expect(store.structuredMutation!({
kind: 'delete-subjects',
input: { graphUri: 'urn:test:target', subjects: ['urn:test:subject'] },
})).rejects.toThrow('commit failed');
expect(invalidate).not.toHaveBeenCalled();
expect(markProjectionDirty).not.toHaveBeenCalled();
});
});
Loading
Loading