From e82be379c5ce85d0f47877f12e6329ab8ee63459 Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sat, 8 Aug 2026 22:21:41 +0200 Subject: [PATCH 01/12] refactor(agent): decompose profile record production --- packages/agent/src/profile.ts | 86 +- .../agent-profile-producer-commit-v1.ts | 50 + .../agent-profile-producer-contract-v1.ts | 143 ++ .../agent-profile-producer-inventory-v1.ts | 217 +++ .../agent-profile-producer-preparation-v1.ts | 357 ++++ .../agent-profile-producer-signing-v1.ts | 65 + .../agent-profile-producer-v1.ts | 714 +------- ...gent-profile-producer-authority-v1.test.ts | 336 ++++ ...gent-profile-producer-lifecycle-v1.test.ts | 287 ++++ ...nt-profile-producer-publication-v1.test.ts | 286 ++++ ...m-record-agent-profile-producer-v1.test.ts | 1521 ----------------- ...ent-profile-producer-validation-v1.test.ts | 669 ++++++++ .../src/agent-profile-projection-schema-v1.ts | 95 +- .../core/src/agent-profile-schema-model-v1.ts | 303 ++++ packages/core/src/system-record-objects-v1.ts | 101 +- .../test/system-record-package-export-v1.mjs | 1 + .../system-record-policy-helpers-v1.test.ts | 63 + 17 files changed, 2917 insertions(+), 2377 deletions(-) create mode 100644 packages/agent/src/system-records/agent-profile-producer-commit-v1.ts create mode 100644 packages/agent/src/system-records/agent-profile-producer-contract-v1.ts create mode 100644 packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts create mode 100644 packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts create mode 100644 packages/agent/src/system-records/agent-profile-producer-signing-v1.ts create mode 100644 packages/agent/test/system-record-agent-profile-producer-authority-v1.test.ts create mode 100644 packages/agent/test/system-record-agent-profile-producer-lifecycle-v1.test.ts create mode 100644 packages/agent/test/system-record-agent-profile-producer-publication-v1.test.ts delete mode 100644 packages/agent/test/system-record-agent-profile-producer-v1.test.ts create mode 100644 packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts create mode 100644 packages/core/src/agent-profile-schema-model-v1.ts diff --git a/packages/agent/src/profile.ts b/packages/agent/src/profile.ts index d91bd31357..65729a3f91 100644 --- a/packages/agent/src/profile.ts +++ b/packages/agent/src/profile.ts @@ -1,11 +1,12 @@ import type { Quad } from '@origintrail-official/dkg-storage'; import { - DKG_ONTOLOGY, SYSTEM_CONTEXT_GRAPHS, isPublicLikeAddress, } from '@origintrail-official/dkg-core'; import { + AGENT_PROFILE_SCHEMA_TERMS_V1, agentProfileIdentityFactsV1, + deriveAgentProfileOwnedSubjectV1, type AgentProfileIdentityFactsV1, type AgentProfileProjectionQuadV1, } from '@origintrail-official/dkg-core/system-record-v1'; @@ -77,12 +78,7 @@ export function collectPublishableMultiaddrs( return out; } -const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; -const SCHEMA = 'https://schema.org/'; -const DKG = 'https://dkg.network/ontology#'; -const ERC8004 = 'https://eips.ethereum.org/erc-8004#'; -const PROV = 'http://www.w3.org/ns/prov#'; -const SKILL = 'https://dkg.origintrail.io/skill#'; +const T = AGENT_PROFILE_SCHEMA_TERMS_V1; export const AGENT_REGISTRY_CONTEXT_GRAPH = SYSTEM_CONTEXT_GRAPHS.AGENTS; export const AGENT_REGISTRY_GRAPH = `did:dkg:context-graph:${AGENT_REGISTRY_CONTEXT_GRAPH}`; @@ -267,24 +263,24 @@ function buildAgentProfileModelV1(config: AgentProfileConfig): AgentProfileModel facts.push({ subject: s, predicate: p, object: o }); // Type: dkg:Agent + role-specific subclass - q(entity, RDF_TYPE, `${DKG}Agent`); - q(entity, RDF_TYPE, role === 'core' ? `${DKG}CoreNode` : `${DKG}EdgeNode`); + q(entity, T.rdfType, T.dkgAgent); + q(entity, T.rdfType, role === 'core' ? T.dkgCoreNode : T.dkgEdgeNode); // schema.org metadata - q(entity, `${SCHEMA}name`, `"${config.name}"`); + q(entity, T.schemaName, `"${config.name}"`); if (config.description) { - q(entity, `${SCHEMA}description`, `"${config.description}"`); + q(entity, T.schemaDescription, `"${config.description}"`); } // DKG P2P properties q(entity, identity.peerId.predicate, identity.peerId.object); - q(entity, `${DKG}nodeRole`, `"${role}"`); + q(entity, T.dkgNodeRole, `"${role}"`); if (identity.publicKey !== undefined) { q(entity, identity.publicKey.predicate, identity.publicKey.object); } if (config.relayAddress) { - q(entity, `${DKG}relayAddress`, `"${config.relayAddress}"`); + q(entity, T.dkgRelayAddress, `"${config.relayAddress}"`); } if (identity.agentAddress !== undefined) { q(entity, identity.agentAddress.predicate, identity.agentAddress.object); @@ -304,76 +300,76 @@ function buildAgentProfileModelV1(config: AgentProfileConfig): AgentProfileModel // contain quote characters; this guard is purely against // malformed callers. if (!ma || ma.includes('"')) continue; - q(entity, `${DKG}multiaddr`, `"${ma}"`); + q(entity, T.dkgMultiaddr, `"${ma}"`); } } - q(entity, `${DKG}lastSeen`, `"${profileTimestamp}"`); + q(entity, T.dkgLastSeen, `"${profileTimestamp}"`); // Encryption keys: prefer the multi-key array; fall back to the deprecated // singular fields only when the array isn't supplied (legacy callers / // test fixtures). Retired keys still get published so peers learn their // wallet-signed revocations and the resolver can prune them. if (config.encryptionKeys && config.encryptionKeys.length > 0) { for (const key of config.encryptionKeys) { - q(entity, `${DKG}publicEncryptionKey`, `"${key.publicEncryptionKey}"`); - q(entity, `${DKG}encryptionKeyAlgorithm`, `"${key.encryptionKeyAlgorithm}"`); - q(entity, `${DKG}encryptionKeyProof`, `"${key.encryptionKeyProof}"`); + q(entity, T.dkgPublicEncryptionKey, `"${key.publicEncryptionKey}"`); + q(entity, T.dkgEncryptionKeyAlgorithm, `"${key.encryptionKeyAlgorithm}"`); + q(entity, T.dkgEncryptionKeyProof, `"${key.encryptionKeyProof}"`); if (key.revokedAt && key.revocationProof) { - q(key.encryptionKeyId, `${DKG}revokedAt`, `"${key.revokedAt}"`); - q(key.encryptionKeyId, `${DKG}revokedBy`, entity); - q(key.encryptionKeyId, `${DKG}encryptionKeyRevocationProof`, `"${key.revocationProof}"`); + q(key.encryptionKeyId, T.dkgRevokedAt, `"${key.revokedAt}"`); + q(key.encryptionKeyId, T.dkgRevokedBy, entity); + q(key.encryptionKeyId, T.dkgEncryptionKeyRevocationProof, `"${key.revocationProof}"`); } } } else if (config.publicEncryptionKey && config.encryptionKeyAlgorithm && config.encryptionKeyProof) { - q(entity, `${DKG}publicEncryptionKey`, `"${config.publicEncryptionKey}"`); - q(entity, `${DKG}encryptionKeyAlgorithm`, `"${config.encryptionKeyAlgorithm}"`); - q(entity, `${DKG}encryptionKeyProof`, `"${config.encryptionKeyProof}"`); + q(entity, T.dkgPublicEncryptionKey, `"${config.publicEncryptionKey}"`); + q(entity, T.dkgEncryptionKeyAlgorithm, `"${config.encryptionKeyAlgorithm}"`); + q(entity, T.dkgEncryptionKeyProof, `"${config.encryptionKeyProof}"`); } if (config.framework) { - q(entity, `${SKILL}framework`, `"${config.framework}"`); + q(entity, T.skillFramework, `"${config.framework}"`); } // ERC-8004 capabilities (skills as capabilities) for (let i = 0; i < config.skills.length; i++) { const skill = config.skills[i]; - const capUri = `${entity}/.well-known/genid/cap${i + 1}`; + const capUri = deriveAgentProfileOwnedSubjectV1(entity, 'capability', i + 1); - q(entity, `${ERC8004}capabilities`, capUri); - q(capUri, RDF_TYPE, `${ERC8004}Capability`); - q(capUri, `${SCHEMA}name`, `"${skill.skillType}"`); + q(entity, T.erc8004Capabilities, capUri); + q(capUri, T.rdfType, T.erc8004Capability); + q(capUri, T.schemaName, `"${skill.skillType}"`); // Keep backward-compatible skill offering triples - const offeringUri = `${entity}/.well-known/genid/offering${i + 1}`; - q(entity, `${SKILL}offersSkill`, offeringUri); - q(offeringUri, RDF_TYPE, `${SKILL}SkillOffering`); - q(offeringUri, `${SKILL}skill`, `${SKILL}${skill.skillType}`); + const offeringUri = deriveAgentProfileOwnedSubjectV1(entity, 'offering', i + 1); + q(entity, T.skillOffersSkill, offeringUri); + q(offeringUri, T.rdfType, T.skillSkillOffering); + q(offeringUri, T.skillSkill, `${T.skillNamespace}${skill.skillType}`); if (skill.pricePerCall !== undefined) { - q(offeringUri, `${SKILL}pricePerCall`, `"${skill.pricePerCall}"`); + q(offeringUri, T.skillPricePerCall, `"${skill.pricePerCall}"`); } if (skill.currency) { - q(offeringUri, `${SKILL}currency`, `"${skill.currency}"`); + q(offeringUri, T.skillCurrency, `"${skill.currency}"`); } if (skill.successRate !== undefined) { - q(offeringUri, `${SKILL}successRate`, `"${skill.successRate}"`); + q(offeringUri, T.skillSuccessRate, `"${skill.successRate}"`); } if (skill.pricingModel) { - q(offeringUri, `${SKILL}pricing`, `${SKILL}${skill.pricingModel}`); + q(offeringUri, T.skillPricing, `${T.skillNamespace}${skill.pricingModel}`); } } // PROV provenance - const activityUri = `${entity}/.well-known/genid/registration`; - q(entity, `${PROV}wasGeneratedBy`, activityUri); - q(activityUri, RDF_TYPE, `${PROV}Activity`); - q(activityUri, `${PROV}atTime`, `"${profileTimestamp}"`); + const activityUri = deriveAgentProfileOwnedSubjectV1(entity, 'registration'); + q(entity, T.provWasGeneratedBy, activityUri); + q(activityUri, T.rdfType, T.provActivity); + q(activityUri, T.provAtTime, `"${profileTimestamp}"`); const served = config.contextGraphsServed; if (served?.length) { - const hostingUri = `${entity}/.well-known/genid/hosting`; - q(entity, `${SKILL}hostingProfile`, hostingUri); - q(hostingUri, RDF_TYPE, `${SKILL}HostingProfile`); + const hostingUri = deriveAgentProfileOwnedSubjectV1(entity, 'hosting'); + q(entity, T.skillHostingProfile, hostingUri); + q(hostingUri, T.rdfType, T.skillHostingProfileType); for (const cg of served) { - q(hostingUri, `${SKILL}contextGraphsServed`, `"${cg}"`); + q(hostingUri, T.skillContextGraphsServed, `"${cg}"`); } } diff --git a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts new file mode 100644 index 0000000000..c29c53a94c --- /dev/null +++ b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts @@ -0,0 +1,50 @@ +import type { + AgentProfileProducerPublicationV1, + CreateAgentProfileProducerOptionsV1, +} from './agent-profile-producer-contract-v1.js'; +import type { AgentProfileProductionInventoryV1 } from './agent-profile-producer-inventory-v1.js'; +import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; +import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; + +export async function commitAgentProfileProductionV1( + options: CreateAgentProfileProducerOptionsV1, + preparation: AgentProfileProductionPreparationV1, + signed: SignedAgentProfileProductionV1, + inventoryPlan: AgentProfileProductionInventoryV1, + signal: AbortSignal, +): Promise { + const commitLease = await options.store.prepareCommit({ + expectedHeadDigest: preparation.snapshot.currentHead?.objectDigest ?? null, + expectedRootDescriptorDigest: preparation.snapshot.inventory?.descriptorDigest ?? null, + publicationArtifacts: inventoryPlan.publicationArtifacts, + inventory: inventoryPlan.inventory, + rootEnvelope: inventoryPlan.rootEnvelope, + }); + let committed = false; + try { + signal.throwIfAborted(); + await options.install({ + head: preparation.head, + envelope: signed.envelope, + canonicalProjectionBytes: preparation.projectionBytes, + projectionQuads: preparation.projectionQuads, + ownedSubjectTable: preparation.ownedSubjectTable, + verifiedAuthoritySummary: inventoryPlan.verifiedAuthoritySummary, + signal, + }); + // Installation is the point of no return: a late abort cannot roll it back, + // so the already-reserved advertisement must commit to keep both views aligned. + await commitLease.commit(); + committed = true; + } finally { + if (!committed) commitLease.abort(); + } + return Object.freeze({ + headDigest: preparation.headDigest, + rootDescriptorDigest: inventoryPlan.inventory.descriptorDigest, + version: preparation.head.version, + authoritySequence: preparation.head.authoritySequence, + inventoryWrites: inventoryPlan.inventoryWrites, + inventoryWriteBytes: inventoryPlan.inventoryWriteBytes, + }); +} diff --git a/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts b/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts new file mode 100644 index 0000000000..e9c03b5b14 --- /dev/null +++ b/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts @@ -0,0 +1,143 @@ +import { + type AssertionCoordinateV1, + type CanonicalGraphScopedAuthorSealV1, + type CatalogSealDeploymentProfileV1, +} from '@origintrail-official/dkg-core'; +import { + type AgentProfileActiveHeadObjectV1, + type AgentProfileVerifiedAuthoritySummaryV1, + type Digest32V1, + type NetworkIdV1, + type OwnedSubjectTableObjectV1, + type SignedAgentProfileHeadEnvelopeV1, + type SignedSystemRecordRootDescriptorEnvelopeV1, + type SystemRecordInventoryTreeSnapshotV1, + type SystemRecordObjectKindV1, + type SystemRecordPeerPublicKeyV1, +} from '@origintrail-official/dkg-core/system-record-v1'; +import type { Quad } from '@origintrail-official/dkg-storage'; + +import type { EvmPersonalMessageSignerV1 } from '../evm-message-signer-v1.js'; +import type { PreparedAgentProfileV1 } from '../profile.js'; +import type { SystemRecordArtifactV1 } from './artifact-v1.js'; + +export interface SystemRecordPeerSignerV1 { + readonly peerId: string; + readonly publicKey: SystemRecordPeerPublicKeyV1; + sign(message: Uint8Array): Promise; +} + +export type AgentProfilePublicationStatusV1 = 'tentative' | 'confirmed' | 'failed'; + +/** Untrusted legacy-publication result accepted at the producer boundary. */ +export interface AgentProfilePublicationBindingV1 { + readonly publicationStatus: AgentProfilePublicationStatusV1; + readonly assertionCoordinate: AssertionCoordinateV1; + readonly seal: Readonly; + readonly issuedAt: string; + readonly validUntil: string; + readonly projectionSchemaDigest: Digest32V1; +} + +export interface AgentProfileProducerInstallInputV1 { + readonly head: AgentProfileActiveHeadObjectV1; + readonly envelope: SignedAgentProfileHeadEnvelopeV1; + readonly canonicalProjectionBytes: Uint8Array; + readonly projectionQuads: readonly Readonly[]; + readonly ownedSubjectTable: OwnedSubjectTableObjectV1; + readonly verifiedAuthoritySummary: AgentProfileVerifiedAuthoritySummaryV1; + readonly signal: AbortSignal; +} + +export interface AgentProfileProducerPublicationV1 { + readonly headDigest: Digest32V1; + readonly rootDescriptorDigest: Digest32V1; + readonly version: string; + readonly authoritySequence: string; + readonly inventoryWrites: number; + readonly inventoryWriteBytes: number; +} + +export interface AgentProfileProducerPublicationCommitV1 { + /** Snapshot preconditions reserved before materialization begins. */ + readonly expectedHeadDigest: Digest32V1 | null; + readonly expectedRootDescriptorDigest: Digest32V1 | null; + readonly publicationArtifacts: AgentProfileProducerPublicationArtifactsV1; + readonly inventory: SystemRecordInventoryTreeSnapshotV1; + readonly rootEnvelope: SignedSystemRecordRootDescriptorEnvelopeV1; +} + +type AgentProfileProducerArtifactV1 = Readonly< + Omit & { objectKind: Kind } +>; + +export interface AgentProfileProducerPublicationArtifactsV1 { + readonly head: AgentProfileProducerArtifactV1<'agent-profile-head'>; + readonly bundle: AgentProfileProducerArtifactV1<'profile-bundle'>; + readonly ownedSubjectTable: AgentProfileProducerArtifactV1<'owned-subject-table'>; + readonly inventoryObjects: readonly AgentProfileProducerArtifactV1< + 'inventory-internal' | 'inventory-leaf' + >[]; +} + +export function flattenAgentProfileProducerPublicationArtifactsV1( + artifacts: AgentProfileProducerPublicationArtifactsV1, +): readonly SystemRecordArtifactV1[] { + return Object.freeze([ + artifacts.head, + artifacts.bundle, + artifacts.ownedSubjectTable, + ...artifacts.inventoryObjects, + ]); +} + +export interface AgentProfileProducerPublicationCommitLeaseV1 { + commit(): void | Promise; + abort(): void; +} + +export interface AgentProfileProducerPublicationStoreV1 { + snapshot(): Readonly<{ + inventory: SystemRecordInventoryTreeSnapshotV1 | null; + currentHead: SignedAgentProfileHeadEnvelopeV1 | null; + }>; + /** Resolve retained authority history by content address, without wire semantics. */ + resolveArtifact( + reference: Pick, + ): SystemRecordArtifactV1 | null | Promise; + /** Atomically verify and reserve the expected snapshot until commit or abort. */ + prepareCommit( + input: AgentProfileProducerPublicationCommitV1, + ): AgentProfileProducerPublicationCommitLeaseV1 | Promise; +} + +export interface CreateAgentProfileProducerOptionsV1 { + readonly networkId: NetworkIdV1; + /** Locally pinned VM publication lane; never derived from an untrusted seal. */ + readonly publicationDeployment: Readonly; + readonly peerSigner: SystemRecordPeerSignerV1; + readonly evmSigner: EvmPersonalMessageSignerV1; + readonly store: AgentProfileProducerPublicationStoreV1; + /** Independent verifier clock; publication timestamps are untrusted input. */ + readonly nowMs?: () => number; + /** Storage-runtime bridge: fence before publish; successful install commits advertisement. */ + readonly fence: ( + prepared: PreparedAgentProfileV1, + signal: AbortSignal, + ) => void | Promise; + readonly install: (input: AgentProfileProducerInstallInputV1) => void | Promise; +} + +export interface AgentProfileProducerLeaseV1 { + complete( + publication: AgentProfilePublicationBindingV1, + ): Promise; + abort(reason?: unknown): void; +} + +export interface AgentProfileProducerV1 { + /** Fence one immutable profile before the legacy publication begins. */ + prepare( + prepared: PreparedAgentProfileV1, + ): Promise; +} diff --git a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts new file mode 100644 index 0000000000..724ad577f6 --- /dev/null +++ b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts @@ -0,0 +1,217 @@ +import { + buildAgentProfileVerificationClosureV1, + buildSystemRecordInventoryTreeV1, + buildSystemRecordProviderSignatureMessageV1, + canonicalizeSystemRecordRootDescriptorObjectV1, + computeSystemRecordStableKeyHashV1, + updateSystemRecordInventoryTreeV1, + verifySignedSystemRecordEnvelopeV1, + verifySignedSystemRecordRootDescriptorEnvelopeV1, + type AgentProfileAuthorityTransitionV1, + type AgentProfileForkResolutionV1, + type AgentProfileHeadObjectV1, + type AgentProfileVerifiedAuthoritySummaryV1, + type Digest32V1, + type SignedSystemRecordRootDescriptorEnvelopeV1, + type SystemRecordInventoryRowV1, + type SystemRecordInventoryTreeSnapshotV1, + type SystemRecordObjectKindV1, +} from '@origintrail-official/dkg-core/system-record-v1'; + +import { + flattenAgentProfileProducerPublicationArtifactsV1, + type AgentProfileProducerPublicationArtifactsV1, + type CreateAgentProfileProducerOptionsV1, +} from './agent-profile-producer-contract-v1.js'; +import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; +import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; +import { + systemRecordArtifactKeyV1, + type SystemRecordArtifactV1, +} from './artifact-v1.js'; + +type AgentProfileProducerArtifactV1 = Readonly< + Omit & { objectKind: Kind } +>; + +export interface AgentProfileProductionInventoryV1 { + readonly inventory: SystemRecordInventoryTreeSnapshotV1; + readonly rootEnvelope: SignedSystemRecordRootDescriptorEnvelopeV1; + readonly publicationArtifacts: AgentProfileProducerPublicationArtifactsV1; + readonly verifiedAuthoritySummary: AgentProfileVerifiedAuthoritySummaryV1; + readonly inventoryWrites: number; + readonly inventoryWriteBytes: number; +} + +export async function prepareAgentProfileProductionInventoryV1( + options: CreateAgentProfileProducerOptionsV1, + preparation: AgentProfileProductionPreparationV1, + signed: SignedAgentProfileProductionV1, + signal: AbortSignal, +): Promise { + const row: SystemRecordInventoryRowV1 = { + stableKeyHash: computeSystemRecordStableKeyHashV1( + options.networkId, + options.peerSigner.peerId, + ), + peerId: options.peerSigner.peerId, + authoritySequence: preparation.head.authoritySequence, + version: preparation.head.version, + headDigest: preparation.headDigest, + tombstone: false, + quarantined: false, + }; + const inventoryUpdate = preparation.snapshot.inventory === null + ? null + : updateSystemRecordInventoryTreeV1(preparation.snapshot.inventory, { + operation: 'upsert', + row, + }); + const inventory = inventoryUpdate === null + ? buildSystemRecordInventoryTreeV1(options.networkId, [row]) + : applyInventoryUpdate(preparation.snapshot.inventory!, inventoryUpdate); + const inventoryWrites = inventoryUpdate?.writes.length + ?? inventory.objects.size + 1; + const inventoryWriteBytes = inventoryUpdate?.accounting.encodedBytes + ?? [...inventory.objects.values()].reduce( + (sum, object) => sum + object.canonicalBytes.byteLength, + 0, + ) + canonicalizeSystemRecordRootDescriptorObjectV1(inventory.descriptor).byteLength; + const rootSignature = await options.peerSigner.sign( + buildSystemRecordProviderSignatureMessageV1( + inventory.descriptor, + inventory.descriptorDigest, + options.peerSigner.peerId, + ), + ); + signal.throwIfAborted(); + const rootEnvelope: SignedSystemRecordRootDescriptorEnvelopeV1 = { + object: inventory.descriptor, + objectDigest: inventory.descriptorDigest, + providerPeerId: options.peerSigner.peerId, + signatureSuite: 'ed25519-v1', + signature: Buffer.from(rootSignature).toString('base64url'), + }; + if (!await verifySignedSystemRecordRootDescriptorEnvelopeV1( + rootEnvelope, + options.peerSigner.publicKey, + )) { + throw new Error('new profile inventory root signature verification failed'); + } + const publicationArtifacts = publicationArtifactSet({ + signed, + preparation, + inventory, + inventoryUpdate, + }); + const artifacts = flattenAgentProfileProducerPublicationArtifactsV1(publicationArtifacts); + const artifactsByKey = new Map( + artifacts.map((artifact) => [systemRecordArtifactKeyV1(artifact), artifact]), + ); + const verifiedClosure = await buildAgentProfileVerificationClosureV1( + preparation.headDigest, + { + nowMs: preparation.verifierNowMs, + resolve: async ({ objectKind, digest }) => { + const reference = { + objectKind, + objectDigest: digest, + } as const; + const artifact = artifactsByKey.get(systemRecordArtifactKeyV1(reference)) + ?? await options.store.resolveArtifact(reference); + return artifact === undefined + || artifact === null + ? undefined + : { + objectKind: artifact.objectKind, + digest: artifact.objectDigest, + canonicalBytes: Uint8Array.from(artifact.canonicalBytes), + }; + }, + verifyAuthorityEnvelope: (envelope) => verifySignedSystemRecordEnvelopeV1< + AgentProfileHeadObjectV1 | AgentProfileAuthorityTransitionV1 | AgentProfileForkResolutionV1 + >(envelope), + verifyCurrentBundle: (_candidate, canonicalBundleBytes) => + Buffer.from(canonicalBundleBytes).equals(Buffer.from(preparation.bundle)), + }, + ); + signal.throwIfAborted(); + return Object.freeze({ + inventory, + rootEnvelope, + publicationArtifacts, + verifiedAuthoritySummary: verifiedClosure.authoritySummary, + inventoryWrites, + inventoryWriteBytes, + }); +} + +interface PublicationArtifactSetInputV1 { + readonly signed: SignedAgentProfileProductionV1; + readonly preparation: AgentProfileProductionPreparationV1; + readonly inventory: SystemRecordInventoryTreeSnapshotV1; + readonly inventoryUpdate: ReturnType | null; +} + +function publicationArtifactSet( + input: PublicationArtifactSetInputV1, +): AgentProfileProducerPublicationArtifactsV1 { + const inventoryObjects = input.inventoryUpdate === null + ? [...input.inventory.objects.entries()].map(([digest, stored]) => ({ digest, ...stored })) + : input.inventoryUpdate.writes; + return Object.freeze({ + head: freezeArtifact( + 'agent-profile-head', + input.signed.envelope.objectDigest, + input.signed.envelopeBytes, + ), + bundle: freezeArtifact( + 'profile-bundle', + input.preparation.bundleDigest, + input.preparation.bundle, + ), + ownedSubjectTable: freezeArtifact( + 'owned-subject-table', + input.preparation.ownedSubjectTableDigest, + input.preparation.ownedSubjectTableBytes, + ), + inventoryObjects: Object.freeze(inventoryObjects.map((object) => freezeArtifact( + object.objectKind, + object.digest, + object.canonicalBytes, + ))), + }); +} + +function freezeArtifact( + objectKind: Kind, + objectDigest: Digest32V1, + bytes: Uint8Array, +): AgentProfileProducerArtifactV1 { + return Object.freeze({ + objectKind, + objectDigest, + canonicalBytes: Uint8Array.from(bytes), + }); +} + +function applyInventoryUpdate( + previous: SystemRecordInventoryTreeSnapshotV1, + update: ReturnType, +): SystemRecordInventoryTreeSnapshotV1 { + if (!update.changed) return previous; + const objects = new Map(previous.objects); + for (const write of update.writes) { + objects.set(write.digest, Object.freeze({ + objectKind: write.objectKind, + object: write.object, + canonicalBytes: Uint8Array.from(write.canonicalBytes), + })); + } + return Object.freeze({ + networkId: previous.networkId, + descriptor: update.descriptor, + descriptorDigest: update.descriptorDigest, + objects, + }); +} diff --git a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts new file mode 100644 index 0000000000..f733fc14c3 --- /dev/null +++ b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts @@ -0,0 +1,357 @@ +import { + SENTINEL_NO_PRIVATE_V10, + V10MerkleTree, + assertCanonicalDecimalU64, + assertCanonicalDigest, + assertCanonicalEvmAddress, + canonicalizeCanonicalGraphScopedAuthorSealV1, + encodeCanonicalCgSharedPublicRootProjectionV1, + encodeOpaqueKaBundleV1, + keccak256, + parseDeterministicKnowledgeAssetUal, + tripleContentV10, + type CanonicalGraphScopedAuthorSealV1, + type CatalogSealDeploymentProfileV1, +} from '@origintrail-official/dkg-core'; +import { + SYSTEM_RECORD_DIGEST_DOMAINS_V1, + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS, + SYSTEM_RECORD_OBJECT_CAPS_V1, + assertAgentProfileProjectionIdentityV1, + assertAgentProfileProjectionSchemaV1, + assertCanonicalRfc3339SecondsV1, + canonicalizeOwnedSubjectTableObjectV1, + computeAgentProfileHeadObjectDigestV1, + computeOwnedSubjectTableDigestV1, + digestSystemRecordBytesV1, + verifySignedSystemRecordEnvelopeV1, + type AgentProfileActiveHeadObjectV1, + type CanonicalRfc3339SecondsV1, + type Digest32V1, + type NetworkIdV1, + type OwnedSubjectTableObjectV1, +} from '@origintrail-official/dkg-core/system-record-v1'; +import type { Quad } from '@origintrail-official/dkg-storage'; + +import type { PreparedAgentProfileV1 } from '../profile.js'; +import { assertRecoverableGraphScopedAuthorAttestationV1 } from '../rfc64/recoverable-author-attestation-v1.js'; +import type { + AgentProfilePublicationBindingV1, + CreateAgentProfileProducerOptionsV1, + SystemRecordPeerSignerV1, +} from './agent-profile-producer-contract-v1.js'; + +const UTF8 = new TextEncoder(); + +export interface AgentProfileProductionPreparationV1 { + readonly snapshot: ReturnType; + readonly verifierNowMs: number; + readonly projectionQuads: readonly Readonly[]; + readonly projectionBytes: Uint8Array; + readonly ownedSubjectTable: OwnedSubjectTableObjectV1; + readonly ownedSubjectTableBytes: Uint8Array; + readonly ownedSubjectTableDigest: Digest32V1; + readonly bundle: Uint8Array; + readonly bundleDigest: Digest32V1; + readonly head: AgentProfileActiveHeadObjectV1; + readonly headDigest: Digest32V1; +} + +export async function prepareAgentProfileProductionV1( + options: CreateAgentProfileProducerOptionsV1, + prepared: PreparedAgentProfileV1, + projectionQuads: readonly Readonly[], + inputPublication: AgentProfilePublicationBindingV1, +): Promise { + const publication = snapshotConfirmedPublicationBindingV1(inputPublication); + const issuedAt = normalizePublicationTimestampV1(publication.issuedAt, 'issuedAt'); + const validUntil = normalizePublicationTimestampV1(publication.validUntil, 'validUntil'); + normalizePublicationTimestampV1( + publication.seal.assertionFinalizedAt, + 'assertionFinalizedAt', + ); + const assertionFinalizedAtMs = Date.parse(publication.seal.assertionFinalizedAt); + const verifierNowMs = producerNowMs(options.nowMs?.() ?? Date.now()); + if (Date.parse(issuedAt) > verifierNowMs + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS) { + throw new Error('agent-profile issuedAt exceeds the future clock-skew bound'); + } + if (Date.parse(issuedAt) < assertionFinalizedAtMs) { + throw new Error('agent-profile issuedAt predates assertion finalization'); + } + if (Date.parse(validUntil) <= Date.parse(issuedAt)) { + throw new Error('agent-profile validUntil must be later than issuedAt'); + } + if (Date.parse(validUntil) <= verifierNowMs) { + throw new Error('agent-profile validUntil is already expired'); + } + const evmIssuer = options.evmSigner.address; + assertCanonicalEvmAddress(evmIssuer, 'profile EVM issuer'); + const snapshot = options.store.snapshot(); + const previous = snapshot.currentHead; + if (previous !== null + && (previous.object.networkId !== options.networkId + || previous.object.peerId !== options.peerSigner.peerId)) { + throw new Error('stored profile head belongs to a different stable record'); + } + if (previous !== null && !await verifySignedSystemRecordEnvelopeV1(previous)) { + throw new Error('stored profile head signature verification failed'); + } + if (previous?.object.state === 'tombstone') { + throw new Error('a tombstoned profile requires an explicit authority transition'); + } + if (previous !== null && previous.object.evmIssuer !== evmIssuer) { + throw new Error('agent-profile authority transition must be authored explicitly'); + } + const projectionBytes = encodeCanonicalCgSharedPublicRootProjectionV1(projectionQuads); + const contentDigest = computeProjectionContentDigest(projectionQuads); + if (contentDigest !== publication.seal.assertionMerkleRoot + || publication.seal.authorAddress !== evmIssuer + || publication.seal.publicTripleCount !== String(projectionQuads.length) + || publication.seal.privateTripleCount !== '0' + || publication.seal.privateMerkleRoot !== null) { + throw new Error('profile publication seal does not bind the exact public projection'); + } + assertPublicationLaneV1( + publication.seal, + options.networkId, + options.publicationDeployment, + evmIssuer, + ); + assertRecoverableGraphScopedAuthorAttestationV1(publication.seal); + const ownedSubjectTable = ownedSubjects(prepared.rootEntity, projectionQuads); + const ownedSubjectTableBytes = canonicalizeOwnedSubjectTableObjectV1( + prepared.rootEntity, + ownedSubjectTable, + ); + const ownedSubjectTableDigest = computeOwnedSubjectTableDigestV1( + prepared.rootEntity, + ownedSubjectTable, + ); + const sealBytes = UTF8.encode(canonicalizeCanonicalGraphScopedAuthorSealV1(publication.seal)); + const bundle = encodeOpaqueKaBundleV1(projectionBytes, sealBytes).bundleBytes; + if (bundle.byteLength > SYSTEM_RECORD_OBJECT_CAPS_V1['profile-bundle']) { + throw new Error('profile bundle exceeds the system-record V1 cap'); + } + const bundleDigest = digestSystemRecordBytesV1( + SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, + bundle, + ); + const authoritySequence = previous?.object.authoritySequence ?? '0'; + const version = previous === null + ? '0' + : (BigInt(previous.object.version) + 1n).toString(); + const ownedSubjectCount = String(ownedSubjectTable.length); + const projectionByteCount = String(projectionBytes.byteLength); + const projectionQuadCount = String(projectionQuads.length); + assertCanonicalDecimalU64(authoritySequence, 'profile authoritySequence'); + assertCanonicalDecimalU64(version, 'profile version'); + assertCanonicalDecimalU64(ownedSubjectCount, 'profile ownedSubjectCount'); + assertCanonicalDecimalU64(projectionByteCount, 'profile projectionBytes'); + assertCanonicalDecimalU64(projectionQuadCount, 'profile projectionQuads'); + if (previous !== null + && (previous.object.rootSubject !== prepared.rootEntity + || previous.object.projectionSchemaDigest !== publication.projectionSchemaDigest)) { + throw new Error('ordinary profile update changed its root or projection schema'); + } + const head: AgentProfileActiveHeadObjectV1 = { + objectType: 'agent-profile-head', + kind: 'agents', + state: 'active', + networkId: options.networkId, + peerId: options.peerSigner.peerId, + peerPublicKey: options.peerSigner.publicKey, + authoritySequence, + version, + ...(previous === null ? {} : { previousHeadDigest: previous.objectDigest }), + ...(previous?.object.acceptedTransitionDigest === undefined ? {} : { + acceptedTransitionDigest: previous.object.acceptedTransitionDigest, + }), + evmIssuer, + rootSubject: prepared.rootEntity, + projectionSchemaDigest: publication.projectionSchemaDigest, + issuedAt, + ownedSubjectTableDigest, + ownedSubjectCount, + projectionBytes: projectionByteCount, + projectionQuads: projectionQuadCount, + validUntil, + assertionCoordinate: publication.assertionCoordinate, + graphScopedAuthorSeal: publication.seal, + contentDigest, + bundleDigest, + }; + const headDigest = computeAgentProfileHeadObjectDigestV1(head); + return Object.freeze({ + snapshot, + verifierNowMs, + projectionQuads, + projectionBytes, + ownedSubjectTable, + ownedSubjectTableBytes, + ownedSubjectTableDigest, + bundle, + bundleDigest, + head, + headDigest, + }); +} + +export function validateAgentProfileProjectionV1( + prepared: PreparedAgentProfileV1, +): readonly Readonly[] { + const projected = prepared.projectionQuads.map((quad) => Object.freeze({ ...quad })); + projected.sort(compareQuads); + for (let index = 1; index < projected.length; index += 1) { + if (compareQuads(projected[index - 1]!, projected[index]!) === 0) { + throw new Error('profile projection must be canonical and duplicate-free'); + } + } + try { + assertAgentProfileProjectionSchemaV1( + prepared.rootEntity, + ownedSubjects(prepared.rootEntity, projected), + projected, + ); + } catch (cause) { + throw new Error('profile projection is outside schema V1', { cause }); + } + return Object.freeze(projected); +} + +export function snapshotPreparedProfileV1( + prepared: PreparedAgentProfileV1, +): PreparedAgentProfileV1 { + if (!Array.isArray(prepared.publicationQuads) + || !Array.isArray(prepared.projectionQuads) + || typeof prepared.rootEntity !== 'string' + || typeof prepared.lastSeen !== 'string') { + throw new TypeError('prepared profile has an invalid structural shape'); + } + return Object.freeze({ + publicationQuads: Object.freeze( + prepared.publicationQuads.map((quad) => Object.freeze({ ...quad })), + ), + projectionQuads: Object.freeze( + prepared.projectionQuads.map((quad) => Object.freeze({ ...quad })), + ), + rootEntity: prepared.rootEntity, + lastSeen: prepared.lastSeen, + }); +} + +export function assertAdvertisedAgentProfileIdentityV1( + rootSubject: string, + quads: readonly Readonly[], + peerSigner: SystemRecordPeerSignerV1, + evmAddress: string, +): void { + assertCanonicalEvmAddress(evmAddress, 'profile EVM issuer'); + assertAgentProfileProjectionIdentityV1({ + rootSubject, + peerId: peerSigner.peerId, + peerPublicKey: peerSigner.publicKey, + evmIssuer: evmAddress, + }, quads); +} + +type ConfirmedAgentProfilePublicationBindingV1 = Readonly< + Omit & { + readonly publicationStatus: 'confirmed'; + } +>; + +function snapshotConfirmedPublicationBindingV1( + publication: AgentProfilePublicationBindingV1, +): ConfirmedAgentProfilePublicationBindingV1 { + if (publication.publicationStatus !== 'confirmed') { + throw new Error('agent-profile system record requires a confirmed publication'); + } + return Object.freeze({ + publicationStatus: 'confirmed', + assertionCoordinate: publication.assertionCoordinate, + seal: Object.freeze({ ...publication.seal }), + issuedAt: publication.issuedAt, + validUntil: publication.validUntil, + projectionSchemaDigest: publication.projectionSchemaDigest, + }); +} + +function producerNowMs(value: number): number { + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error('agent-profile producer clock returned an invalid value'); + } + return value; +} + +function assertPublicationLaneV1( + seal: Readonly, + networkId: NetworkIdV1, + deployment: Readonly, + evmIssuer: string, +): void { + const ual = parseDeterministicKnowledgeAssetUal(seal.kaUal); + if (deployment.networkId !== networkId + || ual.ual !== seal.kaUal + || ual.chainId !== networkId + || ual.agentAddress !== evmIssuer + || seal.assertedAtChainId !== deployment.assertedAtChainId + || seal.assertedAtKav10Address !== deployment.assertedAtKav10Address) { + throw new Error('profile publication seal belongs to a different network or deployment'); + } +} + +function ownedSubjects( + rootSubject: string, + quads: readonly Readonly[], +): OwnedSubjectTableObjectV1 { + const subjects = [...new Set(quads.map((quad) => quad.subject))].sort(compareUtf8); + if (!subjects.includes(rootSubject)) { + throw new Error('profile projection does not contain its canonical root subject'); + } + return Object.freeze(subjects); +} + +function computeProjectionContentDigest( + quads: readonly Readonly[], +): Digest32V1 { + const leaves = quads.map((quad) => keccak256( + tripleContentV10(quad.subject, quad.predicate, quad.object), + )); + const root = V10MerkleTree.computeKARoot( + new V10MerkleTree(leaves).root, + SENTINEL_NO_PRIVATE_V10, + ); + const digest = `0x${Buffer.from(root).toString('hex')}`; + assertCanonicalDigest(digest, 'profile content digest'); + return digest; +} + +function compareQuads(left: Readonly, right: Readonly): number { + return Buffer.compare( + tripleContentV10(left.subject, left.predicate, left.object), + tripleContentV10(right.subject, right.predicate, right.object), + ); +} + +function compareUtf8(left: string, right: string): number { + return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8')); +} + +function normalizePublicationTimestampV1( + value: string, + label: string, +): CanonicalRfc3339SecondsV1 { + if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value)) { + throw new Error(`${label} must be an RFC3339 UTC timestamp`); + } + const millis = Date.parse(value); + if (!Number.isFinite(millis)) throw new Error(`${label} must be a valid RFC3339 UTC timestamp`); + const canonical = new Date(Math.floor(millis / 1_000) * 1_000) + .toISOString() + .replace('.000Z', 'Z'); + if (canonical !== `${value.slice(0, 19)}Z`) { + throw new Error(`${label} must be a calendar-valid RFC3339 UTC timestamp`); + } + assertCanonicalRfc3339SecondsV1(canonical, label); + return canonical; +} diff --git a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts new file mode 100644 index 0000000000..6580dfa2e1 --- /dev/null +++ b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts @@ -0,0 +1,65 @@ +import { + buildSystemRecordSignatureMessageV1, + canonicalizeSignedSystemRecordEnvelopeV1, + verifySignedSystemRecordEnvelopeV1, + type SignedAgentProfileHeadEnvelopeV1, +} from '@origintrail-official/dkg-core/system-record-v1'; + +import type { CreateAgentProfileProducerOptionsV1 } from './agent-profile-producer-contract-v1.js'; +import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; + +export interface SignedAgentProfileProductionV1 { + readonly envelope: SignedAgentProfileHeadEnvelopeV1; + readonly envelopeBytes: Uint8Array; +} + +export async function signAgentProfileProductionV1( + options: CreateAgentProfileProducerOptionsV1, + preparation: AgentProfileProductionPreparationV1, + signal: AbortSignal, +): Promise { + const [peerSignature, evmSignature] = await Promise.all([ + options.peerSigner.sign( + buildSystemRecordSignatureMessageV1( + preparation.head, + preparation.headDigest, + 'peer', + ), + ), + options.evmSigner.signMessage( + buildSystemRecordSignatureMessageV1( + preparation.head, + preparation.headDigest, + 'current-evm', + ), + ), + ]); + signal.throwIfAborted(); + const envelope: SignedAgentProfileHeadEnvelopeV1 = { + object: preparation.head, + objectDigest: preparation.headDigest, + signatures: Object.freeze([ + Object.freeze({ + role: 'peer', + suite: 'ed25519-v1', + signer: options.peerSigner.peerId, + evidence: Object.freeze({ kind: 'none' }), + signature: Buffer.from(peerSignature).toString('base64url'), + }), + Object.freeze({ + role: 'current-evm', + suite: 'eip191-personal-sign-digest-v1', + signer: options.evmSigner.address, + evidence: Object.freeze({ kind: 'none' }), + signature: evmSignature, + }), + ]), + }; + if (!await verifySignedSystemRecordEnvelopeV1(envelope)) { + throw new Error('new profile head signature verification failed'); + } + return Object.freeze({ + envelope, + envelopeBytes: canonicalizeSignedSystemRecordEnvelopeV1(envelope), + }); +} diff --git a/packages/agent/src/system-records/agent-profile-producer-v1.ts b/packages/agent/src/system-records/agent-profile-producer-v1.ts index e7e9e01686..f364d0b609 100644 --- a/packages/agent/src/system-records/agent-profile-producer-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-v1.ts @@ -1,190 +1,26 @@ // SPDX-License-Identifier: Apache-2.0 -import { - SENTINEL_NO_PRIVATE_V10, - V10MerkleTree, - canonicalizeCanonicalGraphScopedAuthorSealV1, - encodeCanonicalCgSharedPublicRootProjectionV1, - encodeOpaqueKaBundleV1, - keccak256, - parseDeterministicKnowledgeAssetUal, - tripleContentV10, - assertCanonicalDecimalU64, - assertCanonicalDigest, - assertCanonicalEvmAddress, - type AssertionCoordinateV1, - type CanonicalGraphScopedAuthorSealV1, - type CatalogSealDeploymentProfileV1, -} from '@origintrail-official/dkg-core'; -import { - SYSTEM_RECORD_DIGEST_DOMAINS_V1, - SYSTEM_RECORD_MAX_CLOCK_SKEW_MS, - SYSTEM_RECORD_OBJECT_CAPS_V1, - assertAgentProfileProjectionIdentityV1, - assertAgentProfileProjectionSchemaV1, - assertCanonicalRfc3339SecondsV1, - buildSystemRecordProviderSignatureMessageV1, - buildSystemRecordSignatureMessageV1, - buildSystemRecordInventoryTreeV1, - buildAgentProfileVerificationClosureV1, - canonicalizeOwnedSubjectTableObjectV1, - canonicalizeSignedSystemRecordEnvelopeV1, - canonicalizeSystemRecordRootDescriptorObjectV1, - computeAgentProfileHeadObjectDigestV1, - computeOwnedSubjectTableDigestV1, - computeSystemRecordStableKeyHashV1, - digestSystemRecordBytesV1, - updateSystemRecordInventoryTreeV1, - verifySignedSystemRecordEnvelopeV1, - verifySignedSystemRecordRootDescriptorEnvelopeV1, - type AgentProfileActiveHeadObjectV1, - type AgentProfileAuthorityTransitionV1, - type AgentProfileForkResolutionV1, - type AgentProfileHeadObjectV1, - type AgentProfileVerifiedAuthoritySummaryV1, - type CanonicalRfc3339SecondsV1, - type Digest32V1, - type NetworkIdV1, - type OwnedSubjectTableObjectV1, - type SignedAgentProfileHeadEnvelopeV1, - type SignedSystemRecordRootDescriptorEnvelopeV1, - type SystemRecordInventoryRowV1, - type SystemRecordInventoryTreeSnapshotV1, - type SystemRecordObjectKindV1, - type SystemRecordPeerPublicKeyV1, -} from '@origintrail-official/dkg-core/system-record-v1'; import type { Quad } from '@origintrail-official/dkg-storage'; -import type { EvmPersonalMessageSignerV1 } from '../evm-message-signer-v1.js'; import type { PreparedAgentProfileV1 } from '../profile.js'; -import { assertRecoverableGraphScopedAuthorAttestationV1 } from '../rfc64/recoverable-author-attestation-v1.js'; +import { commitAgentProfileProductionV1 } from './agent-profile-producer-commit-v1.js'; +import type { + AgentProfileProducerLeaseV1, + AgentProfileProducerPublicationV1, + AgentProfileProducerV1, + AgentProfilePublicationBindingV1, + CreateAgentProfileProducerOptionsV1, +} from './agent-profile-producer-contract-v1.js'; +import { prepareAgentProfileProductionInventoryV1 } from './agent-profile-producer-inventory-v1.js'; import { - systemRecordArtifactKeyV1, - type SystemRecordArtifactV1, -} from './artifact-v1.js'; - -const UTF8 = new TextEncoder(); - -export interface SystemRecordPeerSignerV1 { - readonly peerId: string; - readonly publicKey: SystemRecordPeerPublicKeyV1; - sign(message: Uint8Array): Promise; -} - -export type AgentProfilePublicationStatusV1 = 'tentative' | 'confirmed' | 'failed'; - -/** Untrusted legacy-publication result accepted at the producer boundary. */ -export interface AgentProfilePublicationBindingV1 { - readonly publicationStatus: AgentProfilePublicationStatusV1; - readonly assertionCoordinate: AssertionCoordinateV1; - readonly seal: Readonly; - readonly issuedAt: string; - readonly validUntil: string; - readonly projectionSchemaDigest: Digest32V1; -} - -export interface AgentProfileProducerInstallInputV1 { - readonly head: AgentProfileActiveHeadObjectV1; - readonly envelope: SignedAgentProfileHeadEnvelopeV1; - readonly canonicalProjectionBytes: Uint8Array; - readonly projectionQuads: readonly Readonly[]; - readonly ownedSubjectTable: OwnedSubjectTableObjectV1; - readonly verifiedAuthoritySummary: AgentProfileVerifiedAuthoritySummaryV1; - readonly signal: AbortSignal; -} - -export interface AgentProfileProducerPublicationV1 { - readonly headDigest: Digest32V1; - readonly rootDescriptorDigest: Digest32V1; - readonly version: string; - readonly authoritySequence: string; - readonly inventoryWrites: number; - readonly inventoryWriteBytes: number; -} - -export interface AgentProfileProducerPublicationCommitV1 { - /** Snapshot preconditions reserved before materialization begins. */ - readonly expectedHeadDigest: Digest32V1 | null; - readonly expectedRootDescriptorDigest: Digest32V1 | null; - readonly publicationArtifacts: AgentProfileProducerPublicationArtifactsV1; - readonly inventory: SystemRecordInventoryTreeSnapshotV1; - readonly rootEnvelope: SignedSystemRecordRootDescriptorEnvelopeV1; -} - -type AgentProfileProducerArtifactV1 = Readonly< - Omit & { objectKind: Kind } ->; - -export interface AgentProfileProducerPublicationArtifactsV1 { - readonly head: AgentProfileProducerArtifactV1<'agent-profile-head'>; - readonly bundle: AgentProfileProducerArtifactV1<'profile-bundle'>; - readonly ownedSubjectTable: AgentProfileProducerArtifactV1<'owned-subject-table'>; - readonly inventoryObjects: readonly AgentProfileProducerArtifactV1< - 'inventory-internal' | 'inventory-leaf' - >[]; -} + assertAdvertisedAgentProfileIdentityV1, + prepareAgentProfileProductionV1, + snapshotPreparedProfileV1, + validateAgentProfileProjectionV1, +} from './agent-profile-producer-preparation-v1.js'; +import { signAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; -export function flattenAgentProfileProducerPublicationArtifactsV1( - artifacts: AgentProfileProducerPublicationArtifactsV1, -): readonly SystemRecordArtifactV1[] { - return Object.freeze([ - artifacts.head, - artifacts.bundle, - artifacts.ownedSubjectTable, - ...artifacts.inventoryObjects, - ]); -} - -export interface AgentProfileProducerPublicationCommitLeaseV1 { - commit(): void | Promise; - abort(): void; -} - -export interface AgentProfileProducerPublicationStoreV1 { - snapshot(): Readonly<{ - inventory: SystemRecordInventoryTreeSnapshotV1 | null; - currentHead: SignedAgentProfileHeadEnvelopeV1 | null; - }>; - /** Resolve retained authority history by content address, without wire semantics. */ - resolveArtifact( - reference: Pick, - ): SystemRecordArtifactV1 | null | Promise; - /** Atomically verify and reserve the expected snapshot until commit or abort. */ - prepareCommit( - input: AgentProfileProducerPublicationCommitV1, - ): AgentProfileProducerPublicationCommitLeaseV1 | Promise; -} - -export interface CreateAgentProfileProducerOptionsV1 { - readonly networkId: NetworkIdV1; - /** Locally pinned VM publication lane; never derived from an untrusted seal. */ - readonly publicationDeployment: Readonly; - readonly peerSigner: SystemRecordPeerSignerV1; - readonly evmSigner: EvmPersonalMessageSignerV1; - readonly store: AgentProfileProducerPublicationStoreV1; - /** Independent verifier clock; publication timestamps are untrusted input. */ - readonly nowMs?: () => number; - /** Storage-runtime bridge: fence before publish; successful install commits advertisement. */ - readonly fence: ( - prepared: PreparedAgentProfileV1, - signal: AbortSignal, - ) => void | Promise; - readonly install: (input: AgentProfileProducerInstallInputV1) => void | Promise; -} - -export interface AgentProfileProducerLeaseV1 { - complete( - publication: AgentProfilePublicationBindingV1, - ): Promise; - abort(reason?: unknown): void; -} - -export interface AgentProfileProducerV1 { - /** Fence one immutable profile before the legacy publication begins. */ - prepare( - prepared: PreparedAgentProfileV1, - ): Promise; -} +export * from './agent-profile-producer-contract-v1.js'; /** * Author one local profile record. No protocol, timer, queue, or independent @@ -197,292 +33,38 @@ export function createAgentProfileProducerV1( const completePrepared = async ( prepared: PreparedAgentProfileV1, projectionQuads: readonly Readonly[], - inputPublication: AgentProfilePublicationBindingV1, + publication: AgentProfilePublicationBindingV1, signal: AbortSignal, ): Promise => { signal.throwIfAborted(); - const publication = snapshotConfirmedPublicationBindingV1(inputPublication); - const issuedAt = normalizePublicationTimestampV1(publication.issuedAt, 'issuedAt'); - const validUntil = normalizePublicationTimestampV1(publication.validUntil, 'validUntil'); - normalizePublicationTimestampV1( - publication.seal.assertionFinalizedAt, - 'assertionFinalizedAt', + const preparation = await prepareAgentProfileProductionV1( + options, + prepared, + projectionQuads, + publication, ); - const assertionFinalizedAtMs = Date.parse(publication.seal.assertionFinalizedAt); - const verifierNowMs = producerNowMs(options.nowMs?.() ?? Date.now()); - if (Date.parse(issuedAt) > verifierNowMs + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS) { - throw new Error('agent-profile issuedAt exceeds the future clock-skew bound'); - } - if (Date.parse(issuedAt) < assertionFinalizedAtMs) { - throw new Error('agent-profile issuedAt predates assertion finalization'); - } - if (Date.parse(validUntil) <= Date.parse(issuedAt)) { - throw new Error('agent-profile validUntil must be later than issuedAt'); - } - if (Date.parse(validUntil) <= verifierNowMs) { - throw new Error('agent-profile validUntil is already expired'); - } - const evmIssuer = options.evmSigner.address; - assertCanonicalEvmAddress(evmIssuer, 'profile EVM issuer'); - const snapshot = options.store.snapshot(); - const previous = snapshot.currentHead; - if (previous !== null - && (previous.object.networkId !== options.networkId - || previous.object.peerId !== options.peerSigner.peerId)) { - throw new Error('stored profile head belongs to a different stable record'); - } - if (previous !== null && !await verifySignedSystemRecordEnvelopeV1(previous)) { - throw new Error('stored profile head signature verification failed'); - } - if (previous?.object.state === 'tombstone') { - throw new Error('a tombstoned profile requires an explicit authority transition'); - } - if (previous !== null && previous.object.evmIssuer !== evmIssuer) { - throw new Error('agent-profile authority transition must be authored explicitly'); - } - const projectionBytes = encodeCanonicalCgSharedPublicRootProjectionV1(projectionQuads); - const contentDigest = computeProjectionContentDigest(projectionQuads); - if (contentDigest !== publication.seal.assertionMerkleRoot - || publication.seal.authorAddress !== evmIssuer - || publication.seal.publicTripleCount !== String(projectionQuads.length) - || publication.seal.privateTripleCount !== '0' - || publication.seal.privateMerkleRoot !== null) { - throw new Error('profile publication seal does not bind the exact public projection'); - } - assertPublicationLaneV1( - publication.seal, - options.networkId, - options.publicationDeployment, - evmIssuer, + const signed = await signAgentProfileProductionV1(options, preparation, signal); + const inventoryPlan = await prepareAgentProfileProductionInventoryV1( + options, + preparation, + signed, + signal, ); - assertRecoverableGraphScopedAuthorAttestationV1(publication.seal); - const ownedSubjectTable = ownedSubjects(prepared.rootEntity, projectionQuads); - const ownedSubjectTableBytes = canonicalizeOwnedSubjectTableObjectV1( - prepared.rootEntity, - ownedSubjectTable, + return commitAgentProfileProductionV1( + options, + preparation, + signed, + inventoryPlan, + signal, ); - const ownedSubjectTableDigest = computeOwnedSubjectTableDigestV1( - prepared.rootEntity, - ownedSubjectTable, - ); - const sealBytes = UTF8.encode(canonicalizeCanonicalGraphScopedAuthorSealV1(publication.seal)); - const bundle = encodeOpaqueKaBundleV1(projectionBytes, sealBytes).bundleBytes; - if (bundle.byteLength > SYSTEM_RECORD_OBJECT_CAPS_V1['profile-bundle']) { - throw new Error('profile bundle exceeds the system-record V1 cap'); - } - const bundleDigest = digestSystemRecordBytesV1( - SYSTEM_RECORD_DIGEST_DOMAINS_V1.profileBundle, - bundle, - ); - const authoritySequence = previous?.object.authoritySequence ?? '0'; - const version = previous === null - ? '0' - : (BigInt(previous.object.version) + 1n).toString(); - const ownedSubjectCount = String(ownedSubjectTable.length); - const projectionByteCount = String(projectionBytes.byteLength); - const projectionQuadCount = String(projectionQuads.length); - assertCanonicalDecimalU64(authoritySequence, 'profile authoritySequence'); - assertCanonicalDecimalU64(version, 'profile version'); - assertCanonicalDecimalU64(ownedSubjectCount, 'profile ownedSubjectCount'); - assertCanonicalDecimalU64(projectionByteCount, 'profile projectionBytes'); - assertCanonicalDecimalU64(projectionQuadCount, 'profile projectionQuads'); - if (previous !== null - && (previous.object.rootSubject !== prepared.rootEntity - || previous.object.projectionSchemaDigest !== publication.projectionSchemaDigest)) { - throw new Error('ordinary profile update changed its root or projection schema'); - } - const head: AgentProfileActiveHeadObjectV1 = { - objectType: 'agent-profile-head', - kind: 'agents', - state: 'active', - networkId: options.networkId, - peerId: options.peerSigner.peerId, - peerPublicKey: options.peerSigner.publicKey, - authoritySequence, - version, - ...(previous === null ? {} : { previousHeadDigest: previous.objectDigest }), - ...(previous?.object.acceptedTransitionDigest === undefined ? {} : { - acceptedTransitionDigest: previous.object.acceptedTransitionDigest, - }), - evmIssuer, - rootSubject: prepared.rootEntity, - projectionSchemaDigest: publication.projectionSchemaDigest, - issuedAt, - ownedSubjectTableDigest, - ownedSubjectCount, - projectionBytes: projectionByteCount, - projectionQuads: projectionQuadCount, - validUntil, - assertionCoordinate: publication.assertionCoordinate, - graphScopedAuthorSeal: publication.seal, - contentDigest, - bundleDigest, - }; - const headDigest = computeAgentProfileHeadObjectDigestV1(head); - const [peerSignature, evmSignature] = await Promise.all([ - options.peerSigner.sign( - buildSystemRecordSignatureMessageV1(head, headDigest, 'peer'), - ), - options.evmSigner.signMessage( - buildSystemRecordSignatureMessageV1(head, headDigest, 'current-evm'), - ), - ]); - signal.throwIfAborted(); - const envelope: SignedAgentProfileHeadEnvelopeV1 = { - object: head, - objectDigest: headDigest, - signatures: Object.freeze([ - Object.freeze({ - role: 'peer', - suite: 'ed25519-v1', - signer: options.peerSigner.peerId, - evidence: Object.freeze({ kind: 'none' }), - signature: Buffer.from(peerSignature).toString('base64url'), - }), - Object.freeze({ - role: 'current-evm', - suite: 'eip191-personal-sign-digest-v1', - signer: options.evmSigner.address, - evidence: Object.freeze({ kind: 'none' }), - signature: evmSignature, - }), - ]), - }; - if (!await verifySignedSystemRecordEnvelopeV1(envelope)) { - throw new Error('new profile head signature verification failed'); - } - const envelopeBytes = canonicalizeSignedSystemRecordEnvelopeV1(envelope); - const row: SystemRecordInventoryRowV1 = { - stableKeyHash: computeSystemRecordStableKeyHashV1( - options.networkId, - options.peerSigner.peerId, - ), - peerId: options.peerSigner.peerId, - authoritySequence: head.authoritySequence, - version: head.version, - headDigest, - tombstone: false, - quarantined: false, - }; - const inventoryUpdate = snapshot.inventory === null - ? null - : updateSystemRecordInventoryTreeV1(snapshot.inventory, { - operation: 'upsert', - row, - }); - const inventory = inventoryUpdate === null - ? buildSystemRecordInventoryTreeV1(options.networkId, [row]) - : applyInventoryUpdate(snapshot.inventory!, inventoryUpdate); - const inventoryWrites = inventoryUpdate?.writes.length - ?? inventory.objects.size + 1; - const inventoryWriteBytes = inventoryUpdate?.accounting.encodedBytes - ?? [...inventory.objects.values()].reduce( - (sum, object) => sum + object.canonicalBytes.byteLength, - 0, - ) + canonicalizeSystemRecordRootDescriptorObjectV1(inventory.descriptor).byteLength; - const rootSignature = await options.peerSigner.sign( - buildSystemRecordProviderSignatureMessageV1( - inventory.descriptor, - inventory.descriptorDigest, - options.peerSigner.peerId, - ), - ); - signal.throwIfAborted(); - const rootEnvelope: SignedSystemRecordRootDescriptorEnvelopeV1 = { - object: inventory.descriptor, - objectDigest: inventory.descriptorDigest, - providerPeerId: options.peerSigner.peerId, - signatureSuite: 'ed25519-v1', - signature: Buffer.from(rootSignature).toString('base64url'), - }; - if (!await verifySignedSystemRecordRootDescriptorEnvelopeV1( - rootEnvelope, - options.peerSigner.publicKey, - )) { - throw new Error('new profile inventory root signature verification failed'); - } - const publicationArtifactSet = publicationArtifacts({ - envelope, - envelopeBytes, - bundle, - bundleDigest, - ownedSubjectTableBytes, - ownedSubjectTableDigest, - inventory, - inventoryUpdate, - }); - const artifacts = flattenAgentProfileProducerPublicationArtifactsV1(publicationArtifactSet); - const artifactsByKey = new Map( - artifacts.map((artifact) => [systemRecordArtifactKeyV1(artifact), artifact]), - ); - const verifiedClosure = await buildAgentProfileVerificationClosureV1(headDigest, { - nowMs: verifierNowMs, - resolve: async ({ objectKind, digest }) => { - const reference = { - objectKind, - objectDigest: digest, - } as const; - const artifact = artifactsByKey.get(systemRecordArtifactKeyV1(reference)) - ?? await options.store.resolveArtifact(reference); - return artifact === undefined - || artifact === null - ? undefined - : { - objectKind: artifact.objectKind, - digest: artifact.objectDigest, - canonicalBytes: Uint8Array.from(artifact.canonicalBytes), - }; - }, - verifyAuthorityEnvelope: (envelope) => verifySignedSystemRecordEnvelopeV1< - AgentProfileHeadObjectV1 | AgentProfileAuthorityTransitionV1 | AgentProfileForkResolutionV1 - >(envelope), - verifyCurrentBundle: (_candidate, canonicalBundleBytes) => - Buffer.from(canonicalBundleBytes).equals(Buffer.from(bundle)), - }); - signal.throwIfAborted(); - - const commitLease = await options.store.prepareCommit({ - expectedHeadDigest: previous?.objectDigest ?? null, - expectedRootDescriptorDigest: snapshot.inventory?.descriptorDigest ?? null, - publicationArtifacts: publicationArtifactSet, - inventory, - rootEnvelope, - }); - let committed = false; - try { - signal.throwIfAborted(); - await options.install({ - head, - envelope, - canonicalProjectionBytes: projectionBytes, - projectionQuads, - ownedSubjectTable, - verifiedAuthoritySummary: verifiedClosure.authoritySummary, - signal, - }); - // Installation is the point of no return: a late abort cannot roll it back, - // so the already-reserved advertisement must commit to keep both views aligned. - await commitLease.commit(); - committed = true; - } finally { - if (!committed) commitLease.abort(); - } - return Object.freeze({ - headDigest, - rootDescriptorDigest: inventory.descriptorDigest, - version, - authoritySequence, - inventoryWrites, - inventoryWriteBytes, - }); }; return Object.freeze({ async prepare(prepared: PreparedAgentProfileV1): Promise { if (active) throw new Error('agent-profile producer is busy'); const preparedSnapshot = snapshotPreparedProfileV1(prepared); - const projectionQuads = validateAndProject(preparedSnapshot); - assertAdvertisedIdentity( + const projectionQuads = validateAgentProfileProjectionV1(preparedSnapshot); + assertAdvertisedAgentProfileIdentityV1( preparedSnapshot.rootEntity, projectionQuads, options.peerSigner, @@ -527,227 +109,3 @@ export function createAgentProfileProducerV1( }, }); } - -interface PublicationArtifactsInputV1 { - readonly envelope: SignedAgentProfileHeadEnvelopeV1; - readonly envelopeBytes: Uint8Array; - readonly bundle: Uint8Array; - readonly bundleDigest: Digest32V1; - readonly ownedSubjectTableBytes: Uint8Array; - readonly ownedSubjectTableDigest: Digest32V1; - readonly inventory: SystemRecordInventoryTreeSnapshotV1; - readonly inventoryUpdate: ReturnType | null; -} - -function publicationArtifacts( - input: PublicationArtifactsInputV1, -): AgentProfileProducerPublicationArtifactsV1 { - const inventoryObjects = input.inventoryUpdate === null - ? [...input.inventory.objects.entries()].map(([digest, stored]) => ({ digest, ...stored })) - : input.inventoryUpdate.writes; - return Object.freeze({ - head: freezeArtifact('agent-profile-head', input.envelope.objectDigest, input.envelopeBytes), - bundle: freezeArtifact('profile-bundle', input.bundleDigest, input.bundle), - ownedSubjectTable: freezeArtifact( - 'owned-subject-table', - input.ownedSubjectTableDigest, - input.ownedSubjectTableBytes, - ), - inventoryObjects: Object.freeze(inventoryObjects.map((object) => freezeArtifact( - object.objectKind, - object.digest, - object.canonicalBytes, - ))), - }); -} - -function freezeArtifact( - objectKind: Kind, - objectDigest: Digest32V1, - bytes: Uint8Array, -): AgentProfileProducerArtifactV1 { - return Object.freeze({ - objectKind, - objectDigest, - canonicalBytes: Uint8Array.from(bytes), - }); -} - -function applyInventoryUpdate( - previous: SystemRecordInventoryTreeSnapshotV1, - update: ReturnType, -): SystemRecordInventoryTreeSnapshotV1 { - if (!update.changed) return previous; - const objects = new Map(previous.objects); - for (const write of update.writes) { - objects.set(write.digest, Object.freeze({ - objectKind: write.objectKind, - object: write.object, - canonicalBytes: Uint8Array.from(write.canonicalBytes), - })); - } - return Object.freeze({ - networkId: previous.networkId, - descriptor: update.descriptor, - descriptorDigest: update.descriptorDigest, - objects, - }); -} - -function validateAndProject( - prepared: PreparedAgentProfileV1, -): readonly Readonly[] { - const projected = prepared.projectionQuads.map((quad) => Object.freeze({ ...quad })); - projected.sort(compareQuads); - for (let index = 1; index < projected.length; index += 1) { - if (compareQuads(projected[index - 1]!, projected[index]!) === 0) { - throw new Error('profile projection must be canonical and duplicate-free'); - } - } - try { - assertAgentProfileProjectionSchemaV1( - prepared.rootEntity, - ownedSubjects(prepared.rootEntity, projected), - projected, - ); - } catch (cause) { - throw new Error('profile projection is outside schema V1', { cause }); - } - return Object.freeze(projected); -} - -function snapshotPreparedProfileV1(prepared: PreparedAgentProfileV1): PreparedAgentProfileV1 { - if (!Array.isArray(prepared.publicationQuads) - || !Array.isArray(prepared.projectionQuads) - || typeof prepared.rootEntity !== 'string' - || typeof prepared.lastSeen !== 'string') { - throw new TypeError('prepared profile has an invalid structural shape'); - } - return Object.freeze({ - publicationQuads: Object.freeze( - prepared.publicationQuads.map((quad) => Object.freeze({ ...quad })), - ), - projectionQuads: Object.freeze( - prepared.projectionQuads.map((quad) => Object.freeze({ ...quad })), - ), - rootEntity: prepared.rootEntity, - lastSeen: prepared.lastSeen, - }); -} - -type ConfirmedAgentProfilePublicationBindingV1 = Readonly< - Omit & { - readonly publicationStatus: 'confirmed'; - } ->; - -function snapshotConfirmedPublicationBindingV1( - publication: AgentProfilePublicationBindingV1, -): ConfirmedAgentProfilePublicationBindingV1 { - if (publication.publicationStatus !== 'confirmed') { - throw new Error('agent-profile system record requires a confirmed publication'); - } - return Object.freeze({ - publicationStatus: 'confirmed', - assertionCoordinate: publication.assertionCoordinate, - seal: Object.freeze({ ...publication.seal }), - issuedAt: publication.issuedAt, - validUntil: publication.validUntil, - projectionSchemaDigest: publication.projectionSchemaDigest, - }); -} - -function producerNowMs(value: number): number { - if (!Number.isSafeInteger(value) || value < 0) { - throw new Error('agent-profile producer clock returned an invalid value'); - } - return value; -} - -function assertPublicationLaneV1( - seal: Readonly, - networkId: NetworkIdV1, - deployment: Readonly, - evmIssuer: string, -): void { - const ual = parseDeterministicKnowledgeAssetUal(seal.kaUal); - if (deployment.networkId !== networkId - || ual.ual !== seal.kaUal - || ual.chainId !== networkId - || ual.agentAddress !== evmIssuer - || seal.assertedAtChainId !== deployment.assertedAtChainId - || seal.assertedAtKav10Address !== deployment.assertedAtKav10Address) { - throw new Error('profile publication seal belongs to a different network or deployment'); - } -} - -function ownedSubjects( - rootSubject: string, - quads: readonly Readonly[], -): OwnedSubjectTableObjectV1 { - const subjects = [...new Set(quads.map((quad) => quad.subject))].sort(compareUtf8); - if (!subjects.includes(rootSubject)) { - throw new Error('profile projection does not contain its canonical root subject'); - } - return Object.freeze(subjects); -} - -function computeProjectionContentDigest( - quads: readonly Readonly[], -): Digest32V1 { - const leaves = quads.map((quad) => keccak256( - tripleContentV10(quad.subject, quad.predicate, quad.object), - )); - const root = V10MerkleTree.computeKARoot( - new V10MerkleTree(leaves).root, - SENTINEL_NO_PRIVATE_V10, - ); - const digest = `0x${Buffer.from(root).toString('hex')}`; - assertCanonicalDigest(digest, 'profile content digest'); - return digest; -} - -function compareQuads(left: Readonly, right: Readonly): number { - return Buffer.compare( - tripleContentV10(left.subject, left.predicate, left.object), - tripleContentV10(right.subject, right.predicate, right.object), - ); -} - -function compareUtf8(left: string, right: string): number { - return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8')); -} - -function normalizePublicationTimestampV1( - value: string, - label: string, -): CanonicalRfc3339SecondsV1 { - if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value)) { - throw new Error(`${label} must be an RFC3339 UTC timestamp`); - } - const millis = Date.parse(value); - if (!Number.isFinite(millis)) throw new Error(`${label} must be a valid RFC3339 UTC timestamp`); - const canonical = new Date(Math.floor(millis / 1_000) * 1_000) - .toISOString() - .replace('.000Z', 'Z'); - if (canonical !== `${value.slice(0, 19)}Z`) { - throw new Error(`${label} must be a calendar-valid RFC3339 UTC timestamp`); - } - assertCanonicalRfc3339SecondsV1(canonical, label); - return canonical; -} - -function assertAdvertisedIdentity( - rootSubject: string, - quads: readonly Readonly[], - peerSigner: SystemRecordPeerSignerV1, - evmAddress: string, -): void { - assertCanonicalEvmAddress(evmAddress, 'profile EVM issuer'); - assertAgentProfileProjectionIdentityV1({ - rootSubject, - peerId: peerSigner.peerId, - peerPublicKey: peerSigner.publicKey, - evmIssuer: evmAddress, - }, quads); -} diff --git a/packages/agent/test/system-record-agent-profile-producer-authority-v1.test.ts b/packages/agent/test/system-record-agent-profile-producer-authority-v1.test.ts new file mode 100644 index 0000000000..38a6a5e013 --- /dev/null +++ b/packages/agent/test/system-record-agent-profile-producer-authority-v1.test.ts @@ -0,0 +1,336 @@ +import { + EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, + parseCanonicalSignedAgentProfileHeadEnvelopeV1, + type AgentProfileAuthorityTransitionV1, + type AgentProfileHeadObjectV1, + type Digest32V1, +} from '@origintrail-official/dkg-core/system-record-v1'; +import { ethers } from 'ethers'; +import { + describe, + expect, + it, + vi, +} from 'vitest'; +import { createEvmPersonalMessageSignerV1 } from '../src/evm-message-signer-v1.js'; +import { + type AgentProfileProducerPublicationCommitV1, + type AgentProfileProducerPublicationStoreV1, +} from '../src/system-records/agent-profile-producer-v1.js'; +import { createInMemoryAgentProfilePublicationStoreV1 } from '../src/system-records/in-memory-agent-profile-publication-store-v1.js'; +import { + systemRecordArtifactKeyV1, + type SystemRecordArtifactV1, +} from '../src/system-records/artifact-v1.js'; +import { + DEPLOYMENT, + NETWORK, + OTHER_PRIVATE_KEY, + createFixtureAgentProfileProducerV1 as createAgentProfileProducerV1, + envelopeArtifact, + makePrepared, + produce, + producerFixture, + publicationFor, + signHeadEnvelope, + signTransitionEnvelope, +} from './support/agent-profile-producer-v1-fixture.js'; + + +describe('agent-profile system-record producer V1 authority and lineage', () => { + it('rejects an ordinary update over a signed tombstone before install or commit', async () => { + const fixture = await producerFixture(); + const initialProducer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + await produce(initialProducer, fixture.prepared, fixture.publication); + const activeEnvelope = fixture.store.snapshot().currentHead!; + const active = activeEnvelope.object; + const tombstone = { + objectType: 'agent-profile-head', + kind: 'agents', + state: 'tombstone', + networkId: active.networkId, + peerId: active.peerId, + peerPublicKey: active.peerPublicKey, + authoritySequence: active.authoritySequence, + version: '1', + previousHeadDigest: activeEnvelope.objectDigest, + evmIssuer: active.evmIssuer, + rootSubject: active.rootSubject, + projectionSchemaDigest: active.projectionSchemaDigest, + issuedAt: '2026-08-07T12:10:00Z', + ownedSubjectTableDigest: EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, + ownedSubjectCount: '0', + projectionBytes: '0', + projectionQuads: '0', + } as AgentProfileHeadObjectV1; + const tombstoneEnvelope = await signHeadEnvelope( + tombstone, + fixture.peerSigner, + fixture.evmSigner, + ); + const prepareCommit = vi.fn(fixture.store.prepareCommit.bind(fixture.store)); + const store: AgentProfileProducerPublicationStoreV1 = { + snapshot: () => { + const snapshot = fixture.store.snapshot(); + return Object.freeze({ ...snapshot, currentHead: tombstoneEnvelope }); + }, + resolveArtifact: (reference) => fixture.store.resolveArtifact(reference), + prepareCommit, + }; + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store, + fence: () => {}, + install, + }); + const nextPrepared = makePrepared( + fixture.peerSigner, + fixture.evmSigner.address, + '2026-08-07T12:20:00.000Z', + ); + const lease = await producer.prepare(nextPrepared); + + await expect(lease.complete(await publicationFor( + nextPrepared, + fixture.evmSigner.address, + '2026-08-07T12:20:00Z', + ))).rejects.toThrow(/tombstoned profile/); + expect(install).not.toHaveBeenCalled(); + expect(prepareCommit).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead?.objectDigest).toBe(activeEnvelope.objectDigest); + }); + + it('rejects a concurrent stale writer before installing or replacing the winning head', async () => { + const fixture = await producerFixture(); + const install = vi.fn(); + const firstProducer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + const secondProducer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + const laterPublication = await publicationFor( + fixture.prepared, + fixture.evmSigner.address, + '2026-08-07T12:01:00Z', + ); + + const outcomes = await Promise.allSettled([ + produce(firstProducer, fixture.prepared, fixture.publication), + produce(secondProducer, fixture.prepared, laterPublication), + ]); + const fulfilled = outcomes.filter( + (outcome): outcome is PromiseFulfilledResult>> => + outcome.status === 'fulfilled', + ); + const rejected = outcomes.filter( + (outcome): outcome is PromiseRejectedResult => outcome.status === 'rejected', + ); + + expect(fulfilled).toHaveLength(1); + expect(rejected).toHaveLength(1); + expect(String(rejected[0]!.reason)).toMatch(/prepared commit|snapshot is stale/); + expect(install).toHaveBeenCalledTimes(1); + expect(fixture.store.snapshot().currentHead?.objectDigest) + .toBe(fulfilled[0]!.value.headDigest); + }); + + it('rejects ordinary update authority and schema changes without replacing the head', async () => { + const fixture = await producerFixture(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + const first = await produce(producer, fixture.prepared, fixture.publication); + const nextPrepared = makePrepared( + fixture.peerSigner, + fixture.evmSigner.address, + '2026-08-07T12:20:00.000Z', + ); + const nextPublication = await publicationFor( + nextPrepared, + fixture.evmSigner.address, + '2026-08-07T12:20:00Z', + ); + await expect(produce(producer, nextPrepared, { + ...nextPublication, + projectionSchemaDigest: `0x${'cd'.repeat(32)}` as Digest32V1, + })).rejects.toThrow(/changed its root or projection schema/); + expect(fixture.store.snapshot().currentHead?.objectDigest).toBe(first.headDigest); + + const otherSigner = createEvmPersonalMessageSignerV1({ + mode: 'custodial', + address: new ethers.Wallet(OTHER_PRIVATE_KEY).address, + privateKey: OTHER_PRIVATE_KEY, + purpose: 'system-record alternate authority test', + }); + const otherPrepared = makePrepared( + fixture.peerSigner, + otherSigner.address, + '2026-08-07T12:40:00.000Z', + ); + const otherProducer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: otherSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + const lease = await otherProducer.prepare(otherPrepared); + await expect(lease.complete(nextPublication)).rejects.toThrow(/authority transition/); + expect(fixture.store.snapshot().currentHead?.objectDigest).toBe(first.headDigest); + }); + + it('preserves verified authority lineage on a post-transition heartbeat', async () => { + const prior = await producerFixture(); + const priorProducer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: prior.peerSigner, + evmSigner: prior.evmSigner, + store: prior.store, + fence: () => {}, + install: () => {}, + }); + await produce(priorProducer, prior.prepared, prior.publication); + const priorEnvelope = prior.store.snapshot().currentHead!; + + const nextSigner = createEvmPersonalMessageSignerV1({ + mode: 'custodial', + address: new ethers.Wallet(OTHER_PRIVATE_KEY).address, + privateKey: OTHER_PRIVATE_KEY, + purpose: 'post-transition profile test', + }); + const transitionedPrepared = makePrepared( + prior.peerSigner, + nextSigner.address, + '2026-08-07T12:20:00.000Z', + ); + const transitionedPublication = await publicationFor( + transitionedPrepared, + nextSigner.address, + '2026-08-07T12:20:00Z', + OTHER_PRIVATE_KEY, + ); + const bootstrapStore = createInMemoryAgentProfilePublicationStoreV1(); + const bootstrapProducer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: prior.peerSigner, + evmSigner: nextSigner, + store: bootstrapStore, + fence: () => {}, + install: () => {}, + }); + await produce(bootstrapProducer, transitionedPrepared, transitionedPublication); + const bootstrapEnvelope = bootstrapStore.snapshot().currentHead!; + + const transition: AgentProfileAuthorityTransitionV1 = { + objectType: 'authority-transition', + kind: 'agents', + mode: 'co-signed', + networkId: NETWORK, + peerId: prior.peerSigner.peerId, + peerPublicKey: prior.peerSigner.publicKey, + priorAuthoritySequence: '0', + nextAuthoritySequence: '1', + priorHeadDigest: priorEnvelope.objectDigest, + priorEvmIssuer: prior.evmSigner.address, + nextEvmIssuer: nextSigner.address, + nextRoot: transitionedPrepared.rootEntity, + issuedAt: '2026-08-07T12:10:00Z', + }; + const transitionEnvelope = await signTransitionEnvelope( + transition, + prior.peerSigner, + prior.evmSigner, + nextSigner, + ); + const transitionedEnvelope = await signHeadEnvelope({ + ...bootstrapEnvelope.object, + authoritySequence: '1', + acceptedTransitionDigest: transitionEnvelope.objectDigest, + }, prior.peerSigner, nextSigner); + const history = new Map(); + for (const artifact of [ + envelopeArtifact('agent-profile-head', priorEnvelope), + envelopeArtifact('authority-transition', transitionEnvelope), + ]) { + history.set(systemRecordArtifactKeyV1(artifact), artifact); + } + let pendingCommit: AgentProfileProducerPublicationCommitV1 | null = null; + const store: AgentProfileProducerPublicationStoreV1 = { + snapshot: () => Object.freeze({ inventory: null, currentHead: transitionedEnvelope }), + resolveArtifact: (reference) => history.get(systemRecordArtifactKeyV1(reference)) ?? null, + prepareCommit: (input) => { + pendingCommit = input; + return Object.freeze({ commit: () => {}, abort: () => {} }); + }, + }; + const heartbeatPrepared = makePrepared( + prior.peerSigner, + nextSigner.address, + '2026-08-07T12:30:00.000Z', + ); + const heartbeatProducer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: prior.peerSigner, + evmSigner: nextSigner, + store, + fence: () => {}, + install: () => {}, + }); + + const result = await produce( + heartbeatProducer, + heartbeatPrepared, + await publicationFor( + heartbeatPrepared, + nextSigner.address, + '2026-08-07T12:30:00Z', + OTHER_PRIVATE_KEY, + ), + ); + const headArtifact = pendingCommit!.publicationArtifacts.head; + const heartbeatEnvelope = parseCanonicalSignedAgentProfileHeadEnvelopeV1( + headArtifact.canonicalBytes, + ); + expect(result).toMatchObject({ authoritySequence: '1', version: '1' }); + expect(heartbeatEnvelope.object.acceptedTransitionDigest) + .toBe(transitionEnvelope.objectDigest); + expect(heartbeatEnvelope.object.previousHeadDigest).toBe(transitionedEnvelope.objectDigest); + }); + +}); diff --git a/packages/agent/test/system-record-agent-profile-producer-lifecycle-v1.test.ts b/packages/agent/test/system-record-agent-profile-producer-lifecycle-v1.test.ts new file mode 100644 index 0000000000..de8a05ead5 --- /dev/null +++ b/packages/agent/test/system-record-agent-profile-producer-lifecycle-v1.test.ts @@ -0,0 +1,287 @@ +import { + describe, + expect, + it, + vi, +} from 'vitest'; +import { type PreparedAgentProfileV1 } from '../src/profile.js'; +import { + DEPLOYMENT, + NETWORK, + createFixtureAgentProfileProducerV1 as createAgentProfileProducerV1, + producerFixture, +} from './support/agent-profile-producer-v1-fixture.js'; + + +describe('agent-profile system-record producer V1 lifecycle and schema admission', () => { + it('fences before publication and an aborted lease releases the local single-flight', async () => { + const fixture = await producerFixture(); + const fence = vi.fn(); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence, + install, + }); + const first = await producer.prepare(fixture.prepared); + expect(fence).toHaveBeenCalledTimes(1); + await expect(producer.prepare(fixture.prepared)).rejects.toThrow(/busy/); + first.abort(); + const retry = await producer.prepare(fixture.prepared); + retry.abort(); + expect(fence).toHaveBeenCalledTimes(2); + expect(install).not.toHaveBeenCalled(); + }); + + it('releases the local single-flight when the publication fence rejects', async () => { + const fixture = await producerFixture(); + const fence = vi.fn() + .mockRejectedValueOnce(new Error('transient fence failure')) + .mockResolvedValue(undefined); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence, + install, + }); + + await expect(producer.prepare(fixture.prepared)).rejects.toThrow(/transient fence failure/); + const retry = await producer.prepare(fixture.prepared); + retry.abort(); + expect(fence).toHaveBeenCalledTimes(2); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('aborts a reserved commit when cancellation races a blocked install', async () => { + const fixture = await producerFixture(); + const installStarted = Promise.withResolvers(); + const releaseInstall = Promise.withResolvers(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: async ({ signal }) => { + installStarted.resolve(); + await releaseInstall.promise; + signal.throwIfAborted(); + }, + }); + + const lease = await producer.prepare(fixture.prepared); + const completion = lease.complete(fixture.publication); + await installStarted.promise; + lease.abort(new Error('cancel blocked install')); + releaseInstall.resolve(); + + await expect(completion).rejects.toThrow(/cancel blocked install/); + expect(fixture.store.snapshot().currentHead).toBeNull(); + expect(fixture.store.snapshot().inventory).toBeNull(); + + const retry = await producer.prepare(fixture.prepared); + await expect(retry.complete(fixture.publication)).resolves.toMatchObject({ version: '0' }); + }); + + it('commits the advertisement when cancellation arrives after successful installation', async () => { + const fixture = await producerFixture(); + let lease: Awaited>; + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => { lease.abort(new Error('late cancellation')); }, + }); + + lease = await producer.prepare(fixture.prepared); + await expect(lease.complete(fixture.publication)).resolves.toMatchObject({ version: '0' }); + expect(fixture.store.snapshot().currentHead?.object.version).toBe('0'); + expect(fixture.store.snapshot().inventory).not.toBeNull(); + }); + + it('rejects duplicate canonical profile triples before fencing publication', async () => { + const fixture = await producerFixture(); + const fence = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence, + install: () => {}, + }); + const duplicate = Object.freeze({ + ...fixture.prepared, + projectionQuads: Object.freeze([ + ...fixture.prepared.projectionQuads, + fixture.prepared.projectionQuads[0]!, + ]), + }); + + await expect(producer.prepare(duplicate)).rejects.toThrow(/duplicate-free/); + expect(fence).not.toHaveBeenCalled(); + }); + + it('rejects an out-of-schema profile predicate before fencing publication', async () => { + const fixture = await producerFixture(); + const fence = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence, + install: () => {}, + }); + const outOfSchema = Object.freeze({ + ...fixture.prepared, + projectionQuads: Object.freeze([ + ...fixture.prepared.projectionQuads, + Object.freeze({ + subject: fixture.prepared.rootEntity, + predicate: 'https://example.org/unapproved', + object: '"x"', + graph: '', + }), + ]), + }); + + await expect(producer.prepare(outOfSchema)).rejects.toThrow(/outside schema V1/); + expect(fence).not.toHaveBeenCalled(); + }); + + it.each([ + { + label: 'a literal profile link', + mutate: (prepared: PreparedAgentProfileV1) => prepared.projectionQuads.map((quad) => ( + quad.predicate === 'http://www.w3.org/ns/prov#wasGeneratedBy' + ? { ...quad, object: '"not-an-iri"' } + : quad + )), + }, + { + label: 'an unapproved rdf:type object', + mutate: (prepared: PreparedAgentProfileV1) => prepared.projectionQuads.map((quad) => ( + quad.subject === prepared.rootEntity + && quad.predicate === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' + && quad.object === 'https://dkg.network/ontology#Agent' + ? { ...quad, object: 'https://example.org/InvalidAgentType' } + : quad + )), + }, + { + label: 'an underived x25519 revocation subject', + mutate: (prepared: PreparedAgentProfileV1) => [ + ...prepared.projectionQuads, + { + subject: prepared.rootEntity, + predicate: 'https://dkg.network/ontology#publicEncryptionKey', + object: `"${Buffer.alloc(32, 9).toString('base64url')}"`, + graph: '', + }, + { + subject: `${prepared.rootEntity}#x25519-${'0'.repeat(32)}`, + predicate: 'https://dkg.network/ontology#revokedAt', + object: '"2026-08-07T12:00:00Z"', + graph: '', + }, + ], + }, + ])('rejects $label before fencing publication', async ({ mutate }) => { + const fixture = await producerFixture(); + const fence = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence, + install: () => {}, + }); + const malformed = Object.freeze({ + ...fixture.prepared, + projectionQuads: Object.freeze(mutate(fixture.prepared)), + }); + + await expect(producer.prepare(malformed)).rejects.toThrow(/outside schema V1/); + expect(fence).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it.each([ + ['peerId', '"12D3KooWRhLYc1qpzVncrVpMkykB3ML1PoQ9G9gX9X9G9gX9X9G"'], + ['agentAddress', `"0x${'33'.repeat(20)}"`], + ['publicKey', '"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="'], + ])('rejects a mismatched advertised %s before fencing publication', async (field, object) => { + const fixture = await producerFixture(); + const fence = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence, + install: () => {}, + }); + const predicate = `https://dkg.network/ontology#${field}`; + const mismatched = Object.freeze({ + ...fixture.prepared, + projectionQuads: Object.freeze(fixture.prepared.projectionQuads.map((quad) => Object.freeze( + quad.predicate === predicate ? { ...quad, object } : quad, + ))), + }); + + await expect(producer.prepare(mismatched)).rejects.toThrow(/does not bind the signed/); + expect(fence).not.toHaveBeenCalled(); + }); + + it.each([ + ['peerId', '"12D3KooWRhLYc1qpzVncrVpMkykB3ML1PoQ9G9gX9X9G9gX9X9G"'], + ['agentAddress', `"0x${'33'.repeat(20)}"`], + ['publicKey', '"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="'], + ])('rejects a conflicting advertised %s before fencing publication', async (field, object) => { + const fixture = await producerFixture(); + const fence = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence, + install: () => {}, + }); + const conflicting = Object.freeze({ + ...fixture.prepared, + projectionQuads: Object.freeze([ + ...fixture.prepared.projectionQuads, + Object.freeze({ + subject: fixture.prepared.rootEntity, + predicate: `https://dkg.network/ontology#${field}`, + object, + graph: '', + }), + ]), + }); + + await expect(producer.prepare(conflicting)).rejects.toThrow(/does not bind the signed/); + expect(fence).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/agent/test/system-record-agent-profile-producer-publication-v1.test.ts b/packages/agent/test/system-record-agent-profile-producer-publication-v1.test.ts new file mode 100644 index 0000000000..0c7fddfd48 --- /dev/null +++ b/packages/agent/test/system-record-agent-profile-producer-publication-v1.test.ts @@ -0,0 +1,286 @@ +import { + parseCanonicalSignedAgentProfileHeadEnvelopeV1, + type SignedAgentProfileHeadEnvelopeV1, +} from '@origintrail-official/dkg-core/system-record-v1'; +import { + describe, + expect, + it, + vi, +} from 'vitest'; +import { prepareAgentProfileV1 } from '../src/profile.js'; +import { type AgentProfileProducerPublicationStoreV1 } from '../src/system-records/agent-profile-producer-v1.js'; +import { + DEPLOYMENT, + NETWORK, + createFixtureAgentProfileProducerV1 as createAgentProfileProducerV1, + controlRequest, + makePrepared, + observingStore, + produce, + producerFixture, + publicationFor, + rootRequest, +} from './support/agent-profile-producer-v1-fixture.js'; + + +describe('agent-profile system-record producer V1 publication and inventory', () => { + it('stages one exact profile, installs it, then advertises the signed inventory root', async () => { + const fixture = await producerFixture(); + const events: string[] = []; + const store = observingStore(fixture.store, events); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store, + fence: () => { events.push('fence'); }, + install: (input) => { + events.push('install'); + expect(input.projectionQuads).toHaveLength(fixture.prepared.projectionQuads.length); + expect(input.head.rootSubject).toBe(fixture.prepared.rootEntity); + expect(input.verifiedAuthoritySummary.candidateHeadDigest).toBe(input.envelope.objectDigest); + }, + }); + + const result = await produce(producer, fixture.prepared, fixture.publication); + expect(result).toMatchObject({ version: '0', authoritySequence: '0' }); + expect(result.inventoryWrites).toBe(2); + expect(events).toEqual(['fence', 'install', 'advertise']); + + const snapshot = store.snapshot(); + expect(snapshot.currentHead?.objectDigest).toBe(result.headDigest); + expect(snapshot.inventory?.descriptorDigest).toBe(result.rootDescriptorDigest); + const head = await store.resolve(controlRequest(result.headDigest), new AbortController().signal); + expect(head?.objectKind).toBe('agent-profile-head'); + if (head === null) throw new Error('published profile head is missing'); + const parsedHead = parseCanonicalSignedAgentProfileHeadEnvelopeV1(head.canonicalBytes); + expect(parsedHead.object.version).toBe('0'); + const bundle = await store.resolve({ + type: 'object', objectKind: 'profile-bundle', objectDigest: parsedHead.object.bundleDigest, + }, new AbortController().signal); + expect(bundle?.objectKind).toBe('profile-bundle'); + const subjectTable = await store.resolve({ + type: 'object', objectKind: 'owned-subject-table', + objectDigest: parsedHead.object.ownedSubjectTableDigest, + }, new AbortController().signal); + expect(subjectTable?.objectKind).toBe('owned-subject-table'); + const root = await store.resolve(rootRequest(), new AbortController().signal); + expect(root?.objectKind).toBe('root-descriptor'); + expect(root?.objectDigest).toBe(result.rootDescriptorDigest); + const inventoryDigest = snapshot.inventory?.descriptor.treeRootDigest; + const inventoryObject = inventoryDigest === undefined + ? undefined + : snapshot.inventory?.objects.get(inventoryDigest); + expect(inventoryObject).toBeDefined(); + if (inventoryObject === undefined || inventoryDigest === undefined) { + throw new Error('published inventory object is missing'); + } + await expect(store.resolve({ + type: 'inventory-object', rootDescriptorDigest: result.rootDescriptorDigest, path: [], + objectKind: inventoryObject.objectKind, objectDigest: inventoryDigest, + }, new AbortController().signal)).resolves.toMatchObject({ + objectKind: inventoryObject.objectKind, + objectDigest: inventoryDigest, + }); + await expect(store.resolve({ + type: 'inventory-object', rootDescriptorDigest: `0x${'ff'.repeat(32)}`, path: [], + objectKind: inventoryObject.objectKind, objectDigest: inventoryDigest, + }, new AbortController().signal)).resolves.toBeNull(); + }); + + it('accepts the signed projection for advertised skills and hosted context graphs', async () => { + const fixture = await producerFixture(); + const prepared = prepareAgentProfileV1({ + peerId: fixture.peerSigner.peerId, + publicKey: Buffer.from(fixture.peerSigner.publicKey, 'base64url').toString('base64'), + agentAddress: fixture.evmSigner.address, + name: 'Feature profile fixture', + framework: 'Hermes', + nodeRole: 'edge', + lastSeen: '2026-08-07T12:00:00.000Z', + skills: [{ + skillType: 'ImageAnalysis', + pricePerCall: 1, + currency: 'TRAC', + successRate: 0.99, + pricingModel: 'PerInvocation', + }], + contextGraphsServed: ['public-image-analysis'], + }); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + + await expect(produce( + producer, + prepared, + await publicationFor(prepared, fixture.evmSigner.address, '2026-08-07T12:00:00Z'), + )).resolves.toMatchObject({ version: '0', authoritySequence: '0' }); + const installedSubjects = new Set( + install.mock.calls[0]![0].projectionQuads.map((quad) => quad.subject), + ); + expect(installedSubjects).toContain(`${prepared.rootEntity}/.well-known/genid/cap1`); + expect(installedSubjects).toContain(`${prepared.rootEntity}/.well-known/genid/offering1`); + expect(installedSubjects).toContain(`${prepared.rootEntity}/.well-known/genid/hosting`); + }); + + it('advances one COW path for an ordinary same-authority heartbeat', async () => { + const fixture = await producerFixture(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + const first = await produce(producer, fixture.prepared, fixture.publication); + const nextPrepared = makePrepared( + fixture.peerSigner, + fixture.evmSigner.address, + '2026-08-07T12:20:00.000Z', + ); + const second = await produce( + producer, + nextPrepared, + await publicationFor(nextPrepared, fixture.evmSigner.address, '2026-08-07T12:20:00Z'), + ); + + expect(second.version).toBe('1'); + expect(second.headDigest).not.toBe(first.headDigest); + expect(second.rootDescriptorDigest).not.toBe(first.rootDescriptorDigest); + expect(second.inventoryWrites).toBeLessThanOrEqual(6); + expect(second.inventoryWriteBytes).toBeLessThanOrEqual(1024 * 1024); + expect(fixture.store.snapshot().inventory?.descriptor.totalRows).toBe('1'); + }); + + it('serves an advertised inventory root after a newer root commits', async () => { + const fixture = await producerFixture(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + const first = await produce(producer, fixture.prepared, fixture.publication); + const firstInventory = fixture.store.snapshot().inventory!; + const firstTreeDigest = firstInventory.descriptor.treeRootDigest; + const firstTreeObject = firstInventory.objects.get(firstTreeDigest)!; + const nextPrepared = makePrepared( + fixture.peerSigner, + fixture.evmSigner.address, + '2026-08-07T12:20:00.000Z', + ); + + await produce( + producer, + nextPrepared, + await publicationFor(nextPrepared, fixture.evmSigner.address, '2026-08-07T12:20:00Z'), + ); + + await expect(fixture.store.resolve({ + type: 'inventory-object', + rootDescriptorDigest: first.rootDescriptorDigest, + path: [], + objectKind: firstTreeObject.objectKind, + objectDigest: firstTreeDigest, + }, new AbortController().signal)).resolves.toMatchObject({ + objectKind: firstTreeObject.objectKind, + objectDigest: firstTreeDigest, + }); + await expect(fixture.store.resolve({ + type: 'inventory-object', + rootDescriptorDigest: first.rootDescriptorDigest, + path: [0], + objectKind: firstTreeObject.objectKind, + objectDigest: firstTreeDigest, + }, new AbortController().signal)).resolves.toBeNull(); + }); + + it.each([ + [ + 'belongs to a different stable record', + (head: SignedAgentProfileHeadEnvelopeV1): SignedAgentProfileHeadEnvelopeV1 => ({ + ...head, + object: { ...head.object, peerId: '12D3KooWDifferentStableRecord111111111111111111111111' }, + }), + /different stable record/, + ], + [ + 'has an invalid signature', + (head: SignedAgentProfileHeadEnvelopeV1): SignedAgentProfileHeadEnvelopeV1 => ({ + ...head, + signatures: head.signatures.map((signature, index) => index === 0 + ? { ...signature, signature: Buffer.alloc(64).toString('base64url') } + : signature), + }), + /stored profile head signature verification failed/, + ], + ])('rejects a previous head that %s before installing or committing', async ( + _label, + mutateHead, + expected, + ) => { + const fixture = await producerFixture(); + const initialProducer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + await produce(initialProducer, fixture.prepared, fixture.publication); + const prepareCommit = vi.fn(fixture.store.prepareCommit.bind(fixture.store)); + const store: AgentProfileProducerPublicationStoreV1 = { + snapshot: () => { + const snapshot = fixture.store.snapshot(); + return Object.freeze({ + ...snapshot, + currentHead: mutateHead(snapshot.currentHead!), + }); + }, + resolveArtifact: (reference) => fixture.store.resolveArtifact(reference), + prepareCommit, + }; + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store, + fence: () => {}, + install, + }); + const nextPrepared = makePrepared( + fixture.peerSigner, + fixture.evmSigner.address, + '2026-08-07T12:20:00.000Z', + ); + const lease = await producer.prepare(nextPrepared); + + await expect(lease.complete(await publicationFor( + nextPrepared, + fixture.evmSigner.address, + '2026-08-07T12:20:00Z', + ))).rejects.toThrow(expected); + expect(install).not.toHaveBeenCalled(); + expect(prepareCommit).not.toHaveBeenCalled(); + }); + +}); diff --git a/packages/agent/test/system-record-agent-profile-producer-v1.test.ts b/packages/agent/test/system-record-agent-profile-producer-v1.test.ts deleted file mode 100644 index 9f0c58b157..0000000000 --- a/packages/agent/test/system-record-agent-profile-producer-v1.test.ts +++ /dev/null @@ -1,1521 +0,0 @@ -import { - EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, - SYSTEM_RECORD_MAX_CLOCK_SKEW_MS, - buildSystemRecordSignatureMessageV1, - canonicalizeSignedSystemRecordEnvelopeV1, - computeAgentProfileAuthorityTransitionDigestV1, - computeAgentProfileHeadObjectDigestV1, - parseCanonicalSignedAgentProfileHeadEnvelopeV1, - type AgentProfileAuthorityTransitionV1, - type AgentProfileHeadObjectV1, - type Digest32V1, - type SignedAgentProfileAuthorityTransitionEnvelopeV1, - type SignedAgentProfileHeadEnvelopeV1, - type SystemRecordPeerPublicKeyV1, -} from '@origintrail-official/dkg-core/system-record-v1'; -import { ethers } from 'ethers'; -import { describe, expect, it, vi } from 'vitest'; - -import { - createEvmPersonalMessageSignerV1, - type EvmPersonalMessageSignerV1, -} from '../src/evm-message-signer-v1.js'; -import { prepareAgentProfileV1, type PreparedAgentProfileV1 } from '../src/profile.js'; -import { - type AgentProfileProducerPublicationCommitV1, - type AgentProfileProducerPublicationStoreV1, - type AgentProfileProducerV1, - type AgentProfilePublicationBindingV1, - type SystemRecordPeerSignerV1, -} from '../src/system-records/agent-profile-producer-v1.js'; -import { - createInMemoryAgentProfilePublicationStoreV1, - type InMemoryAgentProfilePublicationStoreV1, -} from '../src/system-records/in-memory-agent-profile-publication-store-v1.js'; -import { - systemRecordArtifactKeyV1, - type SystemRecordArtifactV1, -} from '../src/system-records/artifact-v1.js'; -import { - DEPLOYMENT, - NETWORK, - OTHER_PRIVATE_KEY, - createFixtureAgentProfileProducerV1 as createAgentProfileProducerV1, - controlRequest, - envelopeArtifact, - makePrepared, - observingStore, - produce, - producerFixture, - publicationFor, - rootRequest, - signHeadEnvelope, - signTransitionEnvelope, -} from './support/agent-profile-producer-v1-fixture.js'; - -describe('agent-profile system-record producer V1', () => { - it('stages one exact profile, installs it, then advertises the signed inventory root', async () => { - const fixture = await producerFixture(); - const events: string[] = []; - const store = observingStore(fixture.store, events); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store, - fence: () => { events.push('fence'); }, - install: (input) => { - events.push('install'); - expect(input.projectionQuads).toHaveLength(fixture.prepared.projectionQuads.length); - expect(input.head.rootSubject).toBe(fixture.prepared.rootEntity); - expect(input.verifiedAuthoritySummary.candidateHeadDigest).toBe(input.envelope.objectDigest); - }, - }); - - const result = await produce(producer, fixture.prepared, fixture.publication); - expect(result).toMatchObject({ version: '0', authoritySequence: '0' }); - expect(result.inventoryWrites).toBe(2); - expect(events).toEqual(['fence', 'install', 'advertise']); - - const snapshot = store.snapshot(); - expect(snapshot.currentHead?.objectDigest).toBe(result.headDigest); - expect(snapshot.inventory?.descriptorDigest).toBe(result.rootDescriptorDigest); - const head = await store.resolve(controlRequest(result.headDigest), new AbortController().signal); - expect(head?.objectKind).toBe('agent-profile-head'); - if (head === null) throw new Error('published profile head is missing'); - const parsedHead = parseCanonicalSignedAgentProfileHeadEnvelopeV1(head.canonicalBytes); - expect(parsedHead.object.version).toBe('0'); - const bundle = await store.resolve({ - type: 'object', objectKind: 'profile-bundle', objectDigest: parsedHead.object.bundleDigest, - }, new AbortController().signal); - expect(bundle?.objectKind).toBe('profile-bundle'); - const subjectTable = await store.resolve({ - type: 'object', objectKind: 'owned-subject-table', - objectDigest: parsedHead.object.ownedSubjectTableDigest, - }, new AbortController().signal); - expect(subjectTable?.objectKind).toBe('owned-subject-table'); - const root = await store.resolve(rootRequest(), new AbortController().signal); - expect(root?.objectKind).toBe('root-descriptor'); - expect(root?.objectDigest).toBe(result.rootDescriptorDigest); - const inventoryDigest = snapshot.inventory?.descriptor.treeRootDigest; - const inventoryObject = inventoryDigest === undefined - ? undefined - : snapshot.inventory?.objects.get(inventoryDigest); - expect(inventoryObject).toBeDefined(); - if (inventoryObject === undefined || inventoryDigest === undefined) { - throw new Error('published inventory object is missing'); - } - await expect(store.resolve({ - type: 'inventory-object', rootDescriptorDigest: result.rootDescriptorDigest, path: [], - objectKind: inventoryObject.objectKind, objectDigest: inventoryDigest, - }, new AbortController().signal)).resolves.toMatchObject({ - objectKind: inventoryObject.objectKind, - objectDigest: inventoryDigest, - }); - await expect(store.resolve({ - type: 'inventory-object', rootDescriptorDigest: `0x${'ff'.repeat(32)}`, path: [], - objectKind: inventoryObject.objectKind, objectDigest: inventoryDigest, - }, new AbortController().signal)).resolves.toBeNull(); - }); - - it('accepts the signed projection for advertised skills and hosted context graphs', async () => { - const fixture = await producerFixture(); - const prepared = prepareAgentProfileV1({ - peerId: fixture.peerSigner.peerId, - publicKey: Buffer.from(fixture.peerSigner.publicKey, 'base64url').toString('base64'), - agentAddress: fixture.evmSigner.address, - name: 'Feature profile fixture', - framework: 'Hermes', - nodeRole: 'edge', - lastSeen: '2026-08-07T12:00:00.000Z', - skills: [{ - skillType: 'ImageAnalysis', - pricePerCall: 1, - currency: 'TRAC', - successRate: 0.99, - pricingModel: 'PerInvocation', - }], - contextGraphsServed: ['public-image-analysis'], - }); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - - await expect(produce( - producer, - prepared, - await publicationFor(prepared, fixture.evmSigner.address, '2026-08-07T12:00:00Z'), - )).resolves.toMatchObject({ version: '0', authoritySequence: '0' }); - const installedSubjects = new Set( - install.mock.calls[0]![0].projectionQuads.map((quad) => quad.subject), - ); - expect(installedSubjects).toContain(`${prepared.rootEntity}/.well-known/genid/cap1`); - expect(installedSubjects).toContain(`${prepared.rootEntity}/.well-known/genid/offering1`); - expect(installedSubjects).toContain(`${prepared.rootEntity}/.well-known/genid/hosting`); - }); - - it('advances one COW path for an ordinary same-authority heartbeat', async () => { - const fixture = await producerFixture(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - const first = await produce(producer, fixture.prepared, fixture.publication); - const nextPrepared = makePrepared( - fixture.peerSigner, - fixture.evmSigner.address, - '2026-08-07T12:20:00.000Z', - ); - const second = await produce( - producer, - nextPrepared, - await publicationFor(nextPrepared, fixture.evmSigner.address, '2026-08-07T12:20:00Z'), - ); - - expect(second.version).toBe('1'); - expect(second.headDigest).not.toBe(first.headDigest); - expect(second.rootDescriptorDigest).not.toBe(first.rootDescriptorDigest); - expect(second.inventoryWrites).toBeLessThanOrEqual(6); - expect(second.inventoryWriteBytes).toBeLessThanOrEqual(1024 * 1024); - expect(fixture.store.snapshot().inventory?.descriptor.totalRows).toBe('1'); - }); - - it('serves an advertised inventory root after a newer root commits', async () => { - const fixture = await producerFixture(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - const first = await produce(producer, fixture.prepared, fixture.publication); - const firstInventory = fixture.store.snapshot().inventory!; - const firstTreeDigest = firstInventory.descriptor.treeRootDigest; - const firstTreeObject = firstInventory.objects.get(firstTreeDigest)!; - const nextPrepared = makePrepared( - fixture.peerSigner, - fixture.evmSigner.address, - '2026-08-07T12:20:00.000Z', - ); - - await produce( - producer, - nextPrepared, - await publicationFor(nextPrepared, fixture.evmSigner.address, '2026-08-07T12:20:00Z'), - ); - - await expect(fixture.store.resolve({ - type: 'inventory-object', - rootDescriptorDigest: first.rootDescriptorDigest, - path: [], - objectKind: firstTreeObject.objectKind, - objectDigest: firstTreeDigest, - }, new AbortController().signal)).resolves.toMatchObject({ - objectKind: firstTreeObject.objectKind, - objectDigest: firstTreeDigest, - }); - await expect(fixture.store.resolve({ - type: 'inventory-object', - rootDescriptorDigest: first.rootDescriptorDigest, - path: [0], - objectKind: firstTreeObject.objectKind, - objectDigest: firstTreeDigest, - }, new AbortController().signal)).resolves.toBeNull(); - }); - - it.each([ - [ - 'belongs to a different stable record', - (head: SignedAgentProfileHeadEnvelopeV1): SignedAgentProfileHeadEnvelopeV1 => ({ - ...head, - object: { ...head.object, peerId: '12D3KooWDifferentStableRecord111111111111111111111111' }, - }), - /different stable record/, - ], - [ - 'has an invalid signature', - (head: SignedAgentProfileHeadEnvelopeV1): SignedAgentProfileHeadEnvelopeV1 => ({ - ...head, - signatures: head.signatures.map((signature, index) => index === 0 - ? { ...signature, signature: Buffer.alloc(64).toString('base64url') } - : signature), - }), - /stored profile head signature verification failed/, - ], - ])('rejects a previous head that %s before installing or committing', async ( - _label, - mutateHead, - expected, - ) => { - const fixture = await producerFixture(); - const initialProducer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - await produce(initialProducer, fixture.prepared, fixture.publication); - const prepareCommit = vi.fn(fixture.store.prepareCommit.bind(fixture.store)); - const store: AgentProfileProducerPublicationStoreV1 = { - snapshot: () => { - const snapshot = fixture.store.snapshot(); - return Object.freeze({ - ...snapshot, - currentHead: mutateHead(snapshot.currentHead!), - }); - }, - resolveArtifact: (reference) => fixture.store.resolveArtifact(reference), - prepareCommit, - }; - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store, - fence: () => {}, - install, - }); - const nextPrepared = makePrepared( - fixture.peerSigner, - fixture.evmSigner.address, - '2026-08-07T12:20:00.000Z', - ); - const lease = await producer.prepare(nextPrepared); - - await expect(lease.complete(await publicationFor( - nextPrepared, - fixture.evmSigner.address, - '2026-08-07T12:20:00Z', - ))).rejects.toThrow(expected); - expect(install).not.toHaveBeenCalled(); - expect(prepareCommit).not.toHaveBeenCalled(); - }); - - it('rejects an ordinary update over a signed tombstone before install or commit', async () => { - const fixture = await producerFixture(); - const initialProducer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - await produce(initialProducer, fixture.prepared, fixture.publication); - const activeEnvelope = fixture.store.snapshot().currentHead!; - const active = activeEnvelope.object; - const tombstone = { - objectType: 'agent-profile-head', - kind: 'agents', - state: 'tombstone', - networkId: active.networkId, - peerId: active.peerId, - peerPublicKey: active.peerPublicKey, - authoritySequence: active.authoritySequence, - version: '1', - previousHeadDigest: activeEnvelope.objectDigest, - evmIssuer: active.evmIssuer, - rootSubject: active.rootSubject, - projectionSchemaDigest: active.projectionSchemaDigest, - issuedAt: '2026-08-07T12:10:00Z', - ownedSubjectTableDigest: EMPTY_OWNED_SUBJECT_TABLE_DIGEST_V1, - ownedSubjectCount: '0', - projectionBytes: '0', - projectionQuads: '0', - } as AgentProfileHeadObjectV1; - const tombstoneEnvelope = await signHeadEnvelope( - tombstone, - fixture.peerSigner, - fixture.evmSigner, - ); - const prepareCommit = vi.fn(fixture.store.prepareCommit.bind(fixture.store)); - const store: AgentProfileProducerPublicationStoreV1 = { - snapshot: () => { - const snapshot = fixture.store.snapshot(); - return Object.freeze({ ...snapshot, currentHead: tombstoneEnvelope }); - }, - resolveArtifact: (reference) => fixture.store.resolveArtifact(reference), - prepareCommit, - }; - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store, - fence: () => {}, - install, - }); - const nextPrepared = makePrepared( - fixture.peerSigner, - fixture.evmSigner.address, - '2026-08-07T12:20:00.000Z', - ); - const lease = await producer.prepare(nextPrepared); - - await expect(lease.complete(await publicationFor( - nextPrepared, - fixture.evmSigner.address, - '2026-08-07T12:20:00Z', - ))).rejects.toThrow(/tombstoned profile/); - expect(install).not.toHaveBeenCalled(); - expect(prepareCommit).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead?.objectDigest).toBe(activeEnvelope.objectDigest); - }); - - it('rejects a concurrent stale writer before installing or replacing the winning head', async () => { - const fixture = await producerFixture(); - const install = vi.fn(); - const firstProducer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - const secondProducer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - const laterPublication = await publicationFor( - fixture.prepared, - fixture.evmSigner.address, - '2026-08-07T12:01:00Z', - ); - - const outcomes = await Promise.allSettled([ - produce(firstProducer, fixture.prepared, fixture.publication), - produce(secondProducer, fixture.prepared, laterPublication), - ]); - const fulfilled = outcomes.filter( - (outcome): outcome is PromiseFulfilledResult>> => - outcome.status === 'fulfilled', - ); - const rejected = outcomes.filter( - (outcome): outcome is PromiseRejectedResult => outcome.status === 'rejected', - ); - - expect(fulfilled).toHaveLength(1); - expect(rejected).toHaveLength(1); - expect(String(rejected[0]!.reason)).toMatch(/prepared commit|snapshot is stale/); - expect(install).toHaveBeenCalledTimes(1); - expect(fixture.store.snapshot().currentHead?.objectDigest) - .toBe(fulfilled[0]!.value.headDigest); - }); - - it('rejects ordinary update authority and schema changes without replacing the head', async () => { - const fixture = await producerFixture(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - const first = await produce(producer, fixture.prepared, fixture.publication); - const nextPrepared = makePrepared( - fixture.peerSigner, - fixture.evmSigner.address, - '2026-08-07T12:20:00.000Z', - ); - const nextPublication = await publicationFor( - nextPrepared, - fixture.evmSigner.address, - '2026-08-07T12:20:00Z', - ); - await expect(produce(producer, nextPrepared, { - ...nextPublication, - projectionSchemaDigest: `0x${'cd'.repeat(32)}` as Digest32V1, - })).rejects.toThrow(/changed its root or projection schema/); - expect(fixture.store.snapshot().currentHead?.objectDigest).toBe(first.headDigest); - - const otherSigner = createEvmPersonalMessageSignerV1({ - mode: 'custodial', - address: new ethers.Wallet(OTHER_PRIVATE_KEY).address, - privateKey: OTHER_PRIVATE_KEY, - purpose: 'system-record alternate authority test', - }); - const otherPrepared = makePrepared( - fixture.peerSigner, - otherSigner.address, - '2026-08-07T12:40:00.000Z', - ); - const otherProducer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: otherSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - const lease = await otherProducer.prepare(otherPrepared); - await expect(lease.complete(nextPublication)).rejects.toThrow(/authority transition/); - expect(fixture.store.snapshot().currentHead?.objectDigest).toBe(first.headDigest); - }); - - it('preserves verified authority lineage on a post-transition heartbeat', async () => { - const prior = await producerFixture(); - const priorProducer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: prior.peerSigner, - evmSigner: prior.evmSigner, - store: prior.store, - fence: () => {}, - install: () => {}, - }); - await produce(priorProducer, prior.prepared, prior.publication); - const priorEnvelope = prior.store.snapshot().currentHead!; - - const nextSigner = createEvmPersonalMessageSignerV1({ - mode: 'custodial', - address: new ethers.Wallet(OTHER_PRIVATE_KEY).address, - privateKey: OTHER_PRIVATE_KEY, - purpose: 'post-transition profile test', - }); - const transitionedPrepared = makePrepared( - prior.peerSigner, - nextSigner.address, - '2026-08-07T12:20:00.000Z', - ); - const transitionedPublication = await publicationFor( - transitionedPrepared, - nextSigner.address, - '2026-08-07T12:20:00Z', - OTHER_PRIVATE_KEY, - ); - const bootstrapStore = createInMemoryAgentProfilePublicationStoreV1(); - const bootstrapProducer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: prior.peerSigner, - evmSigner: nextSigner, - store: bootstrapStore, - fence: () => {}, - install: () => {}, - }); - await produce(bootstrapProducer, transitionedPrepared, transitionedPublication); - const bootstrapEnvelope = bootstrapStore.snapshot().currentHead!; - - const transition: AgentProfileAuthorityTransitionV1 = { - objectType: 'authority-transition', - kind: 'agents', - mode: 'co-signed', - networkId: NETWORK, - peerId: prior.peerSigner.peerId, - peerPublicKey: prior.peerSigner.publicKey, - priorAuthoritySequence: '0', - nextAuthoritySequence: '1', - priorHeadDigest: priorEnvelope.objectDigest, - priorEvmIssuer: prior.evmSigner.address, - nextEvmIssuer: nextSigner.address, - nextRoot: transitionedPrepared.rootEntity, - issuedAt: '2026-08-07T12:10:00Z', - }; - const transitionEnvelope = await signTransitionEnvelope( - transition, - prior.peerSigner, - prior.evmSigner, - nextSigner, - ); - const transitionedEnvelope = await signHeadEnvelope({ - ...bootstrapEnvelope.object, - authoritySequence: '1', - acceptedTransitionDigest: transitionEnvelope.objectDigest, - }, prior.peerSigner, nextSigner); - const history = new Map(); - for (const artifact of [ - envelopeArtifact('agent-profile-head', priorEnvelope), - envelopeArtifact('authority-transition', transitionEnvelope), - ]) { - history.set(systemRecordArtifactKeyV1(artifact), artifact); - } - let pendingCommit: AgentProfileProducerPublicationCommitV1 | null = null; - const store: AgentProfileProducerPublicationStoreV1 = { - snapshot: () => Object.freeze({ inventory: null, currentHead: transitionedEnvelope }), - resolveArtifact: (reference) => history.get(systemRecordArtifactKeyV1(reference)) ?? null, - prepareCommit: (input) => { - pendingCommit = input; - return Object.freeze({ commit: () => {}, abort: () => {} }); - }, - }; - const heartbeatPrepared = makePrepared( - prior.peerSigner, - nextSigner.address, - '2026-08-07T12:30:00.000Z', - ); - const heartbeatProducer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: prior.peerSigner, - evmSigner: nextSigner, - store, - fence: () => {}, - install: () => {}, - }); - - const result = await produce( - heartbeatProducer, - heartbeatPrepared, - await publicationFor( - heartbeatPrepared, - nextSigner.address, - '2026-08-07T12:30:00Z', - OTHER_PRIVATE_KEY, - ), - ); - const headArtifact = pendingCommit!.publicationArtifacts.head; - const heartbeatEnvelope = parseCanonicalSignedAgentProfileHeadEnvelopeV1( - headArtifact.canonicalBytes, - ); - expect(result).toMatchObject({ authoritySequence: '1', version: '1' }); - expect(heartbeatEnvelope.object.acceptedTransitionDigest) - .toBe(transitionEnvelope.objectDigest); - expect(heartbeatEnvelope.object.previousHeadDigest).toBe(transitionedEnvelope.objectDigest); - }); - - it('preflights provider capacity before materialization and releases a failed commit lease', async () => { - const fixture = await producerFixture(createInMemoryAgentProfilePublicationStoreV1({ - maxObjects: 1, - maxBytes: 1024 * 1024, - })); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - await expect(produce(producer, fixture.prepared, fixture.publication)) - .rejects.toThrow(/cache capacity exhausted/); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - - const durableStore = createInMemoryAgentProfilePublicationStoreV1(); - const retryFixture = await producerFixture(durableStore); - let fail = true; - const retrying = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: retryFixture.peerSigner, - evmSigner: retryFixture.evmSigner, - store: durableStore, - fence: () => {}, - install: () => { - if (fail) throw new Error('injected materialization failure'); - }, - }); - await expect(produce(retrying, retryFixture.prepared, retryFixture.publication)) - .rejects.toThrow(/injected materialization failure/); - expect(durableStore.snapshot().currentHead).toBeNull(); - fail = false; - await expect(produce(retrying, retryFixture.prepared, retryFixture.publication)).resolves.toMatchObject({ - version: '0', - }); - }); - - it('counts the retained root descriptor against cache capacity on rollover', async () => { - const store = createInMemoryAgentProfilePublicationStoreV1({ - maxObjects: 7, - maxBytes: 1024 * 1024, - }); - const fixture = await producerFixture(store); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store, - fence: () => {}, - install: () => {}, - }); - const first = await produce(producer, fixture.prepared, fixture.publication); - const nextPrepared = makePrepared( - fixture.peerSigner, - fixture.evmSigner.address, - '2026-08-07T12:20:00.000Z', - ); - - await expect(produce( - producer, - nextPrepared, - await publicationFor(nextPrepared, fixture.evmSigner.address, '2026-08-07T12:20:00Z'), - )).rejects.toThrow(/cache capacity exhausted/); - expect(store.snapshot().currentHead?.objectDigest).toBe(first.headDigest); - }); - - it('does not expose mutable store state through snapshots', async () => { - const fixture = await producerFixture(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - const published = await produce(producer, fixture.prepared, fixture.publication); - const snapshot = fixture.store.snapshot(); - (snapshot.inventory!.objects as Map).clear(); - (snapshot.currentHead!.object as { version: string }).version = '99'; - - const fresh = fixture.store.snapshot(); - expect(fresh.inventory!.objects.size).toBeGreaterThan(0); - expect(fresh.currentHead?.objectDigest).toBe(published.headDigest); - expect(fresh.currentHead?.object.version).toBe('0'); - }); - - it('defensively snapshots a structurally valid mutable prepared profile', async () => { - const fixture = await producerFixture(); - const fence = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence, - install: () => {}, - }); - const mutable = { - publicationQuads: fixture.prepared.publicationQuads.map((quad) => ({ ...quad })), - projectionQuads: fixture.prepared.projectionQuads.map((quad) => ({ ...quad })), - rootEntity: fixture.prepared.rootEntity, - lastSeen: fixture.prepared.lastSeen, - }; - - const lease = await producer.prepare(mutable); - mutable.publicationQuads.length = 0; - mutable.projectionQuads.length = 0; - mutable.rootEntity = 'urn:mutated-after-prepare'; - await expect(lease.complete(fixture.publication)).resolves.toMatchObject({ version: '0' }); - expect(fence).toHaveBeenCalledWith( - expect.objectContaining({ rootEntity: fixture.prepared.rootEntity }), - expect.any(AbortSignal), - ); - expect(fixture.store.snapshot().currentHead?.object.rootSubject) - .toBe(fixture.prepared.rootEntity); - }); - - it('defensively snapshots the publication binding across later signing awaits', async () => { - const fixture = await producerFixture(); - const originalFinalizedAt = fixture.publication.seal.assertionFinalizedAt; - const mutableSeal = { ...fixture.publication.seal }; - const mutablePublication = { - ...fixture.publication, - seal: mutableSeal, - } as AgentProfilePublicationBindingV1; - let peerSignatureCount = 0; - const peerSign = vi.fn(async (message: Uint8Array) => { - peerSignatureCount += 1; - if (peerSignatureCount === 2) { - mutableSeal.assertionFinalizedAt = '2026-08-07T12:15:00.000Z'; - } - return fixture.peerSigner.sign(message); - }); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { ...fixture.peerSigner, sign: peerSign }, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - - await expect(produce(producer, fixture.prepared, mutablePublication)) - .resolves.toMatchObject({ version: '0' }); - expect(mutableSeal.assertionFinalizedAt).not.toBe(originalFinalizedAt); - expect(install.mock.calls[0]![0].head.graphScopedAuthorSeal.assertionFinalizedAt) - .toBe(originalFinalizedAt); - expect(fixture.store.snapshot().currentHead?.object.graphScopedAuthorSeal.assertionFinalizedAt) - .toBe(originalFinalizedAt); - }); - - it('fails closed when the publication seal is not for the exact prepared bytes', async () => { - const fixture = await producerFixture(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - const tampered = { - ...fixture.publication, - seal: { ...fixture.publication.seal, assertionMerkleRoot: `0x${'cd'.repeat(32)}` }, - } as AgentProfilePublicationBindingV1; - await expect(produce(producer, fixture.prepared, tampered)).rejects.toThrow(/exact public projection/); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('rejects a non-confirmed publication before installation', async () => { - const fixture = await producerFixture(); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - const publication: AgentProfilePublicationBindingV1 = { - ...fixture.publication, - publicationStatus: 'tentative', - }; - - await expect(produce(producer, fixture.prepared, publication)) - .rejects.toThrow(/requires a confirmed publication/); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it.each([ - [ - 'a different author address', - (publication: AgentProfilePublicationBindingV1) => ({ - ...publication, - seal: { ...publication.seal, authorAddress: new ethers.Wallet(OTHER_PRIVATE_KEY).address.toLowerCase() }, - }), - /exact public projection/, - ], - [ - 'a wrong public triple count', - (publication: AgentProfilePublicationBindingV1) => ({ - ...publication, - seal: { ...publication.seal, publicTripleCount: '999' }, - }), - /exact public projection/, - ], - [ - 'private triples', - (publication: AgentProfilePublicationBindingV1) => ({ - ...publication, - seal: { ...publication.seal, privateTripleCount: '1' }, - }), - /exact public projection/, - ], - [ - 'a private Merkle root', - (publication: AgentProfilePublicationBindingV1) => ({ - ...publication, - seal: { ...publication.seal, privateMerkleRoot: `0x${'cd'.repeat(32)}` }, - }), - /exact public projection/, - ], - ])('rejects publication binding with %s before installation', async (_label, mutate, expected) => { - const fixture = await producerFixture(); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - const publication = mutate(fixture.publication) as unknown as AgentProfilePublicationBindingV1; - - await expect(produce(producer, fixture.prepared, publication)).rejects.toThrow(expected); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('rejects a future-dated publication against an independent producer clock', async () => { - const fixture = await producerFixture(); - const install = vi.fn(); - const peerSign = vi.fn(fixture.peerSigner.sign); - const nowMs = Date.parse('2026-08-07T12:00:00Z'); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { ...fixture.peerSigner, sign: peerSign }, - evmSigner: fixture.evmSigner, - store: fixture.store, - nowMs: () => nowMs, - fence: () => {}, - install, - }); - const issuedAt = new Date(nowMs + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS + 1_000) - .toISOString() - .replace('.000Z', 'Z'); - - await expect(produce( - producer, - fixture.prepared, - await publicationFor(fixture.prepared, fixture.evmSigner.address, issuedAt), - )).rejects.toThrow(/future clock-skew bound/); - expect(peerSign).not.toHaveBeenCalled(); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('rejects a head issue time before assertion finalization without side effects', async () => { - const fixture = await producerFixture(); - const peerSign = vi.fn(fixture.peerSigner.sign); - const evmSignMessage = vi.fn(fixture.evmSigner.signMessage); - const prepareCommit = vi.fn(fixture.store.prepareCommit.bind(fixture.store)); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { ...fixture.peerSigner, sign: peerSign }, - evmSigner: { ...fixture.evmSigner, signMessage: evmSignMessage }, - store: { - snapshot: () => fixture.store.snapshot(), - resolveArtifact: (reference) => fixture.store.resolveArtifact(reference), - prepareCommit, - }, - fence: () => {}, - install, - }); - - await expect(produce(producer, fixture.prepared, { - ...fixture.publication, - seal: { - ...fixture.publication.seal, - assertionFinalizedAt: '2026-08-07T13:00:00.000Z', - }, - })).rejects.toThrow(/issuedAt predates assertion finalization/); - expect(peerSign).not.toHaveBeenCalled(); - expect(evmSignMessage).not.toHaveBeenCalled(); - expect(prepareCommit).not.toHaveBeenCalled(); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('preserves millisecond finalization precision in the issue-time ordering check', async () => { - const fixture = await producerFixture(); - const peerSign = vi.fn(fixture.peerSigner.sign); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { ...fixture.peerSigner, sign: peerSign }, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - const finalizedWithinSecond = { - ...fixture.publication, - seal: { - ...fixture.publication.seal, - assertionFinalizedAt: '2026-08-07T12:00:00.999Z', - }, - } as AgentProfilePublicationBindingV1; - - await expect(produce(producer, fixture.prepared, finalizedWithinSecond)) - .rejects.toThrow(/issuedAt predates assertion finalization/); - expect(peerSign).not.toHaveBeenCalled(); - expect(install).not.toHaveBeenCalled(); - - await expect(produce(producer, fixture.prepared, { - ...finalizedWithinSecond, - issuedAt: '2026-08-07T12:00:01Z', - })).resolves.toMatchObject({ version: '0' }); - expect(install).toHaveBeenCalledOnce(); - expect(fixture.store.snapshot().currentHead?.object.issuedAt).toBe('2026-08-07T12:00:01Z'); - }); - - it.each([ - [ - 'a foreign UAL network', - (publication: AgentProfilePublicationBindingV1) => ({ - ...publication, - seal: { - ...publication.seal, - kaUal: publication.seal.kaUal.replace('did:dkg:base:84532/', 'did:dkg:base:1/'), - }, - }), - ], - [ - 'a foreign UAL account', - (publication: AgentProfilePublicationBindingV1) => ({ - ...publication, - seal: { - ...publication.seal, - kaUal: publication.seal.kaUal.replace( - /\/0x[0-9a-f]{40}\//, - `/0x${'22'.repeat(20)}/`, - ), - }, - }), - ], - [ - 'a foreign asserted chain', - (publication: AgentProfilePublicationBindingV1) => ({ - ...publication, - seal: { ...publication.seal, assertedAtChainId: '1' }, - }), - ], - [ - 'a foreign KAv10 deployment', - (publication: AgentProfilePublicationBindingV1) => ({ - ...publication, - seal: { - ...publication.seal, - assertedAtKav10Address: `0x${'55'.repeat(20)}`, - }, - }), - ], - ])('rejects %s before signing or publication side effects', async (_label, mutate) => { - const fixture = await producerFixture(); - const peerSign = vi.fn(fixture.peerSigner.sign); - const evmSignMessage = vi.fn(fixture.evmSigner.signMessage); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { ...fixture.peerSigner, sign: peerSign }, - evmSigner: { ...fixture.evmSigner, signMessage: evmSignMessage }, - store: fixture.store, - fence: () => {}, - install, - }); - const publication = mutate(fixture.publication) as unknown as AgentProfilePublicationBindingV1; - - await expect(produce(producer, fixture.prepared, publication)) - .rejects.toThrow(/different network or deployment/); - expect(peerSign).not.toHaveBeenCalled(); - expect(evmSignMessage).not.toHaveBeenCalled(); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('rejects a non-positive validity window before signing or committing', async () => { - const fixture = await producerFixture(); - const peerSign = vi.fn(fixture.peerSigner.sign); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { ...fixture.peerSigner, sign: peerSign }, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - - await expect(produce(producer, fixture.prepared, { - ...fixture.publication, - validUntil: fixture.publication.issuedAt, - })).rejects.toThrow(/validUntil must be later than issuedAt/); - expect(peerSign).not.toHaveBeenCalled(); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('rejects an already-expired validity window before signing, install, or commit', async () => { - const fixture = await producerFixture(); - const peerSign = vi.fn(fixture.peerSigner.sign); - const evmSignMessage = vi.fn(fixture.evmSigner.signMessage); - const prepareCommit = vi.fn(fixture.store.prepareCommit.bind(fixture.store)); - const store: AgentProfileProducerPublicationStoreV1 = { - snapshot: () => fixture.store.snapshot(), - resolveArtifact: (reference) => fixture.store.resolveArtifact(reference), - prepareCommit, - }; - const install = vi.fn(); - const nowMs = Date.parse('2026-08-07T12:00:00Z'); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { ...fixture.peerSigner, sign: peerSign }, - evmSigner: { ...fixture.evmSigner, signMessage: evmSignMessage }, - store, - nowMs: () => nowMs, - fence: () => {}, - install, - }); - - await expect(produce(producer, fixture.prepared, { - ...fixture.publication, - issuedAt: '2026-08-06T00:00:00Z', - validUntil: '2026-08-06T01:00:00Z', - seal: { - ...fixture.publication.seal, - assertionFinalizedAt: '2026-08-06T00:00:00.000Z', - }, - })).rejects.toThrow(/already expired/); - expect(peerSign).not.toHaveBeenCalled(); - expect(evmSignMessage).not.toHaveBeenCalled(); - expect(install).not.toHaveBeenCalled(); - expect(prepareCommit).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('rejects an oversized encoded profile bundle before signing, install, or commit', async () => { - const fixture = await producerFixture(); - const multiaddrs = Array.from({ length: 6_000 }, (_, index) => ( - `/dns4/profile-${index.toString().padStart(4, '0')}-${'a'.repeat(96)}.example/tcp/4001` - )); - const prepared = prepareAgentProfileV1({ - peerId: fixture.peerSigner.peerId, - publicKey: Buffer.from(fixture.peerSigner.publicKey, 'base64url').toString('base64'), - agentAddress: fixture.evmSigner.address, - name: 'Oversized profile fixture', - nodeRole: 'edge', - lastSeen: '2026-08-07T12:00:00.000Z', - skills: [], - multiaddrs, - }); - const peerSign = vi.fn(fixture.peerSigner.sign); - const evmSignMessage = vi.fn(fixture.evmSigner.signMessage); - const prepareCommit = vi.fn(fixture.store.prepareCommit.bind(fixture.store)); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { ...fixture.peerSigner, sign: peerSign }, - evmSigner: { ...fixture.evmSigner, signMessage: evmSignMessage }, - store: { - snapshot: () => fixture.store.snapshot(), - resolveArtifact: (reference) => fixture.store.resolveArtifact(reference), - prepareCommit, - }, - fence: () => {}, - install, - }); - - await expect(produce( - producer, - prepared, - await publicationFor(prepared, fixture.evmSigner.address, '2026-08-07T12:00:00Z'), - )).rejects.toThrow(/profile bundle exceeds/); - expect(peerSign).not.toHaveBeenCalled(); - expect(evmSignMessage).not.toHaveBeenCalled(); - expect(prepareCommit).not.toHaveBeenCalled(); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('normalizes millisecond publication timestamps before signing the head', async () => { - const fixture = await producerFixture(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - await produce(producer, fixture.prepared, { - ...fixture.publication, - issuedAt: '2026-08-07T12:00:00.123Z', - validUntil: '2026-08-08T12:00:00.987Z', - }); - - expect(fixture.store.snapshot().currentHead?.object).toMatchObject({ - issuedAt: '2026-08-07T12:00:00Z', - validUntil: '2026-08-08T12:00:00Z', - }); - }); - - it('rejects invalid publication timestamp scalars before signing or committing', async () => { - const fixture = await producerFixture(); - const peerSign = vi.fn(fixture.peerSigner.sign); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { ...fixture.peerSigner, sign: peerSign }, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - - await expect(produce(producer, fixture.prepared, { - ...fixture.publication, - issuedAt: '2026-02-30T12:00:00.000Z', - })).rejects.toThrow(/calendar-valid/); - expect(peerSign).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('rejects a seal whose author attestation does not recover the profile authority', async () => { - const fixture = await producerFixture(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => {}, - }); - const tampered = { - ...fixture.publication, - seal: { - ...fixture.publication.seal, - authorAttestationR: `0x${'22'.repeat(32)}`, - authorAttestationVS: `0x${'33'.repeat(32)}`, - }, - } as AgentProfilePublicationBindingV1; - await expect(produce(producer, fixture.prepared, tampered)) - .rejects.toThrow(/attestation does not recover/); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('rolls back when the peer signer returns an invalid profile-head signature', async () => { - const fixture = await producerFixture(); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { ...fixture.peerSigner, sign: async () => new Uint8Array(64) }, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - - await expect(produce(producer, fixture.prepared, fixture.publication)) - .rejects.toThrow(/head signature verification failed/); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('rolls back when the peer signer returns an invalid inventory-root signature', async () => { - const fixture = await producerFixture(); - const install = vi.fn(); - let signatureNumber = 0; - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: { - ...fixture.peerSigner, - sign: async (message) => { - signatureNumber += 1; - return signatureNumber === 1 - ? fixture.peerSigner.sign(message) - : new Uint8Array(64); - }, - }, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install, - }); - - await expect(produce(producer, fixture.prepared, fixture.publication)) - .rejects.toThrow(/inventory root signature verification failed/); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('fences before publication and an aborted lease releases the local single-flight', async () => { - const fixture = await producerFixture(); - const fence = vi.fn(); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence, - install, - }); - const first = await producer.prepare(fixture.prepared); - expect(fence).toHaveBeenCalledTimes(1); - await expect(producer.prepare(fixture.prepared)).rejects.toThrow(/busy/); - first.abort(); - const retry = await producer.prepare(fixture.prepared); - retry.abort(); - expect(fence).toHaveBeenCalledTimes(2); - expect(install).not.toHaveBeenCalled(); - }); - - it('releases the local single-flight when the publication fence rejects', async () => { - const fixture = await producerFixture(); - const fence = vi.fn() - .mockRejectedValueOnce(new Error('transient fence failure')) - .mockResolvedValue(undefined); - const install = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence, - install, - }); - - await expect(producer.prepare(fixture.prepared)).rejects.toThrow(/transient fence failure/); - const retry = await producer.prepare(fixture.prepared); - retry.abort(); - expect(fence).toHaveBeenCalledTimes(2); - expect(install).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it('aborts a reserved commit when cancellation races a blocked install', async () => { - const fixture = await producerFixture(); - const installStarted = Promise.withResolvers(); - const releaseInstall = Promise.withResolvers(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: async ({ signal }) => { - installStarted.resolve(); - await releaseInstall.promise; - signal.throwIfAborted(); - }, - }); - - const lease = await producer.prepare(fixture.prepared); - const completion = lease.complete(fixture.publication); - await installStarted.promise; - lease.abort(new Error('cancel blocked install')); - releaseInstall.resolve(); - - await expect(completion).rejects.toThrow(/cancel blocked install/); - expect(fixture.store.snapshot().currentHead).toBeNull(); - expect(fixture.store.snapshot().inventory).toBeNull(); - - const retry = await producer.prepare(fixture.prepared); - await expect(retry.complete(fixture.publication)).resolves.toMatchObject({ version: '0' }); - }); - - it('commits the advertisement when cancellation arrives after successful installation', async () => { - const fixture = await producerFixture(); - let lease: Awaited>; - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence: () => {}, - install: () => { lease.abort(new Error('late cancellation')); }, - }); - - lease = await producer.prepare(fixture.prepared); - await expect(lease.complete(fixture.publication)).resolves.toMatchObject({ version: '0' }); - expect(fixture.store.snapshot().currentHead?.object.version).toBe('0'); - expect(fixture.store.snapshot().inventory).not.toBeNull(); - }); - - it('rejects duplicate canonical profile triples before fencing publication', async () => { - const fixture = await producerFixture(); - const fence = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence, - install: () => {}, - }); - const duplicate = Object.freeze({ - ...fixture.prepared, - projectionQuads: Object.freeze([ - ...fixture.prepared.projectionQuads, - fixture.prepared.projectionQuads[0]!, - ]), - }); - - await expect(producer.prepare(duplicate)).rejects.toThrow(/duplicate-free/); - expect(fence).not.toHaveBeenCalled(); - }); - - it('rejects an out-of-schema profile predicate before fencing publication', async () => { - const fixture = await producerFixture(); - const fence = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence, - install: () => {}, - }); - const outOfSchema = Object.freeze({ - ...fixture.prepared, - projectionQuads: Object.freeze([ - ...fixture.prepared.projectionQuads, - Object.freeze({ - subject: fixture.prepared.rootEntity, - predicate: 'https://example.org/unapproved', - object: '"x"', - graph: '', - }), - ]), - }); - - await expect(producer.prepare(outOfSchema)).rejects.toThrow(/outside schema V1/); - expect(fence).not.toHaveBeenCalled(); - }); - - it.each([ - { - label: 'a literal profile link', - mutate: (prepared: PreparedAgentProfileV1) => prepared.projectionQuads.map((quad) => ( - quad.predicate === 'http://www.w3.org/ns/prov#wasGeneratedBy' - ? { ...quad, object: '"not-an-iri"' } - : quad - )), - }, - { - label: 'an unapproved rdf:type object', - mutate: (prepared: PreparedAgentProfileV1) => prepared.projectionQuads.map((quad) => ( - quad.subject === prepared.rootEntity - && quad.predicate === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' - && quad.object === 'https://dkg.network/ontology#Agent' - ? { ...quad, object: 'https://example.org/InvalidAgentType' } - : quad - )), - }, - { - label: 'an underived x25519 revocation subject', - mutate: (prepared: PreparedAgentProfileV1) => [ - ...prepared.projectionQuads, - { - subject: prepared.rootEntity, - predicate: 'https://dkg.network/ontology#publicEncryptionKey', - object: `"${Buffer.alloc(32, 9).toString('base64url')}"`, - graph: '', - }, - { - subject: `${prepared.rootEntity}#x25519-${'0'.repeat(32)}`, - predicate: 'https://dkg.network/ontology#revokedAt', - object: '"2026-08-07T12:00:00Z"', - graph: '', - }, - ], - }, - ])('rejects $label before fencing publication', async ({ mutate }) => { - const fixture = await producerFixture(); - const fence = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence, - install: () => {}, - }); - const malformed = Object.freeze({ - ...fixture.prepared, - projectionQuads: Object.freeze(mutate(fixture.prepared)), - }); - - await expect(producer.prepare(malformed)).rejects.toThrow(/outside schema V1/); - expect(fence).not.toHaveBeenCalled(); - expect(fixture.store.snapshot().currentHead).toBeNull(); - }); - - it.each([ - ['peerId', '"12D3KooWRhLYc1qpzVncrVpMkykB3ML1PoQ9G9gX9X9G9gX9X9G"'], - ['agentAddress', `"0x${'33'.repeat(20)}"`], - ['publicKey', '"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="'], - ])('rejects a mismatched advertised %s before fencing publication', async (field, object) => { - const fixture = await producerFixture(); - const fence = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence, - install: () => {}, - }); - const predicate = `https://dkg.network/ontology#${field}`; - const mismatched = Object.freeze({ - ...fixture.prepared, - projectionQuads: Object.freeze(fixture.prepared.projectionQuads.map((quad) => Object.freeze( - quad.predicate === predicate ? { ...quad, object } : quad, - ))), - }); - - await expect(producer.prepare(mismatched)).rejects.toThrow(/does not bind the signed/); - expect(fence).not.toHaveBeenCalled(); - }); - - it.each([ - ['peerId', '"12D3KooWRhLYc1qpzVncrVpMkykB3ML1PoQ9G9gX9X9G9gX9X9G"'], - ['agentAddress', `"0x${'33'.repeat(20)}"`], - ['publicKey', '"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="'], - ])('rejects a conflicting advertised %s before fencing publication', async (field, object) => { - const fixture = await producerFixture(); - const fence = vi.fn(); - const producer = createAgentProfileProducerV1({ - networkId: NETWORK, - publicationDeployment: DEPLOYMENT, - peerSigner: fixture.peerSigner, - evmSigner: fixture.evmSigner, - store: fixture.store, - fence, - install: () => {}, - }); - const conflicting = Object.freeze({ - ...fixture.prepared, - projectionQuads: Object.freeze([ - ...fixture.prepared.projectionQuads, - Object.freeze({ - subject: fixture.prepared.rootEntity, - predicate: `https://dkg.network/ontology#${field}`, - object, - graph: '', - }), - ]), - }); - - await expect(producer.prepare(conflicting)).rejects.toThrow(/does not bind the signed/); - expect(fence).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts b/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts new file mode 100644 index 0000000000..e17dde0cda --- /dev/null +++ b/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts @@ -0,0 +1,669 @@ +import { SYSTEM_RECORD_MAX_CLOCK_SKEW_MS } from '@origintrail-official/dkg-core/system-record-v1'; +import { ethers } from 'ethers'; +import { + describe, + expect, + it, + vi, +} from 'vitest'; +import { prepareAgentProfileV1 } from '../src/profile.js'; +import { + type AgentProfileProducerPublicationStoreV1, + type AgentProfilePublicationBindingV1, +} from '../src/system-records/agent-profile-producer-v1.js'; +import { createInMemoryAgentProfilePublicationStoreV1 } from '../src/system-records/in-memory-agent-profile-publication-store-v1.js'; +import { + DEPLOYMENT, + NETWORK, + OTHER_PRIVATE_KEY, + createFixtureAgentProfileProducerV1 as createAgentProfileProducerV1, + makePrepared, + produce, + producerFixture, + publicationFor, +} from './support/agent-profile-producer-v1-fixture.js'; + + +describe('agent-profile system-record producer V1 validation and binding', () => { + it('preflights provider capacity before materialization and releases a failed commit lease', async () => { + const fixture = await producerFixture(createInMemoryAgentProfilePublicationStoreV1({ + maxObjects: 1, + maxBytes: 1024 * 1024, + })); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + await expect(produce(producer, fixture.prepared, fixture.publication)) + .rejects.toThrow(/cache capacity exhausted/); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + + const durableStore = createInMemoryAgentProfilePublicationStoreV1(); + const retryFixture = await producerFixture(durableStore); + let fail = true; + const retrying = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: retryFixture.peerSigner, + evmSigner: retryFixture.evmSigner, + store: durableStore, + fence: () => {}, + install: () => { + if (fail) throw new Error('injected materialization failure'); + }, + }); + await expect(produce(retrying, retryFixture.prepared, retryFixture.publication)) + .rejects.toThrow(/injected materialization failure/); + expect(durableStore.snapshot().currentHead).toBeNull(); + fail = false; + await expect(produce(retrying, retryFixture.prepared, retryFixture.publication)).resolves.toMatchObject({ + version: '0', + }); + }); + + it('counts the retained root descriptor against cache capacity on rollover', async () => { + const store = createInMemoryAgentProfilePublicationStoreV1({ + maxObjects: 7, + maxBytes: 1024 * 1024, + }); + const fixture = await producerFixture(store); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store, + fence: () => {}, + install: () => {}, + }); + const first = await produce(producer, fixture.prepared, fixture.publication); + const nextPrepared = makePrepared( + fixture.peerSigner, + fixture.evmSigner.address, + '2026-08-07T12:20:00.000Z', + ); + + await expect(produce( + producer, + nextPrepared, + await publicationFor(nextPrepared, fixture.evmSigner.address, '2026-08-07T12:20:00Z'), + )).rejects.toThrow(/cache capacity exhausted/); + expect(store.snapshot().currentHead?.objectDigest).toBe(first.headDigest); + }); + + it('does not expose mutable store state through snapshots', async () => { + const fixture = await producerFixture(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + const published = await produce(producer, fixture.prepared, fixture.publication); + const snapshot = fixture.store.snapshot(); + (snapshot.inventory!.objects as Map).clear(); + (snapshot.currentHead!.object as { version: string }).version = '99'; + + const fresh = fixture.store.snapshot(); + expect(fresh.inventory!.objects.size).toBeGreaterThan(0); + expect(fresh.currentHead?.objectDigest).toBe(published.headDigest); + expect(fresh.currentHead?.object.version).toBe('0'); + }); + + it('defensively snapshots a structurally valid mutable prepared profile', async () => { + const fixture = await producerFixture(); + const fence = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence, + install: () => {}, + }); + const mutable = { + publicationQuads: fixture.prepared.publicationQuads.map((quad) => ({ ...quad })), + projectionQuads: fixture.prepared.projectionQuads.map((quad) => ({ ...quad })), + rootEntity: fixture.prepared.rootEntity, + lastSeen: fixture.prepared.lastSeen, + }; + + const lease = await producer.prepare(mutable); + mutable.publicationQuads.length = 0; + mutable.projectionQuads.length = 0; + mutable.rootEntity = 'urn:mutated-after-prepare'; + await expect(lease.complete(fixture.publication)).resolves.toMatchObject({ version: '0' }); + expect(fence).toHaveBeenCalledWith( + expect.objectContaining({ rootEntity: fixture.prepared.rootEntity }), + expect.any(AbortSignal), + ); + expect(fixture.store.snapshot().currentHead?.object.rootSubject) + .toBe(fixture.prepared.rootEntity); + }); + + it('defensively snapshots the publication binding across later signing awaits', async () => { + const fixture = await producerFixture(); + const originalFinalizedAt = fixture.publication.seal.assertionFinalizedAt; + const mutableSeal = { ...fixture.publication.seal }; + const mutablePublication = { + ...fixture.publication, + seal: mutableSeal, + } as AgentProfilePublicationBindingV1; + let peerSignatureCount = 0; + const peerSign = vi.fn(async (message: Uint8Array) => { + peerSignatureCount += 1; + if (peerSignatureCount === 2) { + mutableSeal.assertionFinalizedAt = '2026-08-07T12:15:00.000Z'; + } + return fixture.peerSigner.sign(message); + }); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { ...fixture.peerSigner, sign: peerSign }, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + + await expect(produce(producer, fixture.prepared, mutablePublication)) + .resolves.toMatchObject({ version: '0' }); + expect(mutableSeal.assertionFinalizedAt).not.toBe(originalFinalizedAt); + expect(install.mock.calls[0]![0].head.graphScopedAuthorSeal.assertionFinalizedAt) + .toBe(originalFinalizedAt); + expect(fixture.store.snapshot().currentHead?.object.graphScopedAuthorSeal.assertionFinalizedAt) + .toBe(originalFinalizedAt); + }); + + it('fails closed when the publication seal is not for the exact prepared bytes', async () => { + const fixture = await producerFixture(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + const tampered = { + ...fixture.publication, + seal: { ...fixture.publication.seal, assertionMerkleRoot: `0x${'cd'.repeat(32)}` }, + } as AgentProfilePublicationBindingV1; + await expect(produce(producer, fixture.prepared, tampered)).rejects.toThrow(/exact public projection/); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('rejects a non-confirmed publication before installation', async () => { + const fixture = await producerFixture(); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + const publication: AgentProfilePublicationBindingV1 = { + ...fixture.publication, + publicationStatus: 'tentative', + }; + + await expect(produce(producer, fixture.prepared, publication)) + .rejects.toThrow(/requires a confirmed publication/); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it.each([ + [ + 'a different author address', + (publication: AgentProfilePublicationBindingV1) => ({ + ...publication, + seal: { ...publication.seal, authorAddress: new ethers.Wallet(OTHER_PRIVATE_KEY).address.toLowerCase() }, + }), + /exact public projection/, + ], + [ + 'a wrong public triple count', + (publication: AgentProfilePublicationBindingV1) => ({ + ...publication, + seal: { ...publication.seal, publicTripleCount: '999' }, + }), + /exact public projection/, + ], + [ + 'private triples', + (publication: AgentProfilePublicationBindingV1) => ({ + ...publication, + seal: { ...publication.seal, privateTripleCount: '1' }, + }), + /exact public projection/, + ], + [ + 'a private Merkle root', + (publication: AgentProfilePublicationBindingV1) => ({ + ...publication, + seal: { ...publication.seal, privateMerkleRoot: `0x${'cd'.repeat(32)}` }, + }), + /exact public projection/, + ], + ])('rejects publication binding with %s before installation', async (_label, mutate, expected) => { + const fixture = await producerFixture(); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + const publication = mutate(fixture.publication) as unknown as AgentProfilePublicationBindingV1; + + await expect(produce(producer, fixture.prepared, publication)).rejects.toThrow(expected); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('rejects a future-dated publication against an independent producer clock', async () => { + const fixture = await producerFixture(); + const install = vi.fn(); + const peerSign = vi.fn(fixture.peerSigner.sign); + const nowMs = Date.parse('2026-08-07T12:00:00Z'); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { ...fixture.peerSigner, sign: peerSign }, + evmSigner: fixture.evmSigner, + store: fixture.store, + nowMs: () => nowMs, + fence: () => {}, + install, + }); + const issuedAt = new Date(nowMs + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS + 1_000) + .toISOString() + .replace('.000Z', 'Z'); + + await expect(produce( + producer, + fixture.prepared, + await publicationFor(fixture.prepared, fixture.evmSigner.address, issuedAt), + )).rejects.toThrow(/future clock-skew bound/); + expect(peerSign).not.toHaveBeenCalled(); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('rejects a head issue time before assertion finalization without side effects', async () => { + const fixture = await producerFixture(); + const peerSign = vi.fn(fixture.peerSigner.sign); + const evmSignMessage = vi.fn(fixture.evmSigner.signMessage); + const prepareCommit = vi.fn(fixture.store.prepareCommit.bind(fixture.store)); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { ...fixture.peerSigner, sign: peerSign }, + evmSigner: { ...fixture.evmSigner, signMessage: evmSignMessage }, + store: { + snapshot: () => fixture.store.snapshot(), + resolveArtifact: (reference) => fixture.store.resolveArtifact(reference), + prepareCommit, + }, + fence: () => {}, + install, + }); + + await expect(produce(producer, fixture.prepared, { + ...fixture.publication, + seal: { + ...fixture.publication.seal, + assertionFinalizedAt: '2026-08-07T13:00:00.000Z', + }, + })).rejects.toThrow(/issuedAt predates assertion finalization/); + expect(peerSign).not.toHaveBeenCalled(); + expect(evmSignMessage).not.toHaveBeenCalled(); + expect(prepareCommit).not.toHaveBeenCalled(); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('preserves millisecond finalization precision in the issue-time ordering check', async () => { + const fixture = await producerFixture(); + const peerSign = vi.fn(fixture.peerSigner.sign); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { ...fixture.peerSigner, sign: peerSign }, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + const finalizedWithinSecond = { + ...fixture.publication, + seal: { + ...fixture.publication.seal, + assertionFinalizedAt: '2026-08-07T12:00:00.999Z', + }, + } as AgentProfilePublicationBindingV1; + + await expect(produce(producer, fixture.prepared, finalizedWithinSecond)) + .rejects.toThrow(/issuedAt predates assertion finalization/); + expect(peerSign).not.toHaveBeenCalled(); + expect(install).not.toHaveBeenCalled(); + + await expect(produce(producer, fixture.prepared, { + ...finalizedWithinSecond, + issuedAt: '2026-08-07T12:00:01Z', + })).resolves.toMatchObject({ version: '0' }); + expect(install).toHaveBeenCalledOnce(); + expect(fixture.store.snapshot().currentHead?.object.issuedAt).toBe('2026-08-07T12:00:01Z'); + }); + + it.each([ + [ + 'a foreign UAL network', + (publication: AgentProfilePublicationBindingV1) => ({ + ...publication, + seal: { + ...publication.seal, + kaUal: publication.seal.kaUal.replace('did:dkg:base:84532/', 'did:dkg:base:1/'), + }, + }), + ], + [ + 'a foreign UAL account', + (publication: AgentProfilePublicationBindingV1) => ({ + ...publication, + seal: { + ...publication.seal, + kaUal: publication.seal.kaUal.replace( + /\/0x[0-9a-f]{40}\//, + `/0x${'22'.repeat(20)}/`, + ), + }, + }), + ], + [ + 'a foreign asserted chain', + (publication: AgentProfilePublicationBindingV1) => ({ + ...publication, + seal: { ...publication.seal, assertedAtChainId: '1' }, + }), + ], + [ + 'a foreign KAv10 deployment', + (publication: AgentProfilePublicationBindingV1) => ({ + ...publication, + seal: { + ...publication.seal, + assertedAtKav10Address: `0x${'55'.repeat(20)}`, + }, + }), + ], + ])('rejects %s before signing or publication side effects', async (_label, mutate) => { + const fixture = await producerFixture(); + const peerSign = vi.fn(fixture.peerSigner.sign); + const evmSignMessage = vi.fn(fixture.evmSigner.signMessage); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { ...fixture.peerSigner, sign: peerSign }, + evmSigner: { ...fixture.evmSigner, signMessage: evmSignMessage }, + store: fixture.store, + fence: () => {}, + install, + }); + const publication = mutate(fixture.publication) as unknown as AgentProfilePublicationBindingV1; + + await expect(produce(producer, fixture.prepared, publication)) + .rejects.toThrow(/different network or deployment/); + expect(peerSign).not.toHaveBeenCalled(); + expect(evmSignMessage).not.toHaveBeenCalled(); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('rejects a non-positive validity window before signing or committing', async () => { + const fixture = await producerFixture(); + const peerSign = vi.fn(fixture.peerSigner.sign); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { ...fixture.peerSigner, sign: peerSign }, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + + await expect(produce(producer, fixture.prepared, { + ...fixture.publication, + validUntil: fixture.publication.issuedAt, + })).rejects.toThrow(/validUntil must be later than issuedAt/); + expect(peerSign).not.toHaveBeenCalled(); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('rejects an already-expired validity window before signing, install, or commit', async () => { + const fixture = await producerFixture(); + const peerSign = vi.fn(fixture.peerSigner.sign); + const evmSignMessage = vi.fn(fixture.evmSigner.signMessage); + const prepareCommit = vi.fn(fixture.store.prepareCommit.bind(fixture.store)); + const store: AgentProfileProducerPublicationStoreV1 = { + snapshot: () => fixture.store.snapshot(), + resolveArtifact: (reference) => fixture.store.resolveArtifact(reference), + prepareCommit, + }; + const install = vi.fn(); + const nowMs = Date.parse('2026-08-07T12:00:00Z'); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { ...fixture.peerSigner, sign: peerSign }, + evmSigner: { ...fixture.evmSigner, signMessage: evmSignMessage }, + store, + nowMs: () => nowMs, + fence: () => {}, + install, + }); + + await expect(produce(producer, fixture.prepared, { + ...fixture.publication, + issuedAt: '2026-08-06T00:00:00Z', + validUntil: '2026-08-06T01:00:00Z', + seal: { + ...fixture.publication.seal, + assertionFinalizedAt: '2026-08-06T00:00:00.000Z', + }, + })).rejects.toThrow(/already expired/); + expect(peerSign).not.toHaveBeenCalled(); + expect(evmSignMessage).not.toHaveBeenCalled(); + expect(install).not.toHaveBeenCalled(); + expect(prepareCommit).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('rejects an oversized encoded profile bundle before signing, install, or commit', async () => { + const fixture = await producerFixture(); + const multiaddrs = Array.from({ length: 6_000 }, (_, index) => ( + `/dns4/profile-${index.toString().padStart(4, '0')}-${'a'.repeat(96)}.example/tcp/4001` + )); + const prepared = prepareAgentProfileV1({ + peerId: fixture.peerSigner.peerId, + publicKey: Buffer.from(fixture.peerSigner.publicKey, 'base64url').toString('base64'), + agentAddress: fixture.evmSigner.address, + name: 'Oversized profile fixture', + nodeRole: 'edge', + lastSeen: '2026-08-07T12:00:00.000Z', + skills: [], + multiaddrs, + }); + const peerSign = vi.fn(fixture.peerSigner.sign); + const evmSignMessage = vi.fn(fixture.evmSigner.signMessage); + const prepareCommit = vi.fn(fixture.store.prepareCommit.bind(fixture.store)); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { ...fixture.peerSigner, sign: peerSign }, + evmSigner: { ...fixture.evmSigner, signMessage: evmSignMessage }, + store: { + snapshot: () => fixture.store.snapshot(), + resolveArtifact: (reference) => fixture.store.resolveArtifact(reference), + prepareCommit, + }, + fence: () => {}, + install, + }); + + await expect(produce( + producer, + prepared, + await publicationFor(prepared, fixture.evmSigner.address, '2026-08-07T12:00:00Z'), + )).rejects.toThrow(/profile bundle exceeds/); + expect(peerSign).not.toHaveBeenCalled(); + expect(evmSignMessage).not.toHaveBeenCalled(); + expect(prepareCommit).not.toHaveBeenCalled(); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('normalizes millisecond publication timestamps before signing the head', async () => { + const fixture = await producerFixture(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + await produce(producer, fixture.prepared, { + ...fixture.publication, + issuedAt: '2026-08-07T12:00:00.123Z', + validUntil: '2026-08-08T12:00:00.987Z', + }); + + expect(fixture.store.snapshot().currentHead?.object).toMatchObject({ + issuedAt: '2026-08-07T12:00:00Z', + validUntil: '2026-08-08T12:00:00Z', + }); + }); + + it('rejects invalid publication timestamp scalars before signing or committing', async () => { + const fixture = await producerFixture(); + const peerSign = vi.fn(fixture.peerSigner.sign); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { ...fixture.peerSigner, sign: peerSign }, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + + await expect(produce(producer, fixture.prepared, { + ...fixture.publication, + issuedAt: '2026-02-30T12:00:00.000Z', + })).rejects.toThrow(/calendar-valid/); + expect(peerSign).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('rejects a seal whose author attestation does not recover the profile authority', async () => { + const fixture = await producerFixture(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: fixture.peerSigner, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install: () => {}, + }); + const tampered = { + ...fixture.publication, + seal: { + ...fixture.publication.seal, + authorAttestationR: `0x${'22'.repeat(32)}`, + authorAttestationVS: `0x${'33'.repeat(32)}`, + }, + } as AgentProfilePublicationBindingV1; + await expect(produce(producer, fixture.prepared, tampered)) + .rejects.toThrow(/attestation does not recover/); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('rolls back when the peer signer returns an invalid profile-head signature', async () => { + const fixture = await producerFixture(); + const install = vi.fn(); + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { ...fixture.peerSigner, sign: async () => new Uint8Array(64) }, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + + await expect(produce(producer, fixture.prepared, fixture.publication)) + .rejects.toThrow(/head signature verification failed/); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + + it('rolls back when the peer signer returns an invalid inventory-root signature', async () => { + const fixture = await producerFixture(); + const install = vi.fn(); + let signatureNumber = 0; + const producer = createAgentProfileProducerV1({ + networkId: NETWORK, + publicationDeployment: DEPLOYMENT, + peerSigner: { + ...fixture.peerSigner, + sign: async (message) => { + signatureNumber += 1; + return signatureNumber === 1 + ? fixture.peerSigner.sign(message) + : new Uint8Array(64); + }, + }, + evmSigner: fixture.evmSigner, + store: fixture.store, + fence: () => {}, + install, + }); + + await expect(produce(producer, fixture.prepared, fixture.publication)) + .rejects.toThrow(/inventory root signature verification failed/); + expect(install).not.toHaveBeenCalled(); + expect(fixture.store.snapshot().currentHead).toBeNull(); + }); + +}); diff --git a/packages/core/src/agent-profile-projection-schema-v1.ts b/packages/core/src/agent-profile-projection-schema-v1.ts index 0af37e7148..e6ec80849c 100644 --- a/packages/core/src/agent-profile-projection-schema-v1.ts +++ b/packages/core/src/agent-profile-projection-schema-v1.ts @@ -1,40 +1,19 @@ import { decodeWorkspaceEncryptionKey } from './crypto/workspace-encryption.js'; import { assertSafeIri, assertSafeRdfTerm, isSafeIri } from './sparql-safe.js'; import { - AGENT_PROFILE_LINK_PREDICATES_V1, + AGENT_PROFILE_SCHEMA_TERMS_V1, + agentProfilePredicatePolicyV1, + agentProfileSubjectPolicyV1, + classifyAgentProfileOwnedSubjectV1, +} from './agent-profile-schema-model-v1.js'; +import { assertDerivedAgentEncryptionSubjectV1, assertOwnedSubjectTableObjectV1, - classifyAgentProfileOwnedSubjectV1, - isAllowedAgentProfilePredicateV1, type AgentProfileHeadCommonV1, - type AgentProfileOwnedSubjectKindV1, type OwnedSubjectTableObjectV1, } from './system-record-objects-v1.js'; -const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; -const DKG = 'https://dkg.network/ontology#'; -const ERC8004 = 'https://eips.ethereum.org/erc-8004#'; -const PROV = 'http://www.w3.org/ns/prov#'; -const SKILL = 'https://dkg.origintrail.io/skill#'; - -const IRI_OBJECT_PREDICATES = new Set([ - RDF_TYPE, - ...Object.values(AGENT_PROFILE_LINK_PREDICATES_V1), - `${SKILL}skill`, - `${SKILL}pricing`, - `${DKG}revokedBy`, -]); -const PUBLIC_ENCRYPTION_KEY = `${DKG}publicEncryptionKey`; -const ALLOWED_TYPE_OBJECTS: Readonly< - Record> -> = Object.freeze({ - root: new Set([`${DKG}Agent`, `${DKG}CoreNode`, `${DKG}EdgeNode`]), - capability: new Set([`${ERC8004}Capability`]), - offering: new Set([`${SKILL}SkillOffering`]), - registration: new Set([`${PROV}Activity`]), - hosting: new Set([`${SKILL}HostingProfile`]), - x25519: new Set(), -}); +const T = AGENT_PROFILE_SCHEMA_TERMS_V1; export interface AgentProfileProjectionQuadV1 { readonly subject: string; @@ -69,13 +48,13 @@ export function agentProfileIdentityFactsV1( ): AgentProfileIdentityFactsV1 { return Object.freeze({ rootSubject: input.rootSubject, - peerId: Object.freeze({ predicate: `${DKG}peerId`, object: `"${input.peerId}"` }), + peerId: Object.freeze({ predicate: T.dkgPeerId, object: `"${input.peerId}"` }), ...(input.publicKey === undefined ? {} : { - publicKey: Object.freeze({ predicate: `${DKG}publicKey`, object: `"${input.publicKey}"` }), + publicKey: Object.freeze({ predicate: T.dkgPublicKey, object: `"${input.publicKey}"` }), }), ...(input.agentAddress === undefined ? {} : { agentAddress: Object.freeze({ - predicate: `${DKG}agentAddress`, + predicate: T.dkgAgentAddress, object: `"${input.agentAddress}"`, }), }), @@ -133,7 +112,10 @@ export function assertAgentProfileProjectionSchemaV1( throw new Error(`profile projection quad ${index} has an unowned subject`); } const subjectKind = classifyAgentProfileOwnedSubjectV1(rootSubject, quad.subject); - if (subjectKind === null || !isAllowedAgentProfilePredicateV1(subjectKind, quad.predicate)) { + const predicatePolicy = subjectKind === null + ? undefined + : agentProfilePredicatePolicyV1(subjectKind, quad.predicate); + if (subjectKind === null || predicatePolicy === undefined) { throw new Error(`profile projection quad ${index} uses a disallowed profile predicate`); } const objectIsLiteral = quad.object.startsWith('"'); @@ -143,36 +125,31 @@ export function assertAgentProfileProjectionSchemaV1( } else assertSafeIri(quad.object); if (quad.graph !== '') throw new Error('profile projections must be graphless'); seenSubjects.add(quad.subject); - if (IRI_OBJECT_PREDICATES.has(quad.predicate) === objectIsLiteral) { + if ((predicatePolicy.objectTermKind === 'iri') === objectIsLiteral) { throw new Error('profile projection predicate has an invalid object term kind'); } - if (quad.predicate === RDF_TYPE && !ALLOWED_TYPE_OBJECTS[subjectKind].has(quad.object)) { + if (predicatePolicy.allowedObjects !== undefined + && !predicatePolicy.allowedObjects.includes(quad.object)) { throw new Error('profile projection rdf:type object is outside the frozen profile schema'); } - if (quad.subject === rootSubject) { - const linkKind = Object.entries(AGENT_PROFILE_LINK_PREDICATES_V1) - .find(([, predicate]) => predicate === quad.predicate)?.[0] as - | Exclude - | undefined; - if (linkKind !== undefined) { - if (objectIsLiteral || !ownedSubjects.has(quad.object) - || classifyAgentProfileOwnedSubjectV1(rootSubject, quad.object) !== linkKind) { - throw new Error('profile link does not target its exact derived-subject kind'); - } - linked.add(quad.object); + if (predicatePolicy.linkTargetKind !== undefined) { + if (objectIsLiteral || !ownedSubjects.has(quad.object) + || classifyAgentProfileOwnedSubjectV1(rootSubject, quad.object) + !== predicatePolicy.linkTargetKind) { + throw new Error('profile link does not target its exact derived-subject kind'); } - if (quad.predicate === PUBLIC_ENCRYPTION_KEY) { - const match = /^"([A-Za-z0-9_-]{43})"$/.exec(quad.object); - if (match === null) throw new Error('profile public encryption key is not canonical'); - try { - publicKeys.push(decodeWorkspaceEncryptionKey(match[1])); - } catch (cause) { - throw new Error('profile public encryption key is invalid', { cause }); - } + linked.add(quad.object); + } + if (predicatePolicy.capture === 'workspace-public-key') { + const match = /^"([A-Za-z0-9_-]{43})"$/.exec(quad.object); + if (match === null) throw new Error('profile public encryption key is not canonical'); + try { + publicKeys.push(decodeWorkspaceEncryptionKey(match[1])); + } catch (cause) { + throw new Error('profile public encryption key is invalid', { cause }); } } - if (subjectKind === 'x25519' && quad.predicate === `${DKG}revokedBy` - && quad.object !== rootSubject) { + if (predicatePolicy.objectBinding === 'profile-root' && quad.object !== rootSubject) { throw new Error('x25519 revocation does not bind the profile root'); } } @@ -181,9 +158,13 @@ export function assertAgentProfileProjectionSchemaV1( throw new Error('owned-subject table contains a subject absent from the projection'); } const kind = classifyAgentProfileOwnedSubjectV1(rootSubject, subject); - if (kind === 'capability' || kind === 'offering' || kind === 'registration' || kind === 'hosting') { + if (kind === null) { + throw new Error('owned-subject table contains an invalid subject'); + } + const subjectPolicy = agentProfileSubjectPolicyV1(kind); + if (subjectPolicy.rootLinkPredicate !== undefined) { if (!linked.has(subject)) throw new Error('derived profile subject is not linked from the root'); - } else if (kind === 'x25519') { + } else if (subjectPolicy.derivation === 'workspace-public-key') { const derived = publicKeys.some((key) => { try { assertDerivedAgentEncryptionSubjectV1(rootSubject, subject, key); diff --git a/packages/core/src/agent-profile-schema-model-v1.ts b/packages/core/src/agent-profile-schema-model-v1.ts new file mode 100644 index 0000000000..6b6b1c60bc --- /dev/null +++ b/packages/core/src/agent-profile-schema-model-v1.ts @@ -0,0 +1,303 @@ +import { SYSTEM_RECORD_OBJECT_CAPS_V1 } from './system-record-limits-v1.js'; + +export type AgentProfileOwnedSubjectKindV1 = + | 'root' + | 'capability' + | 'offering' + | 'registration' + | 'hosting' + | 'x25519'; + +export type AgentProfileLinkedSubjectKindV1 = Exclude< + AgentProfileOwnedSubjectKindV1, + 'root' | 'x25519' +>; + +export type AgentProfileObjectTermKindV1 = 'iri' | 'literal'; + +export interface AgentProfilePredicatePolicyV1 { + readonly predicate: string; + readonly objectTermKind: AgentProfileObjectTermKindV1; + readonly allowedObjects?: readonly string[]; + readonly linkTargetKind?: AgentProfileLinkedSubjectKindV1; + readonly objectBinding?: 'profile-root'; + readonly capture?: 'workspace-public-key'; +} + +export type AgentProfileSubjectShapeV1 = + | Readonly<{ readonly type: 'root' }> + | Readonly<{ readonly type: 'indexed-genid'; readonly prefix: string }> + | Readonly<{ readonly type: 'exact-genid'; readonly suffix: string }> + | Readonly<{ + readonly type: 'hex-fragment'; + readonly prefix: string; + readonly hexLength: number; + }>; + +export interface AgentProfileSubjectPolicyV1 { + readonly kind: AgentProfileOwnedSubjectKindV1; + readonly subjectShape: AgentProfileSubjectShapeV1; + readonly predicates: readonly Readonly[]; + readonly rootLinkPredicate?: string; + readonly derivation?: 'workspace-public-key'; +} + +const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; +const SCHEMA = 'https://schema.org/'; +const DKG = 'https://dkg.network/ontology#'; +const ERC8004 = 'https://eips.ethereum.org/erc-8004#'; +const PROV = 'http://www.w3.org/ns/prov#'; +const SKILL = 'https://dkg.origintrail.io/skill#'; + +/** Named V1 terms used by both profile authors and verifiers. */ +export const AGENT_PROFILE_SCHEMA_TERMS_V1 = Object.freeze({ + skillNamespace: SKILL, + rdfType: `${RDF}type`, + schemaName: `${SCHEMA}name`, + schemaDescription: `${SCHEMA}description`, + dkgAgent: `${DKG}Agent`, + dkgCoreNode: `${DKG}CoreNode`, + dkgEdgeNode: `${DKG}EdgeNode`, + dkgPeerId: `${DKG}peerId`, + dkgNodeRole: `${DKG}nodeRole`, + dkgPublicKey: `${DKG}publicKey`, + dkgRelayAddress: `${DKG}relayAddress`, + dkgAgentAddress: `${DKG}agentAddress`, + dkgMultiaddr: `${DKG}multiaddr`, + dkgLastSeen: `${DKG}lastSeen`, + dkgPublicEncryptionKey: `${DKG}publicEncryptionKey`, + dkgEncryptionKeyAlgorithm: `${DKG}encryptionKeyAlgorithm`, + dkgEncryptionKeyProof: `${DKG}encryptionKeyProof`, + dkgRevokedAt: `${DKG}revokedAt`, + dkgRevokedBy: `${DKG}revokedBy`, + dkgEncryptionKeyRevocationProof: `${DKG}encryptionKeyRevocationProof`, + erc8004Capabilities: `${ERC8004}capabilities`, + erc8004Capability: `${ERC8004}Capability`, + provWasGeneratedBy: `${PROV}wasGeneratedBy`, + provActivity: `${PROV}Activity`, + provAtTime: `${PROV}atTime`, + skillFramework: `${SKILL}framework`, + skillOffersSkill: `${SKILL}offersSkill`, + skillSkillOffering: `${SKILL}SkillOffering`, + skillSkill: `${SKILL}skill`, + skillPricePerCall: `${SKILL}pricePerCall`, + skillCurrency: `${SKILL}currency`, + skillSuccessRate: `${SKILL}successRate`, + skillPricing: `${SKILL}pricing`, + skillHostingProfile: `${SKILL}hostingProfile`, + skillHostingProfileType: `${SKILL}HostingProfile`, + skillContextGraphsServed: `${SKILL}contextGraphsServed`, + skillParanetsServed: `${SKILL}paranetsServed`, +} as const); + +const T = AGENT_PROFILE_SCHEMA_TERMS_V1; + +export const AGENT_PROFILE_LINK_PREDICATES_V1 = Object.freeze({ + capability: T.erc8004Capabilities, + offering: T.skillOffersSkill, + registration: T.provWasGeneratedBy, + hosting: T.skillHostingProfile, +} as const); + +function literal( + predicate: string, + extra: Omit = {}, +): Readonly { + return Object.freeze({ predicate, objectTermKind: 'literal' as const, ...extra }); +} + +function iri( + predicate: string, + extra: Omit = {}, +): Readonly { + return Object.freeze({ + predicate, + objectTermKind: 'iri' as const, + ...extra, + ...(extra.allowedObjects === undefined + ? {} + : { allowedObjects: Object.freeze([...extra.allowedObjects]) }), + }); +} + +function subjectPolicy( + policy: AgentProfileSubjectPolicyV1, +): Readonly { + return Object.freeze({ + ...policy, + subjectShape: Object.freeze({ ...policy.subjectShape }), + predicates: Object.freeze([...policy.predicates]), + }); +} + +const SUBJECT_POLICIES = Object.freeze([ + subjectPolicy({ + kind: 'root', + subjectShape: { type: 'root' }, + predicates: [ + iri(T.rdfType, { allowedObjects: [T.dkgAgent, T.dkgCoreNode, T.dkgEdgeNode] }), + literal(T.schemaName), + literal(T.schemaDescription), + literal(T.dkgPeerId), + literal(T.dkgNodeRole), + literal(T.dkgPublicKey), + literal(T.dkgRelayAddress), + literal(T.dkgAgentAddress), + literal(T.dkgMultiaddr), + literal(T.dkgLastSeen), + literal(T.dkgPublicEncryptionKey, { capture: 'workspace-public-key' }), + literal(T.dkgEncryptionKeyAlgorithm), + literal(T.dkgEncryptionKeyProof), + literal(T.skillFramework), + iri(T.erc8004Capabilities, { linkTargetKind: 'capability' }), + iri(T.skillOffersSkill, { linkTargetKind: 'offering' }), + iri(T.provWasGeneratedBy, { linkTargetKind: 'registration' }), + iri(T.skillHostingProfile, { linkTargetKind: 'hosting' }), + ], + }), + subjectPolicy({ + kind: 'capability', + subjectShape: { type: 'indexed-genid', prefix: 'cap' }, + rootLinkPredicate: T.erc8004Capabilities, + predicates: [ + iri(T.rdfType, { allowedObjects: [T.erc8004Capability] }), + literal(T.schemaName), + ], + }), + subjectPolicy({ + kind: 'offering', + subjectShape: { type: 'indexed-genid', prefix: 'offering' }, + rootLinkPredicate: T.skillOffersSkill, + predicates: [ + iri(T.rdfType, { allowedObjects: [T.skillSkillOffering] }), + iri(T.skillSkill), + literal(T.skillPricePerCall), + literal(T.skillCurrency), + literal(T.skillSuccessRate), + iri(T.skillPricing), + ], + }), + subjectPolicy({ + kind: 'registration', + subjectShape: { type: 'exact-genid', suffix: 'registration' }, + rootLinkPredicate: T.provWasGeneratedBy, + predicates: [ + iri(T.rdfType, { allowedObjects: [T.provActivity] }), + literal(T.provAtTime), + ], + }), + subjectPolicy({ + kind: 'hosting', + subjectShape: { type: 'exact-genid', suffix: 'hosting' }, + rootLinkPredicate: T.skillHostingProfile, + predicates: [ + iri(T.rdfType, { allowedObjects: [T.skillHostingProfileType] }), + literal(T.skillContextGraphsServed), + literal(T.skillParanetsServed), + ], + }), + subjectPolicy({ + kind: 'x25519', + subjectShape: { type: 'hex-fragment', prefix: 'x25519-', hexLength: 32 }, + derivation: 'workspace-public-key', + predicates: [ + literal(T.dkgRevokedAt), + iri(T.dkgRevokedBy, { objectBinding: 'profile-root' }), + literal(T.dkgEncryptionKeyRevocationProof), + ], + }), +] as const); + +export const AGENT_PROFILE_SCHEMA_V1 = Object.freeze({ + terms: AGENT_PROFILE_SCHEMA_TERMS_V1, + subjectPolicies: SUBJECT_POLICIES, +}); + +const ROOT_PATTERN = /^did:dkg:agent:(0x[0-9a-f]{40})$/; +const SUBJECT_POLICY_BY_KIND = new Map< + AgentProfileOwnedSubjectKindV1, + Readonly +>(SUBJECT_POLICIES.map((policy) => [policy.kind, policy])); +const PREDICATE_POLICY_BY_KIND = new Map( + SUBJECT_POLICIES.map((policy) => [ + policy.kind, + new Map(policy.predicates.map((predicate) => [predicate.predicate, predicate])), + ]), +); + +export function matchAgentProfileRootAddressV1(value: string): string | null { + return ROOT_PATTERN.exec(value)?.[1] ?? null; +} + +export function agentProfileSubjectPolicyV1( + kind: AgentProfileOwnedSubjectKindV1, +): Readonly { + return SUBJECT_POLICY_BY_KIND.get(kind)!; +} + +export function agentProfilePredicatePolicyV1( + kind: AgentProfileOwnedSubjectKindV1, + predicate: string, +): Readonly | undefined { + return PREDICATE_POLICY_BY_KIND.get(kind)?.get(predicate); +} + +export function deriveAgentProfileOwnedSubjectV1( + rootSubject: string, + kind: AgentProfileLinkedSubjectKindV1, + ordinal?: number, +): string { + const shape = agentProfileSubjectPolicyV1(kind).subjectShape; + if (shape.type === 'indexed-genid') { + if (!Number.isSafeInteger(ordinal) || ordinal === undefined || ordinal < 1) { + throw new RangeError(`profile ${kind} subject ordinal must be a positive safe integer`); + } + return `${rootSubject}/.well-known/genid/${shape.prefix}${ordinal}`; + } + if (shape.type === 'exact-genid') { + if (ordinal !== undefined) throw new TypeError(`profile ${kind} subject does not use an ordinal`); + return `${rootSubject}/.well-known/genid/${shape.suffix}`; + } + throw new TypeError(`profile ${kind} subject does not use a derived linked-subject shape`); +} + +export function classifyAgentProfileOwnedSubjectV1( + rootSubject: string, + subject: string, +): AgentProfileOwnedSubjectKindV1 | null { + if (typeof rootSubject !== 'string' || typeof subject !== 'string' + || rootSubject.length > SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'] + || subject.length > SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'] + || matchAgentProfileRootAddressV1(rootSubject) === null) return null; + const genidPrefix = `${rootSubject}/.well-known/genid/`; + const fragmentPrefix = `${rootSubject}#`; + for (const policy of SUBJECT_POLICIES) { + const shape = policy.subjectShape; + if (shape.type === 'root') { + if (subject === rootSubject) return policy.kind; + continue; + } + if (shape.type === 'indexed-genid') { + if (!subject.startsWith(genidPrefix)) continue; + const suffix = subject.slice(genidPrefix.length); + if (suffix.startsWith(shape.prefix) + && /^[1-9][0-9]*$/.test(suffix.slice(shape.prefix.length))) return policy.kind; + continue; + } + if (shape.type === 'exact-genid') { + if (subject === `${genidPrefix}${shape.suffix}`) return policy.kind; + continue; + } + if (!subject.startsWith(`${fragmentPrefix}${shape.prefix}`)) continue; + const suffix = subject.slice(fragmentPrefix.length + shape.prefix.length); + if (suffix.length === shape.hexLength && /^[0-9a-f]+$/.test(suffix)) return policy.kind; + } + return null; +} + +export function isAllowedAgentProfilePredicateV1( + kind: AgentProfileOwnedSubjectKindV1, + predicate: string, +): boolean { + return agentProfilePredicatePolicyV1(kind, predicate) !== undefined; +} diff --git a/packages/core/src/system-record-objects-v1.ts b/packages/core/src/system-record-objects-v1.ts index 4237cbdc46..fed75167cc 100644 --- a/packages/core/src/system-record-objects-v1.ts +++ b/packages/core/src/system-record-objects-v1.ts @@ -21,6 +21,10 @@ import { import { keccak256 } from './crypto/keccak.js'; import { workspaceAgentEncryptionKeyId } from './crypto/workspace-encryption.js'; import { parseDeterministicKnowledgeAssetUal } from './ka-content-scope.js'; +import { + classifyAgentProfileOwnedSubjectV1, + matchAgentProfileRootAddressV1, +} from './agent-profile-schema-model-v1.js'; import { assertCanonicalSystemRecordPeerIdV1, copyBoundedSystemRecordBytesV1, @@ -100,7 +104,6 @@ import { const UTF8 = new TextEncoder(); const RFC3339_SECONDS = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/; -const AGENT_ROOT = /^did:dkg:agent:(0x[0-9a-f]{40})$/; const REQUEST_RECORD_KIND = SYSTEM_RECORD_KIND_V1; export type CanonicalRfc3339SecondsV1 = string & { readonly __rfc3339SecondsV1: true }; @@ -112,6 +115,24 @@ export { SystemRecordObjectErrorV1, }; export type { SystemRecordObjectErrorCodeV1, SystemRecordPeerPublicKeyV1 }; +export { + AGENT_PROFILE_LINK_PREDICATES_V1, + AGENT_PROFILE_SCHEMA_TERMS_V1, + AGENT_PROFILE_SCHEMA_V1, + agentProfilePredicatePolicyV1, + agentProfileSubjectPolicyV1, + classifyAgentProfileOwnedSubjectV1, + deriveAgentProfileOwnedSubjectV1, + isAllowedAgentProfilePredicateV1, +} from './agent-profile-schema-model-v1.js'; +export type { + AgentProfileLinkedSubjectKindV1, + AgentProfileObjectTermKindV1, + AgentProfileOwnedSubjectKindV1, + AgentProfilePredicatePolicyV1, + AgentProfileSubjectPolicyV1, + AgentProfileSubjectShapeV1, +} from './agent-profile-schema-model-v1.js'; export interface AgentProfileHeadCommonV1 { readonly objectType: 'agent-profile-head'; @@ -295,10 +316,10 @@ export function assertSystemRecordPeerBindingV1( } export function assertAgentRootV1(value: unknown, issuer?: string): asserts value is string { - if (typeof value !== 'string' || !AGENT_ROOT.test(value)) { + if (typeof value !== 'string' || matchAgentProfileRootAddressV1(value) === null) { fail('system-record-scalar', 'agent root must be a canonical did:dkg:agent address'); } - const rootAddress = AGENT_ROOT.exec(value)![1]; + const rootAddress = matchAgentProfileRootAddressV1(value)!; try { assertCanonicalEvmAddress(rootAddress, 'agent root address'); } catch (cause) { @@ -794,78 +815,6 @@ function validateConflictEvidence(value: unknown): AgentProfileConflictEvidenceV return Object.freeze({ ...evidence, entries: Object.freeze(entries) }) as unknown as AgentProfileConflictEvidenceV1; } -export type AgentProfileOwnedSubjectKindV1 = - | 'root' - | 'capability' - | 'offering' - | 'registration' - | 'hosting' - | 'x25519'; - -const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; -const SCHEMA = 'https://schema.org/'; -const DKG = 'https://dkg.network/ontology#'; -const ERC8004 = 'https://eips.ethereum.org/erc-8004#'; -const PROV = 'http://www.w3.org/ns/prov#'; -const SKILL = 'https://dkg.origintrail.io/skill#'; - -export const AGENT_PROFILE_LINK_PREDICATES_V1 = Object.freeze({ - capability: `${ERC8004}capabilities`, - offering: `${SKILL}offersSkill`, - registration: `${PROV}wasGeneratedBy`, - hosting: `${SKILL}hostingProfile`, -} as const); - -const AGENT_PROFILE_PREDICATES_V1: Readonly>> = { - root: new Set([ - RDF_TYPE, `${SCHEMA}name`, `${SCHEMA}description`, `${DKG}peerId`, `${DKG}nodeRole`, - `${DKG}publicKey`, `${DKG}relayAddress`, `${DKG}agentAddress`, `${DKG}multiaddr`, - `${DKG}lastSeen`, `${DKG}publicEncryptionKey`, `${DKG}encryptionKeyAlgorithm`, - `${DKG}encryptionKeyProof`, `${SKILL}framework`, - ...Object.values(AGENT_PROFILE_LINK_PREDICATES_V1), - ]), - capability: new Set([RDF_TYPE, `${SCHEMA}name`]), - offering: new Set([ - RDF_TYPE, `${SKILL}skill`, `${SKILL}pricePerCall`, `${SKILL}currency`, - `${SKILL}successRate`, `${SKILL}pricing`, - ]), - registration: new Set([RDF_TYPE, `${PROV}atTime`]), - hosting: new Set([RDF_TYPE, `${SKILL}contextGraphsServed`, `${SKILL}paranetsServed`]), - x25519: new Set([`${DKG}revokedAt`, `${DKG}revokedBy`, `${DKG}encryptionKeyRevocationProof`]), -}; - -export function classifyAgentProfileOwnedSubjectV1( - rootSubject: string, - subject: string, -): AgentProfileOwnedSubjectKindV1 | null { - if (typeof rootSubject !== 'string' || typeof subject !== 'string' - || rootSubject.length > SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table'] - || subject.length > SYSTEM_RECORD_OBJECT_CAPS_V1['owned-subject-table']) return null; - if (!AGENT_ROOT.test(rootSubject)) return null; - if (subject === rootSubject) return 'root'; - const wellKnown = `${rootSubject}/.well-known/genid/`; - if (subject.startsWith(wellKnown)) { - const suffix = subject.slice(wellKnown.length); - if (/^cap[1-9][0-9]*$/.test(suffix)) return 'capability'; - if (/^offering[1-9][0-9]*$/.test(suffix)) return 'offering'; - if (suffix === 'registration') return 'registration'; - if (suffix === 'hosting') return 'hosting'; - return null; - } - const encryptionPrefix = `${rootSubject}#x25519-`; - return subject.startsWith(encryptionPrefix) - && /^[0-9a-f]{32}$/.test(subject.slice(encryptionPrefix.length)) - ? 'x25519' - : null; -} - -export function isAllowedAgentProfilePredicateV1( - kind: AgentProfileOwnedSubjectKindV1, - predicate: string, -): boolean { - return AGENT_PROFILE_PREDICATES_V1[kind].has(predicate); -} - export function assertDerivedAgentEncryptionSubjectV1( rootSubject: string, subject: string, @@ -881,7 +830,7 @@ export function assertDerivedAgentEncryptionSubjectV1( if (ownedPublicKey.byteLength !== 32) { fail('system-record-binding', 'x25519 public key must contain exactly 32 bytes'); } - const address = AGENT_ROOT.exec(rootSubject)![1]; + const address = matchAgentProfileRootAddressV1(rootSubject)!; const expected = workspaceAgentEncryptionKeyId(address, ownedPublicKey); if (subject !== expected) { fail('system-record-binding', 'x25519 owned subject is not derived from its root and public key'); diff --git a/packages/core/test/system-record-package-export-v1.mjs b/packages/core/test/system-record-package-export-v1.mjs index f3f60f3a2b..906234453e 100644 --- a/packages/core/test/system-record-package-export-v1.mjs +++ b/packages/core/test/system-record-package-export-v1.mjs @@ -2,6 +2,7 @@ const api = await import('@origintrail-official/dkg-core/system-record-v1'); const representativeExports = [ 'SYSTEM_RECORD_KIND_V1', + 'AGENT_PROFILE_SCHEMA_V1', 'computeAgentProfileHeadObjectDigestV1', 'parseCanonicalSystemRecordAppliedStateV1', 'computeSystemRecordRootDescriptorDigestV1', diff --git a/packages/core/test/system-record-policy-helpers-v1.test.ts b/packages/core/test/system-record-policy-helpers-v1.test.ts index a0697ab1f7..2faa4374bb 100644 --- a/packages/core/test/system-record-policy-helpers-v1.test.ts +++ b/packages/core/test/system-record-policy-helpers-v1.test.ts @@ -2,6 +2,10 @@ import { describe, expect, it } from 'vitest'; import { AGENT_PROFILE_LINK_PREDICATES_V1, + AGENT_PROFILE_SCHEMA_V1, + agentProfilePredicatePolicyV1, + agentProfileSubjectPolicyV1, + deriveAgentProfileOwnedSubjectV1, evaluateAuthorityTransitionConflictV1, isAllowedAgentProfilePredicateV1, type AgentProfileAuthorityTransitionV1, @@ -73,6 +77,20 @@ const UNLISTED_SAME_NAMESPACE_PREDICATES_V1 = [ const PROFILE_SUBJECT_KINDS_V1 = Object.keys( EXPECTED_ALLOWED_PROFILE_PREDICATES_V1, ) as AgentProfileOwnedSubjectKindV1[]; +const EXPECTED_IRI_OBJECT_PREDICATES_V1 = new Set([ + RDF_TYPE, + ...Object.values(EXPECTED_AGENT_PROFILE_LINK_PREDICATES_V1), + `${SKILL}skill`, + `${SKILL}pricing`, + `${DKG}revokedBy`, +]); +const EXPECTED_ALLOWED_TYPE_OBJECTS_V1 = { + root: [`${DKG}Agent`, `${DKG}CoreNode`, `${DKG}EdgeNode`], + capability: [`${ERC8004}Capability`], + offering: [`${SKILL}SkillOffering`], + registration: [`${PROV}Activity`], + hosting: [`${SKILL}HostingProfile`], +} as const; const FOREIGN_PEER = { peerId: '12D3KooWHwCJEQ7p5idnD7iQAWyCJHEW7rngKQiXCnEfGef69SV4', @@ -154,4 +172,49 @@ describe('system-record V1 public policy helpers', () => { expect(AGENT_PROFILE_LINK_PREDICATES_V1) .toEqual(EXPECTED_AGENT_PROFILE_LINK_PREDICATES_V1); }); + + it('exposes one immutable descriptor for subject, predicate, term, type, and link policy', () => { + expect(Object.isFrozen(AGENT_PROFILE_SCHEMA_V1)).toBe(true); + expect(Object.isFrozen(AGENT_PROFILE_SCHEMA_V1.terms)).toBe(true); + expect(Object.isFrozen(AGENT_PROFILE_SCHEMA_V1.subjectPolicies)).toBe(true); + expect(AGENT_PROFILE_SCHEMA_V1.subjectPolicies.map(({ kind }) => kind)) + .toEqual(PROFILE_SUBJECT_KINDS_V1); + + for (const kind of PROFILE_SUBJECT_KINDS_V1) { + const subjectPolicy = agentProfileSubjectPolicyV1(kind); + expect(Object.isFrozen(subjectPolicy), kind).toBe(true); + expect(Object.isFrozen(subjectPolicy.subjectShape), kind).toBe(true); + expect(Object.isFrozen(subjectPolicy.predicates), kind).toBe(true); + expect(subjectPolicy.predicates.map(({ predicate }) => predicate), kind) + .toEqual(EXPECTED_ALLOWED_PROFILE_PREDICATES_V1[kind]); + for (const predicate of subjectPolicy.predicates) { + expect(Object.isFrozen(predicate), `${kind}: ${predicate.predicate}`).toBe(true); + expect(predicate.objectTermKind, `${kind}: ${predicate.predicate}`) + .toBe(EXPECTED_IRI_OBJECT_PREDICATES_V1.has(predicate.predicate) ? 'iri' : 'literal'); + expect(agentProfilePredicatePolicyV1(kind, predicate.predicate)).toBe(predicate); + } + } + + for (const [kind, objects] of Object.entries(EXPECTED_ALLOWED_TYPE_OBJECTS_V1)) { + const typePolicy = agentProfilePredicatePolicyV1( + kind as AgentProfileOwnedSubjectKindV1, + RDF_TYPE, + ); + expect(typePolicy?.allowedObjects, kind).toEqual(objects); + expect(Object.isFrozen(typePolicy?.allowedObjects), kind).toBe(true); + } + expect(agentProfilePredicatePolicyV1('root', `${ERC8004}capabilities`)?.linkTargetKind) + .toBe('capability'); + expect(agentProfilePredicatePolicyV1('x25519', `${DKG}revokedBy`)?.objectBinding) + .toBe('profile-root'); + expect(agentProfilePredicatePolicyV1('root', `${DKG}publicEncryptionKey`)?.capture) + .toBe('workspace-public-key'); + const root = `did:dkg:agent:0x${'11'.repeat(20)}`; + expect(deriveAgentProfileOwnedSubjectV1(root, 'capability', 2)) + .toBe(`${root}/.well-known/genid/cap2`); + expect(deriveAgentProfileOwnedSubjectV1(root, 'hosting')) + .toBe(`${root}/.well-known/genid/hosting`); + expect(() => deriveAgentProfileOwnedSubjectV1(root, 'offering', 0)).toThrow(/positive/); + expect(() => deriveAgentProfileOwnedSubjectV1(root, 'registration', 1)).toThrow(/ordinal/); + }); }); From add4f8d3f952470ee11dc530e94992e8b4617ad9 Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sat, 8 Aug 2026 22:33:43 +0200 Subject: [PATCH 02/12] refactor(core): tighten profile schema type boundaries --- .../agent-profile-producer-contract-v1.ts | 2 +- .../agent-profile-producer-inventory-v1.ts | 10 +--- .../core/src/agent-profile-schema-model-v1.ts | 49 +++++++++++++------ packages/core/src/system-record-objects-v1.ts | 2 + .../system-record-policy-helpers-v1.test.ts | 9 +++- 5 files changed, 45 insertions(+), 27 deletions(-) diff --git a/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts b/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts index e9c03b5b14..ebafc6a814 100644 --- a/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts @@ -67,7 +67,7 @@ export interface AgentProfileProducerPublicationCommitV1 { readonly rootEnvelope: SignedSystemRecordRootDescriptorEnvelopeV1; } -type AgentProfileProducerArtifactV1 = Readonly< +export type AgentProfileProducerArtifactV1 = Readonly< Omit & { objectKind: Kind } >; diff --git a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts index 724ad577f6..c20a0cbf3c 100644 --- a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts @@ -20,19 +20,13 @@ import { import { flattenAgentProfileProducerPublicationArtifactsV1, + type AgentProfileProducerArtifactV1, type AgentProfileProducerPublicationArtifactsV1, type CreateAgentProfileProducerOptionsV1, } from './agent-profile-producer-contract-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; -import { - systemRecordArtifactKeyV1, - type SystemRecordArtifactV1, -} from './artifact-v1.js'; - -type AgentProfileProducerArtifactV1 = Readonly< - Omit & { objectKind: Kind } ->; +import { systemRecordArtifactKeyV1 } from './artifact-v1.js'; export interface AgentProfileProductionInventoryV1 { readonly inventory: SystemRecordInventoryTreeSnapshotV1; diff --git a/packages/core/src/agent-profile-schema-model-v1.ts b/packages/core/src/agent-profile-schema-model-v1.ts index 6b6b1c60bc..302841a2a8 100644 --- a/packages/core/src/agent-profile-schema-model-v1.ts +++ b/packages/core/src/agent-profile-schema-model-v1.ts @@ -12,6 +12,8 @@ export type AgentProfileLinkedSubjectKindV1 = Exclude< AgentProfileOwnedSubjectKindV1, 'root' | 'x25519' >; +export type AgentProfileIndexedSubjectKindV1 = 'capability' | 'offering'; +export type AgentProfileExactLinkedSubjectKindV1 = 'registration' | 'hosting'; export type AgentProfileObjectTermKindV1 = 'iri' | 'literal'; @@ -42,6 +44,14 @@ export interface AgentProfileSubjectPolicyV1 { readonly derivation?: 'workspace-public-key'; } +type AgentProfileSubjectPolicyForV1 = Readonly< + Omit & { readonly kind: Kind } +>; + +type AgentProfileSubjectPolicyTableV1 = { + readonly [Kind in AgentProfileOwnedSubjectKindV1]: AgentProfileSubjectPolicyForV1; +}; + const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; const SCHEMA = 'https://schema.org/'; const DKG = 'https://dkg.network/ontology#'; @@ -120,9 +130,9 @@ function iri( }); } -function subjectPolicy( - policy: AgentProfileSubjectPolicyV1, -): Readonly { +function subjectPolicy( + policy: AgentProfileSubjectPolicyForV1, +): AgentProfileSubjectPolicyForV1 { return Object.freeze({ ...policy, subjectShape: Object.freeze({ ...policy.subjectShape }), @@ -130,8 +140,8 @@ function subjectPolicy( }); } -const SUBJECT_POLICIES = Object.freeze([ - subjectPolicy({ +const SUBJECT_POLICY_BY_KIND = Object.freeze({ + root: subjectPolicy({ kind: 'root', subjectShape: { type: 'root' }, predicates: [ @@ -155,7 +165,7 @@ const SUBJECT_POLICIES = Object.freeze([ iri(T.skillHostingProfile, { linkTargetKind: 'hosting' }), ], }), - subjectPolicy({ + capability: subjectPolicy({ kind: 'capability', subjectShape: { type: 'indexed-genid', prefix: 'cap' }, rootLinkPredicate: T.erc8004Capabilities, @@ -164,7 +174,7 @@ const SUBJECT_POLICIES = Object.freeze([ literal(T.schemaName), ], }), - subjectPolicy({ + offering: subjectPolicy({ kind: 'offering', subjectShape: { type: 'indexed-genid', prefix: 'offering' }, rootLinkPredicate: T.skillOffersSkill, @@ -177,7 +187,7 @@ const SUBJECT_POLICIES = Object.freeze([ iri(T.skillPricing), ], }), - subjectPolicy({ + registration: subjectPolicy({ kind: 'registration', subjectShape: { type: 'exact-genid', suffix: 'registration' }, rootLinkPredicate: T.provWasGeneratedBy, @@ -186,7 +196,7 @@ const SUBJECT_POLICIES = Object.freeze([ literal(T.provAtTime), ], }), - subjectPolicy({ + hosting: subjectPolicy({ kind: 'hosting', subjectShape: { type: 'exact-genid', suffix: 'hosting' }, rootLinkPredicate: T.skillHostingProfile, @@ -196,7 +206,7 @@ const SUBJECT_POLICIES = Object.freeze([ literal(T.skillParanetsServed), ], }), - subjectPolicy({ + x25519: subjectPolicy({ kind: 'x25519', subjectShape: { type: 'hex-fragment', prefix: 'x25519-', hexLength: 32 }, derivation: 'workspace-public-key', @@ -206,7 +216,9 @@ const SUBJECT_POLICIES = Object.freeze([ literal(T.dkgEncryptionKeyRevocationProof), ], }), -] as const); +} satisfies AgentProfileSubjectPolicyTableV1); + +const SUBJECT_POLICIES = Object.freeze(Object.values(SUBJECT_POLICY_BY_KIND)); export const AGENT_PROFILE_SCHEMA_V1 = Object.freeze({ terms: AGENT_PROFILE_SCHEMA_TERMS_V1, @@ -214,10 +226,6 @@ export const AGENT_PROFILE_SCHEMA_V1 = Object.freeze({ }); const ROOT_PATTERN = /^did:dkg:agent:(0x[0-9a-f]{40})$/; -const SUBJECT_POLICY_BY_KIND = new Map< - AgentProfileOwnedSubjectKindV1, - Readonly ->(SUBJECT_POLICIES.map((policy) => [policy.kind, policy])); const PREDICATE_POLICY_BY_KIND = new Map( SUBJECT_POLICIES.map((policy) => [ policy.kind, @@ -232,7 +240,7 @@ export function matchAgentProfileRootAddressV1(value: string): string | null { export function agentProfileSubjectPolicyV1( kind: AgentProfileOwnedSubjectKindV1, ): Readonly { - return SUBJECT_POLICY_BY_KIND.get(kind)!; + return SUBJECT_POLICY_BY_KIND[kind]; } export function agentProfilePredicatePolicyV1( @@ -242,6 +250,15 @@ export function agentProfilePredicatePolicyV1( return PREDICATE_POLICY_BY_KIND.get(kind)?.get(predicate); } +export function deriveAgentProfileOwnedSubjectV1( + rootSubject: string, + kind: AgentProfileIndexedSubjectKindV1, + ordinal: number, +): string; +export function deriveAgentProfileOwnedSubjectV1( + rootSubject: string, + kind: AgentProfileExactLinkedSubjectKindV1, +): string; export function deriveAgentProfileOwnedSubjectV1( rootSubject: string, kind: AgentProfileLinkedSubjectKindV1, diff --git a/packages/core/src/system-record-objects-v1.ts b/packages/core/src/system-record-objects-v1.ts index fed75167cc..972eedd437 100644 --- a/packages/core/src/system-record-objects-v1.ts +++ b/packages/core/src/system-record-objects-v1.ts @@ -126,6 +126,8 @@ export { isAllowedAgentProfilePredicateV1, } from './agent-profile-schema-model-v1.js'; export type { + AgentProfileExactLinkedSubjectKindV1, + AgentProfileIndexedSubjectKindV1, AgentProfileLinkedSubjectKindV1, AgentProfileObjectTermKindV1, AgentProfileOwnedSubjectKindV1, diff --git a/packages/core/test/system-record-policy-helpers-v1.test.ts b/packages/core/test/system-record-policy-helpers-v1.test.ts index 2faa4374bb..0805b3ac7f 100644 --- a/packages/core/test/system-record-policy-helpers-v1.test.ts +++ b/packages/core/test/system-record-policy-helpers-v1.test.ts @@ -214,7 +214,12 @@ describe('system-record V1 public policy helpers', () => { .toBe(`${root}/.well-known/genid/cap2`); expect(deriveAgentProfileOwnedSubjectV1(root, 'hosting')) .toBe(`${root}/.well-known/genid/hosting`); - expect(() => deriveAgentProfileOwnedSubjectV1(root, 'offering', 0)).toThrow(/positive/); - expect(() => deriveAgentProfileOwnedSubjectV1(root, 'registration', 1)).toThrow(/ordinal/); + const uncheckedDerive = deriveAgentProfileOwnedSubjectV1 as unknown as ( + rootSubject: string, + kind: 'capability' | 'offering' | 'registration' | 'hosting', + ordinal?: number, + ) => string; + expect(() => uncheckedDerive(root, 'offering', 0)).toThrow(/positive/); + expect(() => uncheckedDerive(root, 'registration', 1)).toThrow(/ordinal/); }); }); From ae1dd897a6f1bcb5fe23ef1c7de0434d9b15284f Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sat, 8 Aug 2026 22:48:59 +0200 Subject: [PATCH 03/12] refactor(agent): narrow profile phase contracts --- .../agent-profile-producer-commit-v1.ts | 8 +- .../agent-profile-producer-contract-v1.ts | 26 ++++++ .../agent-profile-producer-inventory-v1.ts | 22 ++--- .../agent-profile-producer-preparation-v1.ts | 26 +++--- .../agent-profile-producer-signing-v1.ts | 14 +-- .../agent-profile-producer-v1.ts | 44 ++++++++- .../src/agent-profile-projection-schema-v1.ts | 77 +++++++++++----- .../core/src/agent-profile-schema-model-v1.ts | 90 ++++++++++++------- packages/core/src/system-record-objects-v1.ts | 1 - .../system-record-policy-helpers-v1.test.ts | 38 ++++++-- 10 files changed, 243 insertions(+), 103 deletions(-) diff --git a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts index c29c53a94c..85b45a82d5 100644 --- a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts @@ -1,19 +1,19 @@ import type { + AgentProfileProducerCommitDependenciesV1, AgentProfileProducerPublicationV1, - CreateAgentProfileProducerOptionsV1, } from './agent-profile-producer-contract-v1.js'; import type { AgentProfileProductionInventoryV1 } from './agent-profile-producer-inventory-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; export async function commitAgentProfileProductionV1( - options: CreateAgentProfileProducerOptionsV1, + dependencies: AgentProfileProducerCommitDependenciesV1, preparation: AgentProfileProductionPreparationV1, signed: SignedAgentProfileProductionV1, inventoryPlan: AgentProfileProductionInventoryV1, signal: AbortSignal, ): Promise { - const commitLease = await options.store.prepareCommit({ + const commitLease = await dependencies.prepareCommit({ expectedHeadDigest: preparation.snapshot.currentHead?.objectDigest ?? null, expectedRootDescriptorDigest: preparation.snapshot.inventory?.descriptorDigest ?? null, publicationArtifacts: inventoryPlan.publicationArtifacts, @@ -23,7 +23,7 @@ export async function commitAgentProfileProductionV1( let committed = false; try { signal.throwIfAborted(); - await options.install({ + await dependencies.install({ head: preparation.head, envelope: signed.envelope, canonicalProjectionBytes: preparation.projectionBytes, diff --git a/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts b/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts index ebafc6a814..5a9b1d2b52 100644 --- a/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts @@ -128,6 +128,32 @@ export interface CreateAgentProfileProducerOptionsV1 { readonly install: (input: AgentProfileProducerInstallInputV1) => void | Promise; } +export interface AgentProfileProducerPreparationDependenciesV1 { + readonly networkId: NetworkIdV1; + readonly publicationDeployment: Readonly; + readonly peerId: string; + readonly peerPublicKey: SystemRecordPeerPublicKeyV1; + readonly evmIssuer: string; + readonly nowMs?: () => number; + readonly snapshot: AgentProfileProducerPublicationStoreV1['snapshot']; +} + +export interface AgentProfileProducerSigningDependenciesV1 { + readonly peerSigner: SystemRecordPeerSignerV1; + readonly evmSigner: EvmPersonalMessageSignerV1; +} + +export interface AgentProfileProducerInventoryDependenciesV1 { + readonly networkId: NetworkIdV1; + readonly peerSigner: SystemRecordPeerSignerV1; + readonly resolveArtifact: AgentProfileProducerPublicationStoreV1['resolveArtifact']; +} + +export interface AgentProfileProducerCommitDependenciesV1 { + readonly prepareCommit: AgentProfileProducerPublicationStoreV1['prepareCommit']; + readonly install: CreateAgentProfileProducerOptionsV1['install']; +} + export interface AgentProfileProducerLeaseV1 { complete( publication: AgentProfilePublicationBindingV1, diff --git a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts index c20a0cbf3c..7a95bf19cf 100644 --- a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts @@ -21,8 +21,8 @@ import { import { flattenAgentProfileProducerPublicationArtifactsV1, type AgentProfileProducerArtifactV1, + type AgentProfileProducerInventoryDependenciesV1, type AgentProfileProducerPublicationArtifactsV1, - type CreateAgentProfileProducerOptionsV1, } from './agent-profile-producer-contract-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; @@ -38,17 +38,17 @@ export interface AgentProfileProductionInventoryV1 { } export async function prepareAgentProfileProductionInventoryV1( - options: CreateAgentProfileProducerOptionsV1, + dependencies: AgentProfileProducerInventoryDependenciesV1, preparation: AgentProfileProductionPreparationV1, signed: SignedAgentProfileProductionV1, signal: AbortSignal, ): Promise { const row: SystemRecordInventoryRowV1 = { stableKeyHash: computeSystemRecordStableKeyHashV1( - options.networkId, - options.peerSigner.peerId, + dependencies.networkId, + dependencies.peerSigner.peerId, ), - peerId: options.peerSigner.peerId, + peerId: dependencies.peerSigner.peerId, authoritySequence: preparation.head.authoritySequence, version: preparation.head.version, headDigest: preparation.headDigest, @@ -62,7 +62,7 @@ export async function prepareAgentProfileProductionInventoryV1( row, }); const inventory = inventoryUpdate === null - ? buildSystemRecordInventoryTreeV1(options.networkId, [row]) + ? buildSystemRecordInventoryTreeV1(dependencies.networkId, [row]) : applyInventoryUpdate(preparation.snapshot.inventory!, inventoryUpdate); const inventoryWrites = inventoryUpdate?.writes.length ?? inventory.objects.size + 1; @@ -71,24 +71,24 @@ export async function prepareAgentProfileProductionInventoryV1( (sum, object) => sum + object.canonicalBytes.byteLength, 0, ) + canonicalizeSystemRecordRootDescriptorObjectV1(inventory.descriptor).byteLength; - const rootSignature = await options.peerSigner.sign( + const rootSignature = await dependencies.peerSigner.sign( buildSystemRecordProviderSignatureMessageV1( inventory.descriptor, inventory.descriptorDigest, - options.peerSigner.peerId, + dependencies.peerSigner.peerId, ), ); signal.throwIfAborted(); const rootEnvelope: SignedSystemRecordRootDescriptorEnvelopeV1 = { object: inventory.descriptor, objectDigest: inventory.descriptorDigest, - providerPeerId: options.peerSigner.peerId, + providerPeerId: dependencies.peerSigner.peerId, signatureSuite: 'ed25519-v1', signature: Buffer.from(rootSignature).toString('base64url'), }; if (!await verifySignedSystemRecordRootDescriptorEnvelopeV1( rootEnvelope, - options.peerSigner.publicKey, + dependencies.peerSigner.publicKey, )) { throw new Error('new profile inventory root signature verification failed'); } @@ -112,7 +112,7 @@ export async function prepareAgentProfileProductionInventoryV1( objectDigest: digest, } as const; const artifact = artifactsByKey.get(systemRecordArtifactKeyV1(reference)) - ?? await options.store.resolveArtifact(reference); + ?? await dependencies.resolveArtifact(reference); return artifact === undefined || artifact === null ? undefined diff --git a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts index f733fc14c3..58b783d275 100644 --- a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts @@ -36,15 +36,15 @@ import type { Quad } from '@origintrail-official/dkg-storage'; import type { PreparedAgentProfileV1 } from '../profile.js'; import { assertRecoverableGraphScopedAuthorAttestationV1 } from '../rfc64/recoverable-author-attestation-v1.js'; import type { + AgentProfileProducerPreparationDependenciesV1, AgentProfilePublicationBindingV1, - CreateAgentProfileProducerOptionsV1, SystemRecordPeerSignerV1, } from './agent-profile-producer-contract-v1.js'; const UTF8 = new TextEncoder(); export interface AgentProfileProductionPreparationV1 { - readonly snapshot: ReturnType; + readonly snapshot: ReturnType; readonly verifierNowMs: number; readonly projectionQuads: readonly Readonly[]; readonly projectionBytes: Uint8Array; @@ -58,7 +58,7 @@ export interface AgentProfileProductionPreparationV1 { } export async function prepareAgentProfileProductionV1( - options: CreateAgentProfileProducerOptionsV1, + dependencies: AgentProfileProducerPreparationDependenciesV1, prepared: PreparedAgentProfileV1, projectionQuads: readonly Readonly[], inputPublication: AgentProfilePublicationBindingV1, @@ -71,7 +71,7 @@ export async function prepareAgentProfileProductionV1( 'assertionFinalizedAt', ); const assertionFinalizedAtMs = Date.parse(publication.seal.assertionFinalizedAt); - const verifierNowMs = producerNowMs(options.nowMs?.() ?? Date.now()); + const verifierNowMs = producerNowMs(dependencies.nowMs?.() ?? Date.now()); if (Date.parse(issuedAt) > verifierNowMs + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS) { throw new Error('agent-profile issuedAt exceeds the future clock-skew bound'); } @@ -84,13 +84,13 @@ export async function prepareAgentProfileProductionV1( if (Date.parse(validUntil) <= verifierNowMs) { throw new Error('agent-profile validUntil is already expired'); } - const evmIssuer = options.evmSigner.address; + const evmIssuer = dependencies.evmIssuer; assertCanonicalEvmAddress(evmIssuer, 'profile EVM issuer'); - const snapshot = options.store.snapshot(); + const snapshot = dependencies.snapshot(); const previous = snapshot.currentHead; if (previous !== null - && (previous.object.networkId !== options.networkId - || previous.object.peerId !== options.peerSigner.peerId)) { + && (previous.object.networkId !== dependencies.networkId + || previous.object.peerId !== dependencies.peerId)) { throw new Error('stored profile head belongs to a different stable record'); } if (previous !== null && !await verifySignedSystemRecordEnvelopeV1(previous)) { @@ -113,8 +113,8 @@ export async function prepareAgentProfileProductionV1( } assertPublicationLaneV1( publication.seal, - options.networkId, - options.publicationDeployment, + dependencies.networkId, + dependencies.publicationDeployment, evmIssuer, ); assertRecoverableGraphScopedAuthorAttestationV1(publication.seal); @@ -157,9 +157,9 @@ export async function prepareAgentProfileProductionV1( objectType: 'agent-profile-head', kind: 'agents', state: 'active', - networkId: options.networkId, - peerId: options.peerSigner.peerId, - peerPublicKey: options.peerSigner.publicKey, + networkId: dependencies.networkId, + peerId: dependencies.peerId, + peerPublicKey: dependencies.peerPublicKey, authoritySequence, version, ...(previous === null ? {} : { previousHeadDigest: previous.objectDigest }), diff --git a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts index 6580dfa2e1..e1b397916f 100644 --- a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts @@ -5,7 +5,9 @@ import { type SignedAgentProfileHeadEnvelopeV1, } from '@origintrail-official/dkg-core/system-record-v1'; -import type { CreateAgentProfileProducerOptionsV1 } from './agent-profile-producer-contract-v1.js'; +import type { + AgentProfileProducerSigningDependenciesV1, +} from './agent-profile-producer-contract-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; export interface SignedAgentProfileProductionV1 { @@ -14,19 +16,19 @@ export interface SignedAgentProfileProductionV1 { } export async function signAgentProfileProductionV1( - options: CreateAgentProfileProducerOptionsV1, + dependencies: AgentProfileProducerSigningDependenciesV1, preparation: AgentProfileProductionPreparationV1, signal: AbortSignal, ): Promise { const [peerSignature, evmSignature] = await Promise.all([ - options.peerSigner.sign( + dependencies.peerSigner.sign( buildSystemRecordSignatureMessageV1( preparation.head, preparation.headDigest, 'peer', ), ), - options.evmSigner.signMessage( + dependencies.evmSigner.signMessage( buildSystemRecordSignatureMessageV1( preparation.head, preparation.headDigest, @@ -42,14 +44,14 @@ export async function signAgentProfileProductionV1( Object.freeze({ role: 'peer', suite: 'ed25519-v1', - signer: options.peerSigner.peerId, + signer: dependencies.peerSigner.peerId, evidence: Object.freeze({ kind: 'none' }), signature: Buffer.from(peerSignature).toString('base64url'), }), Object.freeze({ role: 'current-evm', suite: 'eip191-personal-sign-digest-v1', - signer: options.evmSigner.address, + signer: dependencies.evmSigner.address, evidence: Object.freeze({ kind: 'none' }), signature: evmSignature, }), diff --git a/packages/agent/src/system-records/agent-profile-producer-v1.ts b/packages/agent/src/system-records/agent-profile-producer-v1.ts index f364d0b609..756204ae72 100644 --- a/packages/agent/src/system-records/agent-profile-producer-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-v1.ts @@ -5,8 +5,12 @@ import type { Quad } from '@origintrail-official/dkg-storage'; import type { PreparedAgentProfileV1 } from '../profile.js'; import { commitAgentProfileProductionV1 } from './agent-profile-producer-commit-v1.js'; import type { + AgentProfileProducerCommitDependenciesV1, + AgentProfileProducerInventoryDependenciesV1, AgentProfileProducerLeaseV1, + AgentProfileProducerPreparationDependenciesV1, AgentProfileProducerPublicationV1, + AgentProfileProducerSigningDependenciesV1, AgentProfileProducerV1, AgentProfilePublicationBindingV1, CreateAgentProfileProducerOptionsV1, @@ -29,6 +33,34 @@ export * from './agent-profile-producer-contract-v1.js'; export function createAgentProfileProducerV1( options: CreateAgentProfileProducerOptionsV1, ): AgentProfileProducerV1 { + const preparationDependencies: AgentProfileProducerPreparationDependenciesV1 = Object.freeze({ + networkId: options.networkId, + publicationDeployment: options.publicationDeployment, + peerId: options.peerSigner.peerId, + peerPublicKey: options.peerSigner.publicKey, + evmIssuer: options.evmSigner.address, + ...(options.nowMs === undefined ? {} : { nowMs: () => options.nowMs?.() ?? Date.now() }), + snapshot: () => options.store.snapshot(), + }); + const signingDependencies: AgentProfileProducerSigningDependenciesV1 = Object.freeze({ + peerSigner: options.peerSigner, + evmSigner: options.evmSigner, + }); + const resolveArtifact: AgentProfileProducerInventoryDependenciesV1['resolveArtifact'] = + (reference) => options.store.resolveArtifact(reference); + const inventoryDependencies: AgentProfileProducerInventoryDependenciesV1 = Object.freeze({ + networkId: options.networkId, + peerSigner: options.peerSigner, + resolveArtifact, + }); + const prepareCommit: AgentProfileProducerCommitDependenciesV1['prepareCommit'] = + (input) => options.store.prepareCommit(input); + const install: AgentProfileProducerCommitDependenciesV1['install'] = + (input) => options.install(input); + const commitDependencies: AgentProfileProducerCommitDependenciesV1 = Object.freeze({ + prepareCommit, + install, + }); let active = false; const completePrepared = async ( prepared: PreparedAgentProfileV1, @@ -38,20 +70,24 @@ export function createAgentProfileProducerV1( ): Promise => { signal.throwIfAborted(); const preparation = await prepareAgentProfileProductionV1( - options, + preparationDependencies, prepared, projectionQuads, publication, ); - const signed = await signAgentProfileProductionV1(options, preparation, signal); + const signed = await signAgentProfileProductionV1( + signingDependencies, + preparation, + signal, + ); const inventoryPlan = await prepareAgentProfileProductionInventoryV1( - options, + inventoryDependencies, preparation, signed, signal, ); return commitAgentProfileProductionV1( - options, + commitDependencies, preparation, signed, inventoryPlan, diff --git a/packages/core/src/agent-profile-projection-schema-v1.ts b/packages/core/src/agent-profile-projection-schema-v1.ts index e6ec80849c..ec611e41dc 100644 --- a/packages/core/src/agent-profile-projection-schema-v1.ts +++ b/packages/core/src/agent-profile-projection-schema-v1.ts @@ -125,33 +125,62 @@ export function assertAgentProfileProjectionSchemaV1( } else assertSafeIri(quad.object); if (quad.graph !== '') throw new Error('profile projections must be graphless'); seenSubjects.add(quad.subject); - if ((predicatePolicy.objectTermKind === 'iri') === objectIsLiteral) { - throw new Error('profile projection predicate has an invalid object term kind'); - } - if (predicatePolicy.allowedObjects !== undefined - && !predicatePolicy.allowedObjects.includes(quad.object)) { - throw new Error('profile projection rdf:type object is outside the frozen profile schema'); - } - if (predicatePolicy.linkTargetKind !== undefined) { - if (objectIsLiteral || !ownedSubjects.has(quad.object) - || classifyAgentProfileOwnedSubjectV1(rootSubject, quad.object) - !== predicatePolicy.linkTargetKind) { - throw new Error('profile link does not target its exact derived-subject kind'); + switch (predicatePolicy.objectPolicy) { + case 'literal': + if (!objectIsLiteral) { + throw new Error('profile projection predicate has an invalid object term kind'); + } + break; + case 'iri': + if (objectIsLiteral) { + throw new Error('profile projection predicate has an invalid object term kind'); + } + break; + case 'allowed-iri': + if (objectIsLiteral) { + throw new Error('profile projection predicate has an invalid object term kind'); + } + if (!predicatePolicy.allowedObjects.includes(quad.object)) { + throw new Error('profile projection rdf:type object is outside the frozen profile schema'); + } + break; + case 'owned-subject-link': + if (objectIsLiteral) { + throw new Error('profile projection predicate has an invalid object term kind'); + } + if (!ownedSubjects.has(quad.object) + || classifyAgentProfileOwnedSubjectV1(rootSubject, quad.object) + !== predicatePolicy.linkTargetKind) { + throw new Error('profile link does not target its exact derived-subject kind'); + } + linked.add(quad.object); + break; + case 'workspace-public-key': { + if (!objectIsLiteral) { + throw new Error('profile projection predicate has an invalid object term kind'); + } + const match = /^"([A-Za-z0-9_-]{43})"$/.exec(quad.object); + if (match === null) throw new Error('profile public encryption key is not canonical'); + try { + publicKeys.push(decodeWorkspaceEncryptionKey(match[1])); + } catch (cause) { + throw new Error('profile public encryption key is invalid', { cause }); + } + break; } - linked.add(quad.object); - } - if (predicatePolicy.capture === 'workspace-public-key') { - const match = /^"([A-Za-z0-9_-]{43})"$/.exec(quad.object); - if (match === null) throw new Error('profile public encryption key is not canonical'); - try { - publicKeys.push(decodeWorkspaceEncryptionKey(match[1])); - } catch (cause) { - throw new Error('profile public encryption key is invalid', { cause }); + case 'profile-root-iri': + if (objectIsLiteral) { + throw new Error('profile projection predicate has an invalid object term kind'); + } + if (quad.object !== rootSubject) { + throw new Error('x25519 revocation does not bind the profile root'); + } + break; + default: { + const unsupported: never = predicatePolicy; + throw new TypeError(`unsupported profile predicate policy: ${String(unsupported)}`); } } - if (predicatePolicy.objectBinding === 'profile-root' && quad.object !== rootSubject) { - throw new Error('x25519 revocation does not bind the profile root'); - } } for (const subject of ownedSubjectTable) { if (!seenSubjects.has(subject)) { diff --git a/packages/core/src/agent-profile-schema-model-v1.ts b/packages/core/src/agent-profile-schema-model-v1.ts index 302841a2a8..59fb38e25d 100644 --- a/packages/core/src/agent-profile-schema-model-v1.ts +++ b/packages/core/src/agent-profile-schema-model-v1.ts @@ -15,16 +15,27 @@ export type AgentProfileLinkedSubjectKindV1 = Exclude< export type AgentProfileIndexedSubjectKindV1 = 'capability' | 'offering'; export type AgentProfileExactLinkedSubjectKindV1 = 'registration' | 'hosting'; -export type AgentProfileObjectTermKindV1 = 'iri' | 'literal'; - -export interface AgentProfilePredicatePolicyV1 { - readonly predicate: string; - readonly objectTermKind: AgentProfileObjectTermKindV1; - readonly allowedObjects?: readonly string[]; - readonly linkTargetKind?: AgentProfileLinkedSubjectKindV1; - readonly objectBinding?: 'profile-root'; - readonly capture?: 'workspace-public-key'; -} +export type AgentProfilePredicatePolicyV1 = + | Readonly<{ readonly predicate: string; readonly objectPolicy: 'literal' }> + | Readonly<{ readonly predicate: string; readonly objectPolicy: 'iri' }> + | Readonly<{ + readonly predicate: string; + readonly objectPolicy: 'allowed-iri'; + readonly allowedObjects: readonly string[]; + }> + | Readonly<{ + readonly predicate: string; + readonly objectPolicy: 'owned-subject-link'; + readonly linkTargetKind: AgentProfileLinkedSubjectKindV1; + }> + | Readonly<{ + readonly predicate: string; + readonly objectPolicy: 'profile-root-iri'; + }> + | Readonly<{ + readonly predicate: string; + readonly objectPolicy: 'workspace-public-key'; + }>; export type AgentProfileSubjectShapeV1 = | Readonly<{ readonly type: 'root' }> @@ -109,27 +120,44 @@ export const AGENT_PROFILE_LINK_PREDICATES_V1 = Object.freeze({ hosting: T.skillHostingProfile, } as const); -function literal( +function literal(predicate: string): Readonly { + return Object.freeze({ predicate, objectPolicy: 'literal' as const }); +} + +function iri(predicate: string): Readonly { + return Object.freeze({ predicate, objectPolicy: 'iri' as const }); +} + +function allowedIri( predicate: string, - extra: Omit = {}, + allowedObjects: readonly string[], ): Readonly { - return Object.freeze({ predicate, objectTermKind: 'literal' as const, ...extra }); + return Object.freeze({ + predicate, + objectPolicy: 'allowed-iri' as const, + allowedObjects: Object.freeze([...allowedObjects]), + }); } -function iri( +function ownedSubjectLink( predicate: string, - extra: Omit = {}, + linkTargetKind: AgentProfileLinkedSubjectKindV1, ): Readonly { return Object.freeze({ predicate, - objectTermKind: 'iri' as const, - ...extra, - ...(extra.allowedObjects === undefined - ? {} - : { allowedObjects: Object.freeze([...extra.allowedObjects]) }), + objectPolicy: 'owned-subject-link' as const, + linkTargetKind, }); } +function profileRootIri(predicate: string): Readonly { + return Object.freeze({ predicate, objectPolicy: 'profile-root-iri' as const }); +} + +function workspacePublicKey(predicate: string): Readonly { + return Object.freeze({ predicate, objectPolicy: 'workspace-public-key' as const }); +} + function subjectPolicy( policy: AgentProfileSubjectPolicyForV1, ): AgentProfileSubjectPolicyForV1 { @@ -145,7 +173,7 @@ const SUBJECT_POLICY_BY_KIND = Object.freeze({ kind: 'root', subjectShape: { type: 'root' }, predicates: [ - iri(T.rdfType, { allowedObjects: [T.dkgAgent, T.dkgCoreNode, T.dkgEdgeNode] }), + allowedIri(T.rdfType, [T.dkgAgent, T.dkgCoreNode, T.dkgEdgeNode]), literal(T.schemaName), literal(T.schemaDescription), literal(T.dkgPeerId), @@ -155,14 +183,14 @@ const SUBJECT_POLICY_BY_KIND = Object.freeze({ literal(T.dkgAgentAddress), literal(T.dkgMultiaddr), literal(T.dkgLastSeen), - literal(T.dkgPublicEncryptionKey, { capture: 'workspace-public-key' }), + workspacePublicKey(T.dkgPublicEncryptionKey), literal(T.dkgEncryptionKeyAlgorithm), literal(T.dkgEncryptionKeyProof), literal(T.skillFramework), - iri(T.erc8004Capabilities, { linkTargetKind: 'capability' }), - iri(T.skillOffersSkill, { linkTargetKind: 'offering' }), - iri(T.provWasGeneratedBy, { linkTargetKind: 'registration' }), - iri(T.skillHostingProfile, { linkTargetKind: 'hosting' }), + ownedSubjectLink(T.erc8004Capabilities, 'capability'), + ownedSubjectLink(T.skillOffersSkill, 'offering'), + ownedSubjectLink(T.provWasGeneratedBy, 'registration'), + ownedSubjectLink(T.skillHostingProfile, 'hosting'), ], }), capability: subjectPolicy({ @@ -170,7 +198,7 @@ const SUBJECT_POLICY_BY_KIND = Object.freeze({ subjectShape: { type: 'indexed-genid', prefix: 'cap' }, rootLinkPredicate: T.erc8004Capabilities, predicates: [ - iri(T.rdfType, { allowedObjects: [T.erc8004Capability] }), + allowedIri(T.rdfType, [T.erc8004Capability]), literal(T.schemaName), ], }), @@ -179,7 +207,7 @@ const SUBJECT_POLICY_BY_KIND = Object.freeze({ subjectShape: { type: 'indexed-genid', prefix: 'offering' }, rootLinkPredicate: T.skillOffersSkill, predicates: [ - iri(T.rdfType, { allowedObjects: [T.skillSkillOffering] }), + allowedIri(T.rdfType, [T.skillSkillOffering]), iri(T.skillSkill), literal(T.skillPricePerCall), literal(T.skillCurrency), @@ -192,7 +220,7 @@ const SUBJECT_POLICY_BY_KIND = Object.freeze({ subjectShape: { type: 'exact-genid', suffix: 'registration' }, rootLinkPredicate: T.provWasGeneratedBy, predicates: [ - iri(T.rdfType, { allowedObjects: [T.provActivity] }), + allowedIri(T.rdfType, [T.provActivity]), literal(T.provAtTime), ], }), @@ -201,7 +229,7 @@ const SUBJECT_POLICY_BY_KIND = Object.freeze({ subjectShape: { type: 'exact-genid', suffix: 'hosting' }, rootLinkPredicate: T.skillHostingProfile, predicates: [ - iri(T.rdfType, { allowedObjects: [T.skillHostingProfileType] }), + allowedIri(T.rdfType, [T.skillHostingProfileType]), literal(T.skillContextGraphsServed), literal(T.skillParanetsServed), ], @@ -212,7 +240,7 @@ const SUBJECT_POLICY_BY_KIND = Object.freeze({ derivation: 'workspace-public-key', predicates: [ literal(T.dkgRevokedAt), - iri(T.dkgRevokedBy, { objectBinding: 'profile-root' }), + profileRootIri(T.dkgRevokedBy), literal(T.dkgEncryptionKeyRevocationProof), ], }), diff --git a/packages/core/src/system-record-objects-v1.ts b/packages/core/src/system-record-objects-v1.ts index 972eedd437..c15b4b04c5 100644 --- a/packages/core/src/system-record-objects-v1.ts +++ b/packages/core/src/system-record-objects-v1.ts @@ -129,7 +129,6 @@ export type { AgentProfileExactLinkedSubjectKindV1, AgentProfileIndexedSubjectKindV1, AgentProfileLinkedSubjectKindV1, - AgentProfileObjectTermKindV1, AgentProfileOwnedSubjectKindV1, AgentProfilePredicatePolicyV1, AgentProfileSubjectPolicyV1, diff --git a/packages/core/test/system-record-policy-helpers-v1.test.ts b/packages/core/test/system-record-policy-helpers-v1.test.ts index 0805b3ac7f..7887fedd44 100644 --- a/packages/core/test/system-record-policy-helpers-v1.test.ts +++ b/packages/core/test/system-record-policy-helpers-v1.test.ts @@ -84,6 +84,9 @@ const EXPECTED_IRI_OBJECT_PREDICATES_V1 = new Set([ `${SKILL}pricing`, `${DKG}revokedBy`, ]); +const EXPECTED_LINK_OBJECT_PREDICATES_V1 = new Set( + Object.values(EXPECTED_AGENT_PROFILE_LINK_PREDICATES_V1), +); const EXPECTED_ALLOWED_TYPE_OBJECTS_V1 = { root: [`${DKG}Agent`, `${DKG}CoreNode`, `${DKG}EdgeNode`], capability: [`${ERC8004}Capability`], @@ -189,8 +192,19 @@ describe('system-record V1 public policy helpers', () => { .toEqual(EXPECTED_ALLOWED_PROFILE_PREDICATES_V1[kind]); for (const predicate of subjectPolicy.predicates) { expect(Object.isFrozen(predicate), `${kind}: ${predicate.predicate}`).toBe(true); - expect(predicate.objectTermKind, `${kind}: ${predicate.predicate}`) - .toBe(EXPECTED_IRI_OBJECT_PREDICATES_V1.has(predicate.predicate) ? 'iri' : 'literal'); + const expectedObjectPolicy = predicate.predicate === RDF_TYPE + ? 'allowed-iri' + : EXPECTED_LINK_OBJECT_PREDICATES_V1.has(predicate.predicate) + ? 'owned-subject-link' + : predicate.predicate === `${DKG}revokedBy` + ? 'profile-root-iri' + : predicate.predicate === `${DKG}publicEncryptionKey` + ? 'workspace-public-key' + : EXPECTED_IRI_OBJECT_PREDICATES_V1.has(predicate.predicate) + ? 'iri' + : 'literal'; + expect(predicate.objectPolicy, `${kind}: ${predicate.predicate}`) + .toBe(expectedObjectPolicy); expect(agentProfilePredicatePolicyV1(kind, predicate.predicate)).toBe(predicate); } } @@ -200,14 +214,20 @@ describe('system-record V1 public policy helpers', () => { kind as AgentProfileOwnedSubjectKindV1, RDF_TYPE, ); - expect(typePolicy?.allowedObjects, kind).toEqual(objects); - expect(Object.isFrozen(typePolicy?.allowedObjects), kind).toBe(true); + expect(typePolicy?.objectPolicy, kind).toBe('allowed-iri'); + if (typePolicy?.objectPolicy !== 'allowed-iri') throw new Error('expected type policy'); + expect(typePolicy.allowedObjects, kind).toEqual(objects); + expect(Object.isFrozen(typePolicy.allowedObjects), kind).toBe(true); } - expect(agentProfilePredicatePolicyV1('root', `${ERC8004}capabilities`)?.linkTargetKind) - .toBe('capability'); - expect(agentProfilePredicatePolicyV1('x25519', `${DKG}revokedBy`)?.objectBinding) - .toBe('profile-root'); - expect(agentProfilePredicatePolicyV1('root', `${DKG}publicEncryptionKey`)?.capture) + const capabilityLink = agentProfilePredicatePolicyV1('root', `${ERC8004}capabilities`); + expect(capabilityLink?.objectPolicy).toBe('owned-subject-link'); + if (capabilityLink?.objectPolicy !== 'owned-subject-link') { + throw new Error('expected capability link policy'); + } + expect(capabilityLink.linkTargetKind).toBe('capability'); + expect(agentProfilePredicatePolicyV1('x25519', `${DKG}revokedBy`)?.objectPolicy) + .toBe('profile-root-iri'); + expect(agentProfilePredicatePolicyV1('root', `${DKG}publicEncryptionKey`)?.objectPolicy) .toBe('workspace-public-key'); const root = `did:dkg:agent:0x${'11'.repeat(20)}`; expect(deriveAgentProfileOwnedSubjectV1(root, 'capability', 2)) From bf6ed34833aa0a951ef6b892766e37a3034ac61c Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sat, 8 Aug 2026 23:01:21 +0200 Subject: [PATCH 04/12] refactor(agent): bind validated profile phase input --- .../agent-profile-producer-preparation-v1.ts | 49 +++++++++++++------ .../agent-profile-producer-v1.ts | 28 ++++------- ...ile-producer-phase-boundaries.typecheck.ts | 19 +++++++ .../core/src/agent-profile-schema-model-v1.ts | 5 -- packages/core/src/system-record-objects-v1.ts | 6 --- .../test/system-record-package-export-v1.mjs | 2 +- .../system-record-policy-helpers-v1.test.ts | 46 +++++++++++++---- 7 files changed, 99 insertions(+), 56 deletions(-) create mode 100644 packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts diff --git a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts index 58b783d275..ecd867d819 100644 --- a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts @@ -43,6 +43,12 @@ import type { const UTF8 = new TextEncoder(); +export interface ValidatedAgentProfileProductionInputV1 { + readonly preparedSnapshot: PreparedAgentProfileV1; + readonly projectionQuads: readonly Readonly[]; + readonly ownedSubjectTable: OwnedSubjectTableObjectV1; +} + export interface AgentProfileProductionPreparationV1 { readonly snapshot: ReturnType; readonly verifierNowMs: number; @@ -59,10 +65,10 @@ export interface AgentProfileProductionPreparationV1 { export async function prepareAgentProfileProductionV1( dependencies: AgentProfileProducerPreparationDependenciesV1, - prepared: PreparedAgentProfileV1, - projectionQuads: readonly Readonly[], + input: ValidatedAgentProfileProductionInputV1, inputPublication: AgentProfilePublicationBindingV1, ): Promise { + const { preparedSnapshot: prepared, projectionQuads, ownedSubjectTable } = input; const publication = snapshotConfirmedPublicationBindingV1(inputPublication); const issuedAt = normalizePublicationTimestampV1(publication.issuedAt, 'issuedAt'); const validUntil = normalizePublicationTimestampV1(publication.validUntil, 'validUntil'); @@ -118,7 +124,6 @@ export async function prepareAgentProfileProductionV1( evmIssuer, ); assertRecoverableGraphScopedAuthorAttestationV1(publication.seal); - const ownedSubjectTable = ownedSubjects(prepared.rootEntity, projectionQuads); const ownedSubjectTableBytes = canonicalizeOwnedSubjectTableObjectV1( prepared.rootEntity, ownedSubjectTable, @@ -196,29 +201,43 @@ export async function prepareAgentProfileProductionV1( }); } -export function validateAgentProfileProjectionV1( +export function validateAgentProfileProductionInputV1( + dependencies: Pick< + AgentProfileProducerPreparationDependenciesV1, + 'peerId' | 'peerPublicKey' | 'evmIssuer' + >, prepared: PreparedAgentProfileV1, -): readonly Readonly[] { - const projected = prepared.projectionQuads.map((quad) => Object.freeze({ ...quad })); +): ValidatedAgentProfileProductionInputV1 { + const preparedSnapshot = snapshotPreparedProfileV1(prepared); + const projected = preparedSnapshot.projectionQuads + .map((quad) => Object.freeze({ ...quad })); projected.sort(compareQuads); for (let index = 1; index < projected.length; index += 1) { if (compareQuads(projected[index - 1]!, projected[index]!) === 0) { throw new Error('profile projection must be canonical and duplicate-free'); } } + let ownedSubjectTable: OwnedSubjectTableObjectV1; try { - assertAgentProfileProjectionSchemaV1( - prepared.rootEntity, - ownedSubjects(prepared.rootEntity, projected), - projected, - ); + ownedSubjectTable = ownedSubjects(preparedSnapshot.rootEntity, projected); + assertAgentProfileProjectionSchemaV1(preparedSnapshot.rootEntity, ownedSubjectTable, projected); } catch (cause) { throw new Error('profile projection is outside schema V1', { cause }); } - return Object.freeze(projected); + assertAdvertisedAgentProfileIdentityV1( + preparedSnapshot.rootEntity, + projected, + { peerId: dependencies.peerId, publicKey: dependencies.peerPublicKey }, + dependencies.evmIssuer, + ); + return Object.freeze({ + preparedSnapshot, + projectionQuads: Object.freeze(projected), + ownedSubjectTable, + }); } -export function snapshotPreparedProfileV1( +function snapshotPreparedProfileV1( prepared: PreparedAgentProfileV1, ): PreparedAgentProfileV1 { if (!Array.isArray(prepared.publicationQuads) @@ -239,10 +258,10 @@ export function snapshotPreparedProfileV1( }); } -export function assertAdvertisedAgentProfileIdentityV1( +function assertAdvertisedAgentProfileIdentityV1( rootSubject: string, quads: readonly Readonly[], - peerSigner: SystemRecordPeerSignerV1, + peerSigner: Pick, evmAddress: string, ): void { assertCanonicalEvmAddress(evmAddress, 'profile EVM issuer'); diff --git a/packages/agent/src/system-records/agent-profile-producer-v1.ts b/packages/agent/src/system-records/agent-profile-producer-v1.ts index 756204ae72..e7ffd09606 100644 --- a/packages/agent/src/system-records/agent-profile-producer-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-v1.ts @@ -1,7 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -import type { Quad } from '@origintrail-official/dkg-storage'; - import type { PreparedAgentProfileV1 } from '../profile.js'; import { commitAgentProfileProductionV1 } from './agent-profile-producer-commit-v1.js'; import type { @@ -17,10 +15,9 @@ import type { } from './agent-profile-producer-contract-v1.js'; import { prepareAgentProfileProductionInventoryV1 } from './agent-profile-producer-inventory-v1.js'; import { - assertAdvertisedAgentProfileIdentityV1, prepareAgentProfileProductionV1, - snapshotPreparedProfileV1, - validateAgentProfileProjectionV1, + validateAgentProfileProductionInputV1, + type ValidatedAgentProfileProductionInputV1, } from './agent-profile-producer-preparation-v1.js'; import { signAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; @@ -63,16 +60,14 @@ export function createAgentProfileProducerV1( }); let active = false; const completePrepared = async ( - prepared: PreparedAgentProfileV1, - projectionQuads: readonly Readonly[], + input: ValidatedAgentProfileProductionInputV1, publication: AgentProfilePublicationBindingV1, signal: AbortSignal, ): Promise => { signal.throwIfAborted(); const preparation = await prepareAgentProfileProductionV1( preparationDependencies, - prepared, - projectionQuads, + input, publication, ); const signed = await signAgentProfileProductionV1( @@ -98,18 +93,14 @@ export function createAgentProfileProducerV1( return Object.freeze({ async prepare(prepared: PreparedAgentProfileV1): Promise { if (active) throw new Error('agent-profile producer is busy'); - const preparedSnapshot = snapshotPreparedProfileV1(prepared); - const projectionQuads = validateAgentProfileProjectionV1(preparedSnapshot); - assertAdvertisedAgentProfileIdentityV1( - preparedSnapshot.rootEntity, - projectionQuads, - options.peerSigner, - options.evmSigner.address, + const validatedInput = validateAgentProfileProductionInputV1( + preparationDependencies, + prepared, ); active = true; const controller = new AbortController(); try { - await options.fence(preparedSnapshot, controller.signal); + await options.fence(validatedInput.preparedSnapshot, controller.signal); } catch (error) { active = false; throw error; @@ -123,8 +114,7 @@ export function createAgentProfileProducerV1( state = 'completing'; try { return await completePrepared( - preparedSnapshot, - projectionQuads, + validatedInput, publication, controller.signal, ); diff --git a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts new file mode 100644 index 0000000000..4905b95db2 --- /dev/null +++ b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts @@ -0,0 +1,19 @@ +import type { PreparedAgentProfileV1 } from '../src/profile.js'; +import { + prepareAgentProfileProductionV1, + type ValidatedAgentProfileProductionInputV1, +} from '../src/system-records/agent-profile-producer-preparation-v1.js'; +import type { + AgentProfileProducerPreparationDependenciesV1, + AgentProfilePublicationBindingV1, +} from '../src/system-records/agent-profile-producer-contract-v1.js'; + +declare const dependencies: AgentProfileProducerPreparationDependenciesV1; +declare const prepared: PreparedAgentProfileV1; +declare const validated: ValidatedAgentProfileProductionInputV1; +declare const publication: AgentProfilePublicationBindingV1; + +void prepareAgentProfileProductionV1(dependencies, validated, publication); + +// @ts-expect-error preparation accepts only the bound validated snapshot/projection input. +void prepareAgentProfileProductionV1(dependencies, prepared, publication); diff --git a/packages/core/src/agent-profile-schema-model-v1.ts b/packages/core/src/agent-profile-schema-model-v1.ts index 59fb38e25d..38107dca8b 100644 --- a/packages/core/src/agent-profile-schema-model-v1.ts +++ b/packages/core/src/agent-profile-schema-model-v1.ts @@ -248,11 +248,6 @@ const SUBJECT_POLICY_BY_KIND = Object.freeze({ const SUBJECT_POLICIES = Object.freeze(Object.values(SUBJECT_POLICY_BY_KIND)); -export const AGENT_PROFILE_SCHEMA_V1 = Object.freeze({ - terms: AGENT_PROFILE_SCHEMA_TERMS_V1, - subjectPolicies: SUBJECT_POLICIES, -}); - const ROOT_PATTERN = /^did:dkg:agent:(0x[0-9a-f]{40})$/; const PREDICATE_POLICY_BY_KIND = new Map( SUBJECT_POLICIES.map((policy) => [ diff --git a/packages/core/src/system-record-objects-v1.ts b/packages/core/src/system-record-objects-v1.ts index c15b4b04c5..afd7a9b872 100644 --- a/packages/core/src/system-record-objects-v1.ts +++ b/packages/core/src/system-record-objects-v1.ts @@ -118,9 +118,6 @@ export type { SystemRecordObjectErrorCodeV1, SystemRecordPeerPublicKeyV1 }; export { AGENT_PROFILE_LINK_PREDICATES_V1, AGENT_PROFILE_SCHEMA_TERMS_V1, - AGENT_PROFILE_SCHEMA_V1, - agentProfilePredicatePolicyV1, - agentProfileSubjectPolicyV1, classifyAgentProfileOwnedSubjectV1, deriveAgentProfileOwnedSubjectV1, isAllowedAgentProfilePredicateV1, @@ -130,9 +127,6 @@ export type { AgentProfileIndexedSubjectKindV1, AgentProfileLinkedSubjectKindV1, AgentProfileOwnedSubjectKindV1, - AgentProfilePredicatePolicyV1, - AgentProfileSubjectPolicyV1, - AgentProfileSubjectShapeV1, } from './agent-profile-schema-model-v1.js'; export interface AgentProfileHeadCommonV1 { diff --git a/packages/core/test/system-record-package-export-v1.mjs b/packages/core/test/system-record-package-export-v1.mjs index 906234453e..8b6c78d139 100644 --- a/packages/core/test/system-record-package-export-v1.mjs +++ b/packages/core/test/system-record-package-export-v1.mjs @@ -2,7 +2,7 @@ const api = await import('@origintrail-official/dkg-core/system-record-v1'); const representativeExports = [ 'SYSTEM_RECORD_KIND_V1', - 'AGENT_PROFILE_SCHEMA_V1', + 'AGENT_PROFILE_SCHEMA_TERMS_V1', 'computeAgentProfileHeadObjectDigestV1', 'parseCanonicalSystemRecordAppliedStateV1', 'computeSystemRecordRootDescriptorDigestV1', diff --git a/packages/core/test/system-record-policy-helpers-v1.test.ts b/packages/core/test/system-record-policy-helpers-v1.test.ts index 7887fedd44..a5d1deb2d8 100644 --- a/packages/core/test/system-record-policy-helpers-v1.test.ts +++ b/packages/core/test/system-record-policy-helpers-v1.test.ts @@ -2,15 +2,17 @@ import { describe, expect, it } from 'vitest'; import { AGENT_PROFILE_LINK_PREDICATES_V1, - AGENT_PROFILE_SCHEMA_V1, - agentProfilePredicatePolicyV1, - agentProfileSubjectPolicyV1, + assertAgentProfileProjectionSchemaV1, deriveAgentProfileOwnedSubjectV1, evaluateAuthorityTransitionConflictV1, isAllowedAgentProfilePredicateV1, type AgentProfileAuthorityTransitionV1, type AgentProfileOwnedSubjectKindV1, } from '../src/system-record-v1.js'; +import { + agentProfilePredicatePolicyV1, + agentProfileSubjectPolicyV1, +} from '../src/agent-profile-schema-model-v1.js'; const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; @@ -176,13 +178,7 @@ describe('system-record V1 public policy helpers', () => { .toEqual(EXPECTED_AGENT_PROFILE_LINK_PREDICATES_V1); }); - it('exposes one immutable descriptor for subject, predicate, term, type, and link policy', () => { - expect(Object.isFrozen(AGENT_PROFILE_SCHEMA_V1)).toBe(true); - expect(Object.isFrozen(AGENT_PROFILE_SCHEMA_V1.terms)).toBe(true); - expect(Object.isFrozen(AGENT_PROFILE_SCHEMA_V1.subjectPolicies)).toBe(true); - expect(AGENT_PROFILE_SCHEMA_V1.subjectPolicies.map(({ kind }) => kind)) - .toEqual(PROFILE_SUBJECT_KINDS_V1); - + it('keeps one immutable internal policy for subject, predicate, term, type, and link rules', () => { for (const kind of PROFILE_SUBJECT_KINDS_V1) { const subjectPolicy = agentProfileSubjectPolicyV1(kind); expect(Object.isFrozen(subjectPolicy), kind).toBe(true); @@ -242,4 +238,34 @@ describe('system-record V1 public policy helpers', () => { expect(() => uncheckedDerive(root, 'offering', 0)).toThrow(/positive/); expect(() => uncheckedDerive(root, 'registration', 1)).toThrow(/ordinal/); }); + + it('rejects literal objects for generic IRI predicates', () => { + const root = `did:dkg:agent:0x${'11'.repeat(20)}`; + const offering = deriveAgentProfileOwnedSubjectV1(root, 'offering', 1); + expect(() => assertAgentProfileProjectionSchemaV1( + root, + [root, offering], + [ + { subject: root, predicate: RDF_TYPE, object: `${DKG}Agent`, graph: '' }, + { + subject: root, + predicate: `${SKILL}offersSkill`, + object: offering, + graph: '', + }, + { + subject: offering, + predicate: RDF_TYPE, + object: `${SKILL}SkillOffering`, + graph: '', + }, + { + subject: offering, + predicate: `${SKILL}skill`, + object: '"ImageAnalysis"', + graph: '', + }, + ], + )).toThrow(/invalid object term kind/); + }); }); From df06d6b911fd4916b5d2c24eda28c572ebd4d4ba Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sat, 8 Aug 2026 23:12:41 +0200 Subject: [PATCH 05/12] refactor(agent): harden profile phase boundaries --- .../agent-profile-producer-commit-v1.ts | 4 +- .../agent-profile-producer-contract-v1.ts | 26 ------------ .../agent-profile-producer-inventory-v1.ts | 4 +- ...ent-profile-producer-phase-contracts-v1.ts | 40 +++++++++++++++++++ .../agent-profile-producer-preparation-v1.ts | 9 ++++- .../agent-profile-producer-signing-v1.ts | 2 +- .../agent-profile-producer-v1.ts | 10 +++-- ...ile-producer-phase-boundaries.typecheck.ts | 18 ++++++++- .../system-record-policy-helpers-v1.test.ts | 27 +++++++++++++ 9 files changed, 105 insertions(+), 35 deletions(-) create mode 100644 packages/agent/src/system-records/agent-profile-producer-phase-contracts-v1.ts diff --git a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts index 85b45a82d5..83c89ffb61 100644 --- a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts @@ -1,7 +1,9 @@ import type { - AgentProfileProducerCommitDependenciesV1, AgentProfileProducerPublicationV1, } from './agent-profile-producer-contract-v1.js'; +import type { + AgentProfileProducerCommitDependenciesV1, +} from './agent-profile-producer-phase-contracts-v1.js'; import type { AgentProfileProductionInventoryV1 } from './agent-profile-producer-inventory-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; diff --git a/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts b/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts index 5a9b1d2b52..ebafc6a814 100644 --- a/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts @@ -128,32 +128,6 @@ export interface CreateAgentProfileProducerOptionsV1 { readonly install: (input: AgentProfileProducerInstallInputV1) => void | Promise; } -export interface AgentProfileProducerPreparationDependenciesV1 { - readonly networkId: NetworkIdV1; - readonly publicationDeployment: Readonly; - readonly peerId: string; - readonly peerPublicKey: SystemRecordPeerPublicKeyV1; - readonly evmIssuer: string; - readonly nowMs?: () => number; - readonly snapshot: AgentProfileProducerPublicationStoreV1['snapshot']; -} - -export interface AgentProfileProducerSigningDependenciesV1 { - readonly peerSigner: SystemRecordPeerSignerV1; - readonly evmSigner: EvmPersonalMessageSignerV1; -} - -export interface AgentProfileProducerInventoryDependenciesV1 { - readonly networkId: NetworkIdV1; - readonly peerSigner: SystemRecordPeerSignerV1; - readonly resolveArtifact: AgentProfileProducerPublicationStoreV1['resolveArtifact']; -} - -export interface AgentProfileProducerCommitDependenciesV1 { - readonly prepareCommit: AgentProfileProducerPublicationStoreV1['prepareCommit']; - readonly install: CreateAgentProfileProducerOptionsV1['install']; -} - export interface AgentProfileProducerLeaseV1 { complete( publication: AgentProfilePublicationBindingV1, diff --git a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts index 7a95bf19cf..bbf4fb6caf 100644 --- a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts @@ -21,9 +21,11 @@ import { import { flattenAgentProfileProducerPublicationArtifactsV1, type AgentProfileProducerArtifactV1, - type AgentProfileProducerInventoryDependenciesV1, type AgentProfileProducerPublicationArtifactsV1, } from './agent-profile-producer-contract-v1.js'; +import type { + AgentProfileProducerInventoryDependenciesV1, +} from './agent-profile-producer-phase-contracts-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; import { systemRecordArtifactKeyV1 } from './artifact-v1.js'; diff --git a/packages/agent/src/system-records/agent-profile-producer-phase-contracts-v1.ts b/packages/agent/src/system-records/agent-profile-producer-phase-contracts-v1.ts new file mode 100644 index 0000000000..8e4a56dd1d --- /dev/null +++ b/packages/agent/src/system-records/agent-profile-producer-phase-contracts-v1.ts @@ -0,0 +1,40 @@ +import type { + CatalogSealDeploymentProfileV1, +} from '@origintrail-official/dkg-core'; +import type { + NetworkIdV1, + SystemRecordPeerPublicKeyV1, +} from '@origintrail-official/dkg-core/system-record-v1'; + +import type { EvmPersonalMessageSignerV1 } from '../evm-message-signer-v1.js'; +import type { + AgentProfileProducerPublicationStoreV1, + CreateAgentProfileProducerOptionsV1, + SystemRecordPeerSignerV1, +} from './agent-profile-producer-contract-v1.js'; + +export interface AgentProfileProducerPreparationDependenciesV1 { + readonly networkId: NetworkIdV1; + readonly publicationDeployment: Readonly; + readonly peerId: string; + readonly peerPublicKey: SystemRecordPeerPublicKeyV1; + readonly evmIssuer: string; + readonly nowMs?: () => number; + readonly snapshot: AgentProfileProducerPublicationStoreV1['snapshot']; +} + +export interface AgentProfileProducerSigningDependenciesV1 { + readonly peerSigner: SystemRecordPeerSignerV1; + readonly evmSigner: EvmPersonalMessageSignerV1; +} + +export interface AgentProfileProducerInventoryDependenciesV1 { + readonly networkId: NetworkIdV1; + readonly peerSigner: SystemRecordPeerSignerV1; + readonly resolveArtifact: AgentProfileProducerPublicationStoreV1['resolveArtifact']; +} + +export interface AgentProfileProducerCommitDependenciesV1 { + readonly prepareCommit: AgentProfileProducerPublicationStoreV1['prepareCommit']; + readonly install: CreateAgentProfileProducerOptionsV1['install']; +} diff --git a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts index ecd867d819..2e581ac559 100644 --- a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts @@ -36,14 +36,20 @@ import type { Quad } from '@origintrail-official/dkg-storage'; import type { PreparedAgentProfileV1 } from '../profile.js'; import { assertRecoverableGraphScopedAuthorAttestationV1 } from '../rfc64/recoverable-author-attestation-v1.js'; import type { - AgentProfileProducerPreparationDependenciesV1, AgentProfilePublicationBindingV1, SystemRecordPeerSignerV1, } from './agent-profile-producer-contract-v1.js'; +import type { + AgentProfileProducerPreparationDependenciesV1, +} from './agent-profile-producer-phase-contracts-v1.js'; const UTF8 = new TextEncoder(); +const VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1: unique symbol = Symbol( + 'validated-agent-profile-production-input-v1', +); export interface ValidatedAgentProfileProductionInputV1 { + readonly [VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1]: true; readonly preparedSnapshot: PreparedAgentProfileV1; readonly projectionQuads: readonly Readonly[]; readonly ownedSubjectTable: OwnedSubjectTableObjectV1; @@ -231,6 +237,7 @@ export function validateAgentProfileProductionInputV1( dependencies.evmIssuer, ); return Object.freeze({ + [VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1]: true as const, preparedSnapshot, projectionQuads: Object.freeze(projected), ownedSubjectTable, diff --git a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts index e1b397916f..aef2014619 100644 --- a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts @@ -7,7 +7,7 @@ import { import type { AgentProfileProducerSigningDependenciesV1, -} from './agent-profile-producer-contract-v1.js'; +} from './agent-profile-producer-phase-contracts-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; export interface SignedAgentProfileProductionV1 { diff --git a/packages/agent/src/system-records/agent-profile-producer-v1.ts b/packages/agent/src/system-records/agent-profile-producer-v1.ts index e7ffd09606..690afbb895 100644 --- a/packages/agent/src/system-records/agent-profile-producer-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-v1.ts @@ -3,16 +3,18 @@ import type { PreparedAgentProfileV1 } from '../profile.js'; import { commitAgentProfileProductionV1 } from './agent-profile-producer-commit-v1.js'; import type { - AgentProfileProducerCommitDependenciesV1, - AgentProfileProducerInventoryDependenciesV1, AgentProfileProducerLeaseV1, - AgentProfileProducerPreparationDependenciesV1, AgentProfileProducerPublicationV1, - AgentProfileProducerSigningDependenciesV1, AgentProfileProducerV1, AgentProfilePublicationBindingV1, CreateAgentProfileProducerOptionsV1, } from './agent-profile-producer-contract-v1.js'; +import type { + AgentProfileProducerCommitDependenciesV1, + AgentProfileProducerInventoryDependenciesV1, + AgentProfileProducerPreparationDependenciesV1, + AgentProfileProducerSigningDependenciesV1, +} from './agent-profile-producer-phase-contracts-v1.js'; import { prepareAgentProfileProductionInventoryV1 } from './agent-profile-producer-inventory-v1.js'; import { prepareAgentProfileProductionV1, diff --git a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts index 4905b95db2..ce2c1c4ac0 100644 --- a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts +++ b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts @@ -4,9 +4,14 @@ import { type ValidatedAgentProfileProductionInputV1, } from '../src/system-records/agent-profile-producer-preparation-v1.js'; import type { - AgentProfileProducerPreparationDependenciesV1, AgentProfilePublicationBindingV1, } from '../src/system-records/agent-profile-producer-contract-v1.js'; +import type { + AgentProfileProducerPreparationDependenciesV1, +} from '../src/system-records/agent-profile-producer-phase-contracts-v1.js'; + +// @ts-expect-error phase dependency DTOs are not exported by the producer entrypoint. +import type { AgentProfileProducerPreparationDependenciesV1 as LeakedPreparationDeps } from '../src/system-records/agent-profile-producer-v1.js'; declare const dependencies: AgentProfileProducerPreparationDependenciesV1; declare const prepared: PreparedAgentProfileV1; @@ -15,5 +20,16 @@ declare const publication: AgentProfilePublicationBindingV1; void prepareAgentProfileProductionV1(dependencies, validated, publication); +// @ts-expect-error callers cannot fabricate the module-private validated-input brand. +const fabricated: ValidatedAgentProfileProductionInputV1 = { + preparedSnapshot: prepared, + projectionQuads: prepared.projectionQuads, + ownedSubjectTable: [], +}; +void fabricated; + // @ts-expect-error preparation accepts only the bound validated snapshot/projection input. void prepareAgentProfileProductionV1(dependencies, prepared, publication); + +declare const leaked: LeakedPreparationDeps; +void leaked; diff --git a/packages/core/test/system-record-policy-helpers-v1.test.ts b/packages/core/test/system-record-policy-helpers-v1.test.ts index a5d1deb2d8..4aa2fa0965 100644 --- a/packages/core/test/system-record-policy-helpers-v1.test.ts +++ b/packages/core/test/system-record-policy-helpers-v1.test.ts @@ -268,4 +268,31 @@ describe('system-record V1 public policy helpers', () => { ], )).toThrow(/invalid object term kind/); }); + + it('enforces workspace-key and revocation-root object policies in the validator', () => { + const root = `did:dkg:agent:0x${'11'.repeat(20)}`; + expect(() => assertAgentProfileProjectionSchemaV1( + root, + [root], + [{ + subject: root, + predicate: `${DKG}publicEncryptionKey`, + object: '"not-a-canonical-workspace-key"', + graph: '', + }], + )).toThrow(/public encryption key is not canonical/); + + const x25519 = `${root}#x25519-${'a'.repeat(32)}`; + const wrongRoot = `did:dkg:agent:0x${'22'.repeat(20)}`; + expect(() => assertAgentProfileProjectionSchemaV1( + root, + [root, x25519], + [{ + subject: x25519, + predicate: `${DKG}revokedBy`, + object: wrongRoot, + graph: '', + }], + )).toThrow(/revocation does not bind the profile root/); + }); }); From 86e908e5ce4bc111a14d4b28c8021e4446fa1263 Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sat, 8 Aug 2026 23:23:23 +0200 Subject: [PATCH 06/12] refactor(agent): colocate profile phase capabilities --- .../agent-profile-producer-commit-v1.ts | 14 ++++--- .../agent-profile-producer-inventory-v1.ts | 14 +++++-- ...ent-profile-producer-phase-contracts-v1.ts | 40 ------------------- .../agent-profile-producer-preparation-v1.ts | 21 +++++++--- .../agent-profile-producer-signing-v1.ts | 10 +++-- .../agent-profile-producer-v1.ts | 38 +++++++++--------- ...ile-producer-phase-boundaries.typecheck.ts | 5 +-- .../system-record-policy-helpers-v1.test.ts | 7 ++++ 8 files changed, 67 insertions(+), 82 deletions(-) delete mode 100644 packages/agent/src/system-records/agent-profile-producer-phase-contracts-v1.ts diff --git a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts index 83c89ffb61..54769a2f85 100644 --- a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts @@ -1,13 +1,17 @@ import type { + AgentProfileProducerPublicationStoreV1, AgentProfileProducerPublicationV1, + CreateAgentProfileProducerOptionsV1, } from './agent-profile-producer-contract-v1.js'; -import type { - AgentProfileProducerCommitDependenciesV1, -} from './agent-profile-producer-phase-contracts-v1.js'; import type { AgentProfileProductionInventoryV1 } from './agent-profile-producer-inventory-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; +export interface AgentProfileProducerCommitDependenciesV1 { + readonly store: Pick; + readonly producer: Pick; +} + export async function commitAgentProfileProductionV1( dependencies: AgentProfileProducerCommitDependenciesV1, preparation: AgentProfileProductionPreparationV1, @@ -15,7 +19,7 @@ export async function commitAgentProfileProductionV1( inventoryPlan: AgentProfileProductionInventoryV1, signal: AbortSignal, ): Promise { - const commitLease = await dependencies.prepareCommit({ + const commitLease = await dependencies.store.prepareCommit({ expectedHeadDigest: preparation.snapshot.currentHead?.objectDigest ?? null, expectedRootDescriptorDigest: preparation.snapshot.inventory?.descriptorDigest ?? null, publicationArtifacts: inventoryPlan.publicationArtifacts, @@ -25,7 +29,7 @@ export async function commitAgentProfileProductionV1( let committed = false; try { signal.throwIfAborted(); - await dependencies.install({ + await dependencies.producer.install({ head: preparation.head, envelope: signed.envelope, canonicalProjectionBytes: preparation.projectionBytes, diff --git a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts index bbf4fb6caf..ed026a790a 100644 --- a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts @@ -12,6 +12,7 @@ import { type AgentProfileHeadObjectV1, type AgentProfileVerifiedAuthoritySummaryV1, type Digest32V1, + type NetworkIdV1, type SignedSystemRecordRootDescriptorEnvelopeV1, type SystemRecordInventoryRowV1, type SystemRecordInventoryTreeSnapshotV1, @@ -21,15 +22,20 @@ import { import { flattenAgentProfileProducerPublicationArtifactsV1, type AgentProfileProducerArtifactV1, + type AgentProfileProducerPublicationStoreV1, type AgentProfileProducerPublicationArtifactsV1, + type SystemRecordPeerSignerV1, } from './agent-profile-producer-contract-v1.js'; -import type { - AgentProfileProducerInventoryDependenciesV1, -} from './agent-profile-producer-phase-contracts-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; import { systemRecordArtifactKeyV1 } from './artifact-v1.js'; +export interface AgentProfileProducerInventoryDependenciesV1 { + readonly networkId: NetworkIdV1; + readonly peerSigner: SystemRecordPeerSignerV1; + readonly store: Pick; +} + export interface AgentProfileProductionInventoryV1 { readonly inventory: SystemRecordInventoryTreeSnapshotV1; readonly rootEnvelope: SignedSystemRecordRootDescriptorEnvelopeV1; @@ -114,7 +120,7 @@ export async function prepareAgentProfileProductionInventoryV1( objectDigest: digest, } as const; const artifact = artifactsByKey.get(systemRecordArtifactKeyV1(reference)) - ?? await dependencies.resolveArtifact(reference); + ?? await dependencies.store.resolveArtifact(reference); return artifact === undefined || artifact === null ? undefined diff --git a/packages/agent/src/system-records/agent-profile-producer-phase-contracts-v1.ts b/packages/agent/src/system-records/agent-profile-producer-phase-contracts-v1.ts deleted file mode 100644 index 8e4a56dd1d..0000000000 --- a/packages/agent/src/system-records/agent-profile-producer-phase-contracts-v1.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { - CatalogSealDeploymentProfileV1, -} from '@origintrail-official/dkg-core'; -import type { - NetworkIdV1, - SystemRecordPeerPublicKeyV1, -} from '@origintrail-official/dkg-core/system-record-v1'; - -import type { EvmPersonalMessageSignerV1 } from '../evm-message-signer-v1.js'; -import type { - AgentProfileProducerPublicationStoreV1, - CreateAgentProfileProducerOptionsV1, - SystemRecordPeerSignerV1, -} from './agent-profile-producer-contract-v1.js'; - -export interface AgentProfileProducerPreparationDependenciesV1 { - readonly networkId: NetworkIdV1; - readonly publicationDeployment: Readonly; - readonly peerId: string; - readonly peerPublicKey: SystemRecordPeerPublicKeyV1; - readonly evmIssuer: string; - readonly nowMs?: () => number; - readonly snapshot: AgentProfileProducerPublicationStoreV1['snapshot']; -} - -export interface AgentProfileProducerSigningDependenciesV1 { - readonly peerSigner: SystemRecordPeerSignerV1; - readonly evmSigner: EvmPersonalMessageSignerV1; -} - -export interface AgentProfileProducerInventoryDependenciesV1 { - readonly networkId: NetworkIdV1; - readonly peerSigner: SystemRecordPeerSignerV1; - readonly resolveArtifact: AgentProfileProducerPublicationStoreV1['resolveArtifact']; -} - -export interface AgentProfileProducerCommitDependenciesV1 { - readonly prepareCommit: AgentProfileProducerPublicationStoreV1['prepareCommit']; - readonly install: CreateAgentProfileProducerOptionsV1['install']; -} diff --git a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts index 2e581ac559..51f80a5ac2 100644 --- a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts @@ -36,18 +36,27 @@ import type { Quad } from '@origintrail-official/dkg-storage'; import type { PreparedAgentProfileV1 } from '../profile.js'; import { assertRecoverableGraphScopedAuthorAttestationV1 } from '../rfc64/recoverable-author-attestation-v1.js'; import type { + AgentProfileProducerPublicationStoreV1, AgentProfilePublicationBindingV1, + CreateAgentProfileProducerOptionsV1, SystemRecordPeerSignerV1, } from './agent-profile-producer-contract-v1.js'; -import type { - AgentProfileProducerPreparationDependenciesV1, -} from './agent-profile-producer-phase-contracts-v1.js'; const UTF8 = new TextEncoder(); const VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1: unique symbol = Symbol( 'validated-agent-profile-production-input-v1', ); +export interface AgentProfileProducerPreparationDependenciesV1 { + readonly networkId: NetworkIdV1; + readonly publicationDeployment: Readonly; + readonly peerId: string; + readonly peerPublicKey: SystemRecordPeerSignerV1['publicKey']; + readonly evmIssuer: string; + readonly clock: Pick; + readonly store: Pick; +} + export interface ValidatedAgentProfileProductionInputV1 { readonly [VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1]: true; readonly preparedSnapshot: PreparedAgentProfileV1; @@ -56,7 +65,7 @@ export interface ValidatedAgentProfileProductionInputV1 { } export interface AgentProfileProductionPreparationV1 { - readonly snapshot: ReturnType; + readonly snapshot: ReturnType; readonly verifierNowMs: number; readonly projectionQuads: readonly Readonly[]; readonly projectionBytes: Uint8Array; @@ -83,7 +92,7 @@ export async function prepareAgentProfileProductionV1( 'assertionFinalizedAt', ); const assertionFinalizedAtMs = Date.parse(publication.seal.assertionFinalizedAt); - const verifierNowMs = producerNowMs(dependencies.nowMs?.() ?? Date.now()); + const verifierNowMs = producerNowMs(dependencies.clock.nowMs?.() ?? Date.now()); if (Date.parse(issuedAt) > verifierNowMs + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS) { throw new Error('agent-profile issuedAt exceeds the future clock-skew bound'); } @@ -98,7 +107,7 @@ export async function prepareAgentProfileProductionV1( } const evmIssuer = dependencies.evmIssuer; assertCanonicalEvmAddress(evmIssuer, 'profile EVM issuer'); - const snapshot = dependencies.snapshot(); + const snapshot = dependencies.store.snapshot(); const previous = snapshot.currentHead; if (previous !== null && (previous.object.networkId !== dependencies.networkId diff --git a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts index aef2014619..617cdadcea 100644 --- a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts @@ -5,11 +5,15 @@ import { type SignedAgentProfileHeadEnvelopeV1, } from '@origintrail-official/dkg-core/system-record-v1'; -import type { - AgentProfileProducerSigningDependenciesV1, -} from './agent-profile-producer-phase-contracts-v1.js'; +import type { EvmPersonalMessageSignerV1 } from '../evm-message-signer-v1.js'; +import type { SystemRecordPeerSignerV1 } from './agent-profile-producer-contract-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; +export interface AgentProfileProducerSigningDependenciesV1 { + readonly peerSigner: SystemRecordPeerSignerV1; + readonly evmSigner: EvmPersonalMessageSignerV1; +} + export interface SignedAgentProfileProductionV1 { readonly envelope: SignedAgentProfileHeadEnvelopeV1; readonly envelopeBytes: Uint8Array; diff --git a/packages/agent/src/system-records/agent-profile-producer-v1.ts b/packages/agent/src/system-records/agent-profile-producer-v1.ts index 690afbb895..863936d75a 100644 --- a/packages/agent/src/system-records/agent-profile-producer-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-v1.ts @@ -1,7 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 import type { PreparedAgentProfileV1 } from '../profile.js'; -import { commitAgentProfileProductionV1 } from './agent-profile-producer-commit-v1.js'; +import { + commitAgentProfileProductionV1, + type AgentProfileProducerCommitDependenciesV1, +} from './agent-profile-producer-commit-v1.js'; import type { AgentProfileProducerLeaseV1, AgentProfileProducerPublicationV1, @@ -9,19 +12,20 @@ import type { AgentProfilePublicationBindingV1, CreateAgentProfileProducerOptionsV1, } from './agent-profile-producer-contract-v1.js'; -import type { - AgentProfileProducerCommitDependenciesV1, - AgentProfileProducerInventoryDependenciesV1, - AgentProfileProducerPreparationDependenciesV1, - AgentProfileProducerSigningDependenciesV1, -} from './agent-profile-producer-phase-contracts-v1.js'; -import { prepareAgentProfileProductionInventoryV1 } from './agent-profile-producer-inventory-v1.js'; +import { + prepareAgentProfileProductionInventoryV1, + type AgentProfileProducerInventoryDependenciesV1, +} from './agent-profile-producer-inventory-v1.js'; import { prepareAgentProfileProductionV1, validateAgentProfileProductionInputV1, + type AgentProfileProducerPreparationDependenciesV1, type ValidatedAgentProfileProductionInputV1, } from './agent-profile-producer-preparation-v1.js'; -import { signAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; +import { + signAgentProfileProductionV1, + type AgentProfileProducerSigningDependenciesV1, +} from './agent-profile-producer-signing-v1.js'; export * from './agent-profile-producer-contract-v1.js'; @@ -38,27 +42,21 @@ export function createAgentProfileProducerV1( peerId: options.peerSigner.peerId, peerPublicKey: options.peerSigner.publicKey, evmIssuer: options.evmSigner.address, - ...(options.nowMs === undefined ? {} : { nowMs: () => options.nowMs?.() ?? Date.now() }), - snapshot: () => options.store.snapshot(), + clock: options, + store: options.store, }); const signingDependencies: AgentProfileProducerSigningDependenciesV1 = Object.freeze({ peerSigner: options.peerSigner, evmSigner: options.evmSigner, }); - const resolveArtifact: AgentProfileProducerInventoryDependenciesV1['resolveArtifact'] = - (reference) => options.store.resolveArtifact(reference); const inventoryDependencies: AgentProfileProducerInventoryDependenciesV1 = Object.freeze({ networkId: options.networkId, peerSigner: options.peerSigner, - resolveArtifact, + store: options.store, }); - const prepareCommit: AgentProfileProducerCommitDependenciesV1['prepareCommit'] = - (input) => options.store.prepareCommit(input); - const install: AgentProfileProducerCommitDependenciesV1['install'] = - (input) => options.install(input); const commitDependencies: AgentProfileProducerCommitDependenciesV1 = Object.freeze({ - prepareCommit, - install, + store: options.store, + producer: options, }); let active = false; const completePrepared = async ( diff --git a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts index ce2c1c4ac0..8a24bafe83 100644 --- a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts +++ b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts @@ -1,15 +1,12 @@ import type { PreparedAgentProfileV1 } from '../src/profile.js'; import { + type AgentProfileProducerPreparationDependenciesV1, prepareAgentProfileProductionV1, type ValidatedAgentProfileProductionInputV1, } from '../src/system-records/agent-profile-producer-preparation-v1.js'; import type { AgentProfilePublicationBindingV1, } from '../src/system-records/agent-profile-producer-contract-v1.js'; -import type { - AgentProfileProducerPreparationDependenciesV1, -} from '../src/system-records/agent-profile-producer-phase-contracts-v1.js'; - // @ts-expect-error phase dependency DTOs are not exported by the producer entrypoint. import type { AgentProfileProducerPreparationDependenciesV1 as LeakedPreparationDeps } from '../src/system-records/agent-profile-producer-v1.js'; diff --git a/packages/core/test/system-record-policy-helpers-v1.test.ts b/packages/core/test/system-record-policy-helpers-v1.test.ts index 4aa2fa0965..b94e4cad3b 100644 --- a/packages/core/test/system-record-policy-helpers-v1.test.ts +++ b/packages/core/test/system-record-policy-helpers-v1.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'; import { AGENT_PROFILE_LINK_PREDICATES_V1, assertAgentProfileProjectionSchemaV1, + classifyAgentProfileOwnedSubjectV1, deriveAgentProfileOwnedSubjectV1, evaluateAuthorityTransitionConflictV1, isAllowedAgentProfilePredicateV1, @@ -228,8 +229,14 @@ describe('system-record V1 public policy helpers', () => { const root = `did:dkg:agent:0x${'11'.repeat(20)}`; expect(deriveAgentProfileOwnedSubjectV1(root, 'capability', 2)) .toBe(`${root}/.well-known/genid/cap2`); + expect(deriveAgentProfileOwnedSubjectV1(root, 'offering', 3)) + .toBe(`${root}/.well-known/genid/offering3`); + expect(deriveAgentProfileOwnedSubjectV1(root, 'registration')) + .toBe(`${root}/.well-known/genid/registration`); expect(deriveAgentProfileOwnedSubjectV1(root, 'hosting')) .toBe(`${root}/.well-known/genid/hosting`); + const x25519 = `${root}#x25519-${'a'.repeat(32)}`; + expect(classifyAgentProfileOwnedSubjectV1(root, x25519)).toBe('x25519'); const uncheckedDerive = deriveAgentProfileOwnedSubjectV1 as unknown as ( rootSubject: string, kind: 'capability' | 'offering' | 'registration' | 'hosting', From 6befa2558137c89b9d5c222a0924302ba63fa083 Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sat, 8 Aug 2026 23:33:45 +0200 Subject: [PATCH 07/12] fix(agent): enforce profile phase runtime boundary --- packages/agent/package.json | 4 ++++ packages/agent/scripts/test-package-root.mjs | 17 +++++++++++++++ .../agent-profile-producer-preparation-v1.ts | 17 +++++++++++++++ ...ile-producer-phase-boundaries.typecheck.ts | 14 +++++++++++++ ...ent-profile-producer-validation-v1.test.ts | 21 +++++++++++++++++++ 5 files changed, 73 insertions(+) diff --git a/packages/agent/package.json b/packages/agent/package.json index b787023002..da62364592 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -30,6 +30,10 @@ "./dist/rfc64/inventory-v1/statements.js": "./dist/rfc64/inventory-v1/statements.js", "./dist/rfc64/public-catalog-activation-config-v1.js": "./dist/rfc64/public-catalog-activation-config-v1.js", "./dist/rfc64/swm-author-inventory-producer-v1.js": "./dist/rfc64/swm-author-inventory-producer-v1.js", + "./dist/system-records/agent-profile-producer-preparation-v1.js": null, + "./dist/system-records/agent-profile-producer-signing-v1.js": null, + "./dist/system-records/agent-profile-producer-inventory-v1.js": null, + "./dist/system-records/agent-profile-producer-commit-v1.js": null, "./dist/rfc64/control-envelope-signer-v1.js": null, "./dist/rfc64/swm-inventory-shadow-runtime-v1.js": null, "./dist/rfc64/control-object-store-v1-internal.js": null, diff --git a/packages/agent/scripts/test-package-root.mjs b/packages/agent/scripts/test-package-root.mjs index 09aaafeb94..994c5bbf1d 100644 --- a/packages/agent/scripts/test-package-root.mjs +++ b/packages/agent/scripts/test-package-root.mjs @@ -16,6 +16,23 @@ const expectedRfc64PolicyCells = [ 'private-open', 'private-curated', ]; +const internalAgentProfilePhaseSpecifiers = [ + '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-preparation-v1.js', + '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-signing-v1.js', + '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-inventory-v1.js', + '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-commit-v1.js', +]; + +for (const specifier of internalAgentProfilePhaseSpecifiers) { + let resolved = false; + try { + await import(specifier); + resolved = true; + } catch (error) { + if (error?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error; + } + if (resolved) throw new Error(`published package exposed internal phase ${specifier}`); +} if ( typeof root.DKGAgent !== 'function' diff --git a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts index 51f80a5ac2..bcd4372e12 100644 --- a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts @@ -83,6 +83,7 @@ export async function prepareAgentProfileProductionV1( input: ValidatedAgentProfileProductionInputV1, inputPublication: AgentProfilePublicationBindingV1, ): Promise { + assertValidatedAgentProfileProductionInputV1(input); const { preparedSnapshot: prepared, projectionQuads, ownedSubjectTable } = input; const publication = snapshotConfirmedPublicationBindingV1(inputPublication); const issuedAt = normalizePublicationTimestampV1(publication.issuedAt, 'issuedAt'); @@ -216,6 +217,22 @@ export async function prepareAgentProfileProductionV1( }); } +function assertValidatedAgentProfileProductionInputV1( + input: unknown, +): asserts input is ValidatedAgentProfileProductionInputV1 { + if (typeof input !== 'object' + || input === null + || !Object.prototype.hasOwnProperty.call( + input, + VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1, + ) + || (input as Record)[ + VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1 + ] !== true) { + throw new TypeError('agent-profile preparation requires a validated production input'); + } +} + export function validateAgentProfileProductionInputV1( dependencies: Pick< AgentProfileProducerPreparationDependenciesV1, diff --git a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts index 8a24bafe83..a53c97e7cf 100644 --- a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts +++ b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts @@ -30,3 +30,17 @@ void prepareAgentProfileProductionV1(dependencies, prepared, publication); declare const leaked: LeakedPreparationDeps; void leaked; + +// @ts-expect-error package exports block the preparation implementation phase. +type PublishedPreparationPhase = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-preparation-v1.js'); +// @ts-expect-error package exports block the signing implementation phase. +type PublishedSigningPhase = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-signing-v1.js'); +// @ts-expect-error package exports block the inventory implementation phase. +type PublishedInventoryPhase = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-inventory-v1.js'); +// @ts-expect-error package exports block the commit implementation phase. +type PublishedCommitPhase = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-commit-v1.js'); + +void (undefined as PublishedPreparationPhase | undefined); +void (undefined as PublishedSigningPhase | undefined); +void (undefined as PublishedInventoryPhase | undefined); +void (undefined as PublishedCommitPhase | undefined); diff --git a/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts b/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts index e17dde0cda..3a25df3261 100644 --- a/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts +++ b/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts @@ -11,6 +11,7 @@ import { type AgentProfileProducerPublicationStoreV1, type AgentProfilePublicationBindingV1, } from '../src/system-records/agent-profile-producer-v1.js'; +import { prepareAgentProfileProductionV1 } from '../src/system-records/agent-profile-producer-preparation-v1.js'; import { createInMemoryAgentProfilePublicationStoreV1 } from '../src/system-records/in-memory-agent-profile-publication-store-v1.js'; import { DEPLOYMENT, @@ -25,6 +26,26 @@ import { describe('agent-profile system-record producer V1 validation and binding', () => { + it('rejects a fabricated preparation input before reading dependencies or publication', async () => { + let reads = 0; + const unread = new Proxy({}, { + get() { + reads += 1; + throw new Error('fabricated input escaped the runtime boundary'); + }, + }); + await expect(prepareAgentProfileProductionV1( + unread as never, + { + preparedSnapshot: null, + projectionQuads: [], + ownedSubjectTable: [], + } as never, + unread as never, + )).rejects.toThrow(/requires a validated production input/); + expect(reads).toBe(0); + }); + it('preflights provider capacity before materialization and releases a failed commit lease', async () => { const fixture = await producerFixture(createInMemoryAgentProfilePublicationStoreV1({ maxObjects: 1, From cfb8a0473e5031329fe054a54b9a5633e9274579 Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sat, 8 Aug 2026 23:46:10 +0200 Subject: [PATCH 08/12] refactor(agent): clarify producer public API boundary --- ...v1.ts => agent-profile-producer-api-v1.ts} | 12 +----------- .../agent-profile-producer-commit-v1.ts | 2 +- .../agent-profile-producer-inventory-v1.ts | 19 ++++++++++++++++--- .../agent-profile-producer-preparation-v1.ts | 2 +- .../agent-profile-producer-signing-v1.ts | 2 +- .../agent-profile-producer-v1.ts | 4 ++-- ...mory-agent-profile-publication-store-v1.ts | 10 ++++++---- ...ile-producer-phase-boundaries.typecheck.ts | 5 ++++- 8 files changed, 32 insertions(+), 24 deletions(-) rename packages/agent/src/system-records/{agent-profile-producer-contract-v1.ts => agent-profile-producer-api-v1.ts} (94%) diff --git a/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts b/packages/agent/src/system-records/agent-profile-producer-api-v1.ts similarity index 94% rename from packages/agent/src/system-records/agent-profile-producer-contract-v1.ts rename to packages/agent/src/system-records/agent-profile-producer-api-v1.ts index ebafc6a814..cd760b5c27 100644 --- a/packages/agent/src/system-records/agent-profile-producer-contract-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-api-v1.ts @@ -21,6 +21,7 @@ import type { EvmPersonalMessageSignerV1 } from '../evm-message-signer-v1.js'; import type { PreparedAgentProfileV1 } from '../profile.js'; import type { SystemRecordArtifactV1 } from './artifact-v1.js'; +/** Stable producer boundary; implementation-phase capabilities stay in their owning modules. */ export interface SystemRecordPeerSignerV1 { readonly peerId: string; readonly publicKey: SystemRecordPeerPublicKeyV1; @@ -80,17 +81,6 @@ export interface AgentProfileProducerPublicationArtifactsV1 { >[]; } -export function flattenAgentProfileProducerPublicationArtifactsV1( - artifacts: AgentProfileProducerPublicationArtifactsV1, -): readonly SystemRecordArtifactV1[] { - return Object.freeze([ - artifacts.head, - artifacts.bundle, - artifacts.ownedSubjectTable, - ...artifacts.inventoryObjects, - ]); -} - export interface AgentProfileProducerPublicationCommitLeaseV1 { commit(): void | Promise; abort(): void; diff --git a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts index 54769a2f85..f46729a267 100644 --- a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts @@ -2,7 +2,7 @@ import type { AgentProfileProducerPublicationStoreV1, AgentProfileProducerPublicationV1, CreateAgentProfileProducerOptionsV1, -} from './agent-profile-producer-contract-v1.js'; +} from './agent-profile-producer-api-v1.js'; import type { AgentProfileProductionInventoryV1 } from './agent-profile-producer-inventory-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; diff --git a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts index ed026a790a..464a67ab46 100644 --- a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts @@ -20,15 +20,17 @@ import { } from '@origintrail-official/dkg-core/system-record-v1'; import { - flattenAgentProfileProducerPublicationArtifactsV1, type AgentProfileProducerArtifactV1, type AgentProfileProducerPublicationStoreV1, type AgentProfileProducerPublicationArtifactsV1, type SystemRecordPeerSignerV1, -} from './agent-profile-producer-contract-v1.js'; +} from './agent-profile-producer-api-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; -import { systemRecordArtifactKeyV1 } from './artifact-v1.js'; +import { + systemRecordArtifactKeyV1, + type SystemRecordArtifactV1, +} from './artifact-v1.js'; export interface AgentProfileProducerInventoryDependenciesV1 { readonly networkId: NetworkIdV1; @@ -155,6 +157,17 @@ interface PublicationArtifactSetInputV1 { readonly inventoryUpdate: ReturnType | null; } +function flattenAgentProfileProducerPublicationArtifactsV1( + artifacts: AgentProfileProducerPublicationArtifactsV1, +): readonly SystemRecordArtifactV1[] { + return Object.freeze([ + artifacts.head, + artifacts.bundle, + artifacts.ownedSubjectTable, + ...artifacts.inventoryObjects, + ]); +} + function publicationArtifactSet( input: PublicationArtifactSetInputV1, ): AgentProfileProducerPublicationArtifactsV1 { diff --git a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts index bcd4372e12..7628f384b8 100644 --- a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts @@ -40,7 +40,7 @@ import type { AgentProfilePublicationBindingV1, CreateAgentProfileProducerOptionsV1, SystemRecordPeerSignerV1, -} from './agent-profile-producer-contract-v1.js'; +} from './agent-profile-producer-api-v1.js'; const UTF8 = new TextEncoder(); const VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1: unique symbol = Symbol( diff --git a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts index 617cdadcea..1d767d7346 100644 --- a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts @@ -6,7 +6,7 @@ import { } from '@origintrail-official/dkg-core/system-record-v1'; import type { EvmPersonalMessageSignerV1 } from '../evm-message-signer-v1.js'; -import type { SystemRecordPeerSignerV1 } from './agent-profile-producer-contract-v1.js'; +import type { SystemRecordPeerSignerV1 } from './agent-profile-producer-api-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; export interface AgentProfileProducerSigningDependenciesV1 { diff --git a/packages/agent/src/system-records/agent-profile-producer-v1.ts b/packages/agent/src/system-records/agent-profile-producer-v1.ts index 863936d75a..7c4ec8e8e8 100644 --- a/packages/agent/src/system-records/agent-profile-producer-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-v1.ts @@ -11,7 +11,7 @@ import type { AgentProfileProducerV1, AgentProfilePublicationBindingV1, CreateAgentProfileProducerOptionsV1, -} from './agent-profile-producer-contract-v1.js'; +} from './agent-profile-producer-api-v1.js'; import { prepareAgentProfileProductionInventoryV1, type AgentProfileProducerInventoryDependenciesV1, @@ -27,7 +27,7 @@ import { type AgentProfileProducerSigningDependenciesV1, } from './agent-profile-producer-signing-v1.js'; -export * from './agent-profile-producer-contract-v1.js'; +export * from './agent-profile-producer-api-v1.js'; /** * Author one local profile record. No protocol, timer, queue, or independent diff --git a/packages/agent/src/system-records/in-memory-agent-profile-publication-store-v1.ts b/packages/agent/src/system-records/in-memory-agent-profile-publication-store-v1.ts index f0e56091fe..066f86cffd 100644 --- a/packages/agent/src/system-records/in-memory-agent-profile-publication-store-v1.ts +++ b/packages/agent/src/system-records/in-memory-agent-profile-publication-store-v1.ts @@ -12,7 +12,6 @@ import { } from '@origintrail-official/dkg-core/system-record-v1'; import { - flattenAgentProfileProducerPublicationArtifactsV1, type AgentProfileProducerPublicationCommitLeaseV1, type AgentProfileProducerPublicationCommitV1, type AgentProfileProducerPublicationStoreV1, @@ -77,9 +76,12 @@ export function createInMemoryAgentProfilePublicationStoreV1( // cannot obtain a second lease and make the installed projection stale. prepared = true; try { - const publicationArtifacts = flattenAgentProfileProducerPublicationArtifactsV1( - input.publicationArtifacts, - ); + const publicationArtifacts: readonly SystemRecordArtifactV1[] = Object.freeze([ + input.publicationArtifacts.head, + input.publicationArtifacts.bundle, + input.publicationArtifacts.ownedSubjectTable, + ...input.publicationArtifacts.inventoryObjects, + ]); let addedObjects = 0; let addedBytes = 0; for (const artifact of publicationArtifacts) { diff --git a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts index a53c97e7cf..8722e50024 100644 --- a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts +++ b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts @@ -6,9 +6,11 @@ import { } from '../src/system-records/agent-profile-producer-preparation-v1.js'; import type { AgentProfilePublicationBindingV1, -} from '../src/system-records/agent-profile-producer-contract-v1.js'; +} from '../src/system-records/agent-profile-producer-api-v1.js'; // @ts-expect-error phase dependency DTOs are not exported by the producer entrypoint. import type { AgentProfileProducerPreparationDependenciesV1 as LeakedPreparationDeps } from '../src/system-records/agent-profile-producer-v1.js'; +// @ts-expect-error artifact flattening is an inventory/store implementation detail. +import { flattenAgentProfileProducerPublicationArtifactsV1 as leakedFlattenArtifacts } from '../src/system-records/agent-profile-producer-v1.js'; declare const dependencies: AgentProfileProducerPreparationDependenciesV1; declare const prepared: PreparedAgentProfileV1; @@ -30,6 +32,7 @@ void prepareAgentProfileProductionV1(dependencies, prepared, publication); declare const leaked: LeakedPreparationDeps; void leaked; +void leakedFlattenArtifacts; // @ts-expect-error package exports block the preparation implementation phase. type PublishedPreparationPhase = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-preparation-v1.js'); From 7e01bab147c5e04f1ed1e37c7df95b5c682578c2 Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sat, 8 Aug 2026 23:57:56 +0200 Subject: [PATCH 09/12] refactor(agent): centralize internal publication artifacts --- packages/agent/package.json | 1 + packages/agent/scripts/test-package-root.mjs | 1 + ...-profile-producer-artifacts-v1-internal.ts | 13 +++++++ .../agent-profile-producer-commit-v1.ts | 19 +++++++++-- .../agent-profile-producer-inventory-v1.ts | 17 ++-------- .../agent-profile-producer-signing-v1.ts | 7 +++- ...mory-agent-profile-publication-store-v1.ts | 10 +++--- ...ile-producer-phase-boundaries.typecheck.ts | 3 ++ ...nt-profile-producer-publication-v1.test.ts | 34 +++++++++++++++++++ 9 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 packages/agent/src/system-records/agent-profile-producer-artifacts-v1-internal.ts diff --git a/packages/agent/package.json b/packages/agent/package.json index da62364592..222b3700aa 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -34,6 +34,7 @@ "./dist/system-records/agent-profile-producer-signing-v1.js": null, "./dist/system-records/agent-profile-producer-inventory-v1.js": null, "./dist/system-records/agent-profile-producer-commit-v1.js": null, + "./dist/system-records/agent-profile-producer-artifacts-v1-internal.js": null, "./dist/rfc64/control-envelope-signer-v1.js": null, "./dist/rfc64/swm-inventory-shadow-runtime-v1.js": null, "./dist/rfc64/control-object-store-v1-internal.js": null, diff --git a/packages/agent/scripts/test-package-root.mjs b/packages/agent/scripts/test-package-root.mjs index 994c5bbf1d..816eb96a32 100644 --- a/packages/agent/scripts/test-package-root.mjs +++ b/packages/agent/scripts/test-package-root.mjs @@ -17,6 +17,7 @@ const expectedRfc64PolicyCells = [ 'private-curated', ]; const internalAgentProfilePhaseSpecifiers = [ + '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-artifacts-v1-internal.js', '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-preparation-v1.js', '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-signing-v1.js', '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-inventory-v1.js', diff --git a/packages/agent/src/system-records/agent-profile-producer-artifacts-v1-internal.ts b/packages/agent/src/system-records/agent-profile-producer-artifacts-v1-internal.ts new file mode 100644 index 0000000000..81170452dd --- /dev/null +++ b/packages/agent/src/system-records/agent-profile-producer-artifacts-v1-internal.ts @@ -0,0 +1,13 @@ +import type { AgentProfileProducerPublicationArtifactsV1 } from './agent-profile-producer-api-v1.js'; +import type { SystemRecordArtifactV1 } from './artifact-v1.js'; + +export function flattenAgentProfileProducerPublicationArtifactsV1( + artifacts: AgentProfileProducerPublicationArtifactsV1, +): readonly SystemRecordArtifactV1[] { + return Object.freeze([ + artifacts.head, + artifacts.bundle, + artifacts.ownedSubjectTable, + ...artifacts.inventoryObjects, + ]); +} diff --git a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts index f46729a267..88862e4029 100644 --- a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts @@ -12,10 +12,25 @@ export interface AgentProfileProducerCommitDependenciesV1 { readonly producer: Pick; } +type AgentProfileProducerCommitPreparationV1 = Pick< + AgentProfileProductionPreparationV1, + | 'snapshot' + | 'head' + | 'headDigest' + | 'projectionBytes' + | 'projectionQuads' + | 'ownedSubjectTable' +>; + +type AgentProfileProducerCommitSigningV1 = Pick< + SignedAgentProfileProductionV1, + 'envelope' +>; + export async function commitAgentProfileProductionV1( dependencies: AgentProfileProducerCommitDependenciesV1, - preparation: AgentProfileProductionPreparationV1, - signed: SignedAgentProfileProductionV1, + preparation: AgentProfileProducerCommitPreparationV1, + signed: AgentProfileProducerCommitSigningV1, inventoryPlan: AgentProfileProductionInventoryV1, signal: AbortSignal, ): Promise { diff --git a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts index 464a67ab46..47c9dd46ac 100644 --- a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts @@ -27,10 +27,8 @@ import { } from './agent-profile-producer-api-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; -import { - systemRecordArtifactKeyV1, - type SystemRecordArtifactV1, -} from './artifact-v1.js'; +import { flattenAgentProfileProducerPublicationArtifactsV1 } from './agent-profile-producer-artifacts-v1-internal.js'; +import { systemRecordArtifactKeyV1 } from './artifact-v1.js'; export interface AgentProfileProducerInventoryDependenciesV1 { readonly networkId: NetworkIdV1; @@ -157,17 +155,6 @@ interface PublicationArtifactSetInputV1 { readonly inventoryUpdate: ReturnType | null; } -function flattenAgentProfileProducerPublicationArtifactsV1( - artifacts: AgentProfileProducerPublicationArtifactsV1, -): readonly SystemRecordArtifactV1[] { - return Object.freeze([ - artifacts.head, - artifacts.bundle, - artifacts.ownedSubjectTable, - ...artifacts.inventoryObjects, - ]); -} - function publicationArtifactSet( input: PublicationArtifactSetInputV1, ): AgentProfileProducerPublicationArtifactsV1 { diff --git a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts index 1d767d7346..692621abe7 100644 --- a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts @@ -19,9 +19,14 @@ export interface SignedAgentProfileProductionV1 { readonly envelopeBytes: Uint8Array; } +type AgentProfileProducerSigningInputV1 = Pick< + AgentProfileProductionPreparationV1, + 'head' | 'headDigest' +>; + export async function signAgentProfileProductionV1( dependencies: AgentProfileProducerSigningDependenciesV1, - preparation: AgentProfileProductionPreparationV1, + preparation: AgentProfileProducerSigningInputV1, signal: AbortSignal, ): Promise { const [peerSignature, evmSignature] = await Promise.all([ diff --git a/packages/agent/src/system-records/in-memory-agent-profile-publication-store-v1.ts b/packages/agent/src/system-records/in-memory-agent-profile-publication-store-v1.ts index 066f86cffd..22b35c0a5a 100644 --- a/packages/agent/src/system-records/in-memory-agent-profile-publication-store-v1.ts +++ b/packages/agent/src/system-records/in-memory-agent-profile-publication-store-v1.ts @@ -16,6 +16,7 @@ import { type AgentProfileProducerPublicationCommitV1, type AgentProfileProducerPublicationStoreV1, } from './agent-profile-producer-v1.js'; +import { flattenAgentProfileProducerPublicationArtifactsV1 } from './agent-profile-producer-artifacts-v1-internal.js'; import { cloneSystemRecordArtifactV1, systemRecordArtifactKeyV1, @@ -76,12 +77,9 @@ export function createInMemoryAgentProfilePublicationStoreV1( // cannot obtain a second lease and make the installed projection stale. prepared = true; try { - const publicationArtifacts: readonly SystemRecordArtifactV1[] = Object.freeze([ - input.publicationArtifacts.head, - input.publicationArtifacts.bundle, - input.publicationArtifacts.ownedSubjectTable, - ...input.publicationArtifacts.inventoryObjects, - ]); + const publicationArtifacts = flattenAgentProfileProducerPublicationArtifactsV1( + input.publicationArtifacts, + ); let addedObjects = 0; let addedBytes = 0; for (const artifact of publicationArtifacts) { diff --git a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts index 8722e50024..5d5f1dbf04 100644 --- a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts +++ b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts @@ -36,6 +36,8 @@ void leakedFlattenArtifacts; // @ts-expect-error package exports block the preparation implementation phase. type PublishedPreparationPhase = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-preparation-v1.js'); +// @ts-expect-error package exports block the internal artifact helper. +type PublishedArtifactHelper = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-artifacts-v1-internal.js'); // @ts-expect-error package exports block the signing implementation phase. type PublishedSigningPhase = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-signing-v1.js'); // @ts-expect-error package exports block the inventory implementation phase. @@ -44,6 +46,7 @@ type PublishedInventoryPhase = typeof import('@origintrail-official/dkg-agent/di type PublishedCommitPhase = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-commit-v1.js'); void (undefined as PublishedPreparationPhase | undefined); +void (undefined as PublishedArtifactHelper | undefined); void (undefined as PublishedSigningPhase | undefined); void (undefined as PublishedInventoryPhase | undefined); void (undefined as PublishedCommitPhase | undefined); diff --git a/packages/agent/test/system-record-agent-profile-producer-publication-v1.test.ts b/packages/agent/test/system-record-agent-profile-producer-publication-v1.test.ts index 0c7fddfd48..61b224f3b2 100644 --- a/packages/agent/test/system-record-agent-profile-producer-publication-v1.test.ts +++ b/packages/agent/test/system-record-agent-profile-producer-publication-v1.test.ts @@ -1,5 +1,6 @@ import { parseCanonicalSignedAgentProfileHeadEnvelopeV1, + type Digest32V1, type SignedAgentProfileHeadEnvelopeV1, } from '@origintrail-official/dkg-core/system-record-v1'; import { @@ -9,6 +10,8 @@ import { vi, } from 'vitest'; import { prepareAgentProfileV1 } from '../src/profile.js'; +import type { AgentProfileProducerPublicationArtifactsV1 } from '../src/system-records/agent-profile-producer-api-v1.js'; +import { flattenAgentProfileProducerPublicationArtifactsV1 } from '../src/system-records/agent-profile-producer-artifacts-v1-internal.js'; import { type AgentProfileProducerPublicationStoreV1 } from '../src/system-records/agent-profile-producer-v1.js'; import { DEPLOYMENT, @@ -25,6 +28,37 @@ import { describe('agent-profile system-record producer V1 publication and inventory', () => { + it('flattens every publication artifact group through one ordered path', () => { + const artifact = ( + objectKind: 'agent-profile-head' | 'profile-bundle' | 'owned-subject-table' + | 'inventory-internal' | 'inventory-leaf', + byte: number, + ) => Object.freeze({ + objectKind, + objectDigest: `0x${byte.toString(16).padStart(64, '0')}` as Digest32V1, + canonicalBytes: Uint8Array.of(byte), + }); + const artifacts = { + head: artifact('agent-profile-head', 1), + bundle: artifact('profile-bundle', 2), + ownedSubjectTable: artifact('owned-subject-table', 3), + inventoryObjects: Object.freeze([ + artifact('inventory-internal', 4), + artifact('inventory-leaf', 5), + ]), + } as AgentProfileProducerPublicationArtifactsV1; + + expect(flattenAgentProfileProducerPublicationArtifactsV1(artifacts).map( + ({ objectKind, canonicalBytes }) => [objectKind, canonicalBytes[0]], + )).toEqual([ + ['agent-profile-head', 1], + ['profile-bundle', 2], + ['owned-subject-table', 3], + ['inventory-internal', 4], + ['inventory-leaf', 5], + ]); + }); + it('stages one exact profile, installs it, then advertises the signed inventory root', async () => { const fixture = await producerFixture(); const events: string[] = []; From 767c5961c11bec1f7c1f0f9e53843004f0ab8fc2 Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sun, 9 Aug 2026 00:08:46 +0200 Subject: [PATCH 10/12] refactor(agent): simplify prepared projection boundary --- .../agent-profile-producer-preparation-v1.ts | 31 +++---------------- .../agent-profile-producer-v1.ts | 12 +++---- ...ile-producer-phase-boundaries.typecheck.ts | 14 ++------- ...ent-profile-producer-validation-v1.test.ts | 21 ------------- 4 files changed, 13 insertions(+), 65 deletions(-) diff --git a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts index 7628f384b8..e9cd683b4e 100644 --- a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts @@ -43,10 +43,6 @@ import type { } from './agent-profile-producer-api-v1.js'; const UTF8 = new TextEncoder(); -const VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1: unique symbol = Symbol( - 'validated-agent-profile-production-input-v1', -); - export interface AgentProfileProducerPreparationDependenciesV1 { readonly networkId: NetworkIdV1; readonly publicationDeployment: Readonly; @@ -57,8 +53,7 @@ export interface AgentProfileProducerPreparationDependenciesV1 { readonly store: Pick; } -export interface ValidatedAgentProfileProductionInputV1 { - readonly [VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1]: true; +export interface PreparedProfileProjectionSnapshotV1 { readonly preparedSnapshot: PreparedAgentProfileV1; readonly projectionQuads: readonly Readonly[]; readonly ownedSubjectTable: OwnedSubjectTableObjectV1; @@ -80,10 +75,9 @@ export interface AgentProfileProductionPreparationV1 { export async function prepareAgentProfileProductionV1( dependencies: AgentProfileProducerPreparationDependenciesV1, - input: ValidatedAgentProfileProductionInputV1, + input: PreparedProfileProjectionSnapshotV1, inputPublication: AgentProfilePublicationBindingV1, ): Promise { - assertValidatedAgentProfileProductionInputV1(input); const { preparedSnapshot: prepared, projectionQuads, ownedSubjectTable } = input; const publication = snapshotConfirmedPublicationBindingV1(inputPublication); const issuedAt = normalizePublicationTimestampV1(publication.issuedAt, 'issuedAt'); @@ -217,29 +211,13 @@ export async function prepareAgentProfileProductionV1( }); } -function assertValidatedAgentProfileProductionInputV1( - input: unknown, -): asserts input is ValidatedAgentProfileProductionInputV1 { - if (typeof input !== 'object' - || input === null - || !Object.prototype.hasOwnProperty.call( - input, - VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1, - ) - || (input as Record)[ - VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1 - ] !== true) { - throw new TypeError('agent-profile preparation requires a validated production input'); - } -} - -export function validateAgentProfileProductionInputV1( +export function snapshotAgentProfileProductionInputV1( dependencies: Pick< AgentProfileProducerPreparationDependenciesV1, 'peerId' | 'peerPublicKey' | 'evmIssuer' >, prepared: PreparedAgentProfileV1, -): ValidatedAgentProfileProductionInputV1 { +): PreparedProfileProjectionSnapshotV1 { const preparedSnapshot = snapshotPreparedProfileV1(prepared); const projected = preparedSnapshot.projectionQuads .map((quad) => Object.freeze({ ...quad })); @@ -263,7 +241,6 @@ export function validateAgentProfileProductionInputV1( dependencies.evmIssuer, ); return Object.freeze({ - [VALIDATED_AGENT_PROFILE_PRODUCTION_INPUT_V1]: true as const, preparedSnapshot, projectionQuads: Object.freeze(projected), ownedSubjectTable, diff --git a/packages/agent/src/system-records/agent-profile-producer-v1.ts b/packages/agent/src/system-records/agent-profile-producer-v1.ts index 7c4ec8e8e8..6a81b6a266 100644 --- a/packages/agent/src/system-records/agent-profile-producer-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-v1.ts @@ -18,9 +18,9 @@ import { } from './agent-profile-producer-inventory-v1.js'; import { prepareAgentProfileProductionV1, - validateAgentProfileProductionInputV1, + snapshotAgentProfileProductionInputV1, type AgentProfileProducerPreparationDependenciesV1, - type ValidatedAgentProfileProductionInputV1, + type PreparedProfileProjectionSnapshotV1, } from './agent-profile-producer-preparation-v1.js'; import { signAgentProfileProductionV1, @@ -60,7 +60,7 @@ export function createAgentProfileProducerV1( }); let active = false; const completePrepared = async ( - input: ValidatedAgentProfileProductionInputV1, + input: PreparedProfileProjectionSnapshotV1, publication: AgentProfilePublicationBindingV1, signal: AbortSignal, ): Promise => { @@ -93,14 +93,14 @@ export function createAgentProfileProducerV1( return Object.freeze({ async prepare(prepared: PreparedAgentProfileV1): Promise { if (active) throw new Error('agent-profile producer is busy'); - const validatedInput = validateAgentProfileProductionInputV1( + const projectionSnapshot = snapshotAgentProfileProductionInputV1( preparationDependencies, prepared, ); active = true; const controller = new AbortController(); try { - await options.fence(validatedInput.preparedSnapshot, controller.signal); + await options.fence(projectionSnapshot.preparedSnapshot, controller.signal); } catch (error) { active = false; throw error; @@ -114,7 +114,7 @@ export function createAgentProfileProducerV1( state = 'completing'; try { return await completePrepared( - validatedInput, + projectionSnapshot, publication, controller.signal, ); diff --git a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts index 5d5f1dbf04..54cadc431a 100644 --- a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts +++ b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts @@ -1,8 +1,8 @@ import type { PreparedAgentProfileV1 } from '../src/profile.js'; import { type AgentProfileProducerPreparationDependenciesV1, + type PreparedProfileProjectionSnapshotV1, prepareAgentProfileProductionV1, - type ValidatedAgentProfileProductionInputV1, } from '../src/system-records/agent-profile-producer-preparation-v1.js'; import type { AgentProfilePublicationBindingV1, @@ -14,20 +14,12 @@ import { flattenAgentProfileProducerPublicationArtifactsV1 as leakedFlattenArtif declare const dependencies: AgentProfileProducerPreparationDependenciesV1; declare const prepared: PreparedAgentProfileV1; -declare const validated: ValidatedAgentProfileProductionInputV1; +declare const validated: PreparedProfileProjectionSnapshotV1; declare const publication: AgentProfilePublicationBindingV1; void prepareAgentProfileProductionV1(dependencies, validated, publication); -// @ts-expect-error callers cannot fabricate the module-private validated-input brand. -const fabricated: ValidatedAgentProfileProductionInputV1 = { - preparedSnapshot: prepared, - projectionQuads: prepared.projectionQuads, - ownedSubjectTable: [], -}; -void fabricated; - -// @ts-expect-error preparation accepts only the bound validated snapshot/projection input. +// @ts-expect-error preparation accepts a snapshotted projection plan, not a raw profile. void prepareAgentProfileProductionV1(dependencies, prepared, publication); declare const leaked: LeakedPreparationDeps; diff --git a/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts b/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts index 3a25df3261..e17dde0cda 100644 --- a/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts +++ b/packages/agent/test/system-record-agent-profile-producer-validation-v1.test.ts @@ -11,7 +11,6 @@ import { type AgentProfileProducerPublicationStoreV1, type AgentProfilePublicationBindingV1, } from '../src/system-records/agent-profile-producer-v1.js'; -import { prepareAgentProfileProductionV1 } from '../src/system-records/agent-profile-producer-preparation-v1.js'; import { createInMemoryAgentProfilePublicationStoreV1 } from '../src/system-records/in-memory-agent-profile-publication-store-v1.js'; import { DEPLOYMENT, @@ -26,26 +25,6 @@ import { describe('agent-profile system-record producer V1 validation and binding', () => { - it('rejects a fabricated preparation input before reading dependencies or publication', async () => { - let reads = 0; - const unread = new Proxy({}, { - get() { - reads += 1; - throw new Error('fabricated input escaped the runtime boundary'); - }, - }); - await expect(prepareAgentProfileProductionV1( - unread as never, - { - preparedSnapshot: null, - projectionQuads: [], - ownedSubjectTable: [], - } as never, - unread as never, - )).rejects.toThrow(/requires a validated production input/); - expect(reads).toBe(0); - }); - it('preflights provider capacity before materialization and releases a failed commit lease', async () => { const fixture = await producerFixture(createInMemoryAgentProfilePublicationStoreV1({ maxObjects: 1, From 03be93e240a1c56e46f71317e0d9c5befc0aa4b6 Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sun, 9 Aug 2026 00:19:25 +0200 Subject: [PATCH 11/12] fix(agent): preserve producer deep import compatibility --- packages/agent/package.json | 11 ++--- packages/agent/scripts/test-package-root.mjs | 42 +++++++++++++++++++ .../agent-profile-producer-v1.ts | 3 ++ ...ile-producer-phase-boundaries.typecheck.ts | 3 -- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/packages/agent/package.json b/packages/agent/package.json index 222b3700aa..e1b0667c16 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -30,11 +30,12 @@ "./dist/rfc64/inventory-v1/statements.js": "./dist/rfc64/inventory-v1/statements.js", "./dist/rfc64/public-catalog-activation-config-v1.js": "./dist/rfc64/public-catalog-activation-config-v1.js", "./dist/rfc64/swm-author-inventory-producer-v1.js": "./dist/rfc64/swm-author-inventory-producer-v1.js", - "./dist/system-records/agent-profile-producer-preparation-v1.js": null, - "./dist/system-records/agent-profile-producer-signing-v1.js": null, - "./dist/system-records/agent-profile-producer-inventory-v1.js": null, - "./dist/system-records/agent-profile-producer-commit-v1.js": null, - "./dist/system-records/agent-profile-producer-artifacts-v1-internal.js": null, + "./dist/system-records/agent-profile-producer-v1.js": "./dist/system-records/agent-profile-producer-v1.js", + "./dist/system-records/artifact-v1.js": "./dist/system-records/artifact-v1.js", + "./dist/system-records/in-memory-agent-profile-publication-store-v1.js": "./dist/system-records/in-memory-agent-profile-publication-store-v1.js", + "./dist/system-records/provider-v1.js": "./dist/system-records/provider-v1.js", + "./dist/system-records/transport-v1.js": "./dist/system-records/transport-v1.js", + "./dist/system-records/*": null, "./dist/rfc64/control-envelope-signer-v1.js": null, "./dist/rfc64/swm-inventory-shadow-runtime-v1.js": null, "./dist/rfc64/control-object-store-v1-internal.js": null, diff --git a/packages/agent/scripts/test-package-root.mjs b/packages/agent/scripts/test-package-root.mjs index 816eb96a32..fff2fc3755 100644 --- a/packages/agent/scripts/test-package-root.mjs +++ b/packages/agent/scripts/test-package-root.mjs @@ -8,6 +8,9 @@ const legacyAgent = await import('@origintrail-official/dkg-agent/dist/dkg-agent const publicCatalogActivation = await import( '@origintrail-official/dkg-agent/rfc64/public-catalog-activation-config-v1' ); +const agentProfileProducer = await import( + '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-v1.js' +); const require = createRequire(import.meta.url); const packageManifest = require('@origintrail-official/dkg-agent/package.json'); const expectedRfc64PolicyCells = [ @@ -17,12 +20,20 @@ const expectedRfc64PolicyCells = [ 'private-curated', ]; const internalAgentProfilePhaseSpecifiers = [ + '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-api-v1.js', '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-artifacts-v1-internal.js', '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-preparation-v1.js', '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-signing-v1.js', '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-inventory-v1.js', '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-commit-v1.js', ]; +const publicSystemRecordSpecifiers = [ + '@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-v1.js', + '@origintrail-official/dkg-agent/dist/system-records/artifact-v1.js', + '@origintrail-official/dkg-agent/dist/system-records/in-memory-agent-profile-publication-store-v1.js', + '@origintrail-official/dkg-agent/dist/system-records/provider-v1.js', + '@origintrail-official/dkg-agent/dist/system-records/transport-v1.js', +]; for (const specifier of internalAgentProfilePhaseSpecifiers) { let resolved = false; @@ -35,6 +46,37 @@ for (const specifier of internalAgentProfilePhaseSpecifiers) { if (resolved) throw new Error(`published package exposed internal phase ${specifier}`); } +for (const specifier of publicSystemRecordSpecifiers) { + const subpath = `.${specifier.slice('@origintrail-official/dkg-agent'.length)}`; + if (packageManifest.exports[subpath] !== subpath) { + throw new Error(`historical System Record module is not explicitly public: ${specifier}`); + } + await import(specifier); +} +if (packageManifest.exports['./dist/system-records/*'] !== null) { + throw new Error('unclassified System Record deep imports are not blocked by default'); +} + +const profileArtifact = (objectKind, byte) => Object.freeze({ + objectKind, + objectDigest: `0x${byte.toString(16).padStart(64, '0')}`, + canonicalBytes: Uint8Array.of(byte), +}); +const flattenedProfileArtifacts = agentProfileProducer + .flattenAgentProfileProducerPublicationArtifactsV1({ + head: profileArtifact('agent-profile-head', 1), + bundle: profileArtifact('profile-bundle', 2), + ownedSubjectTable: profileArtifact('owned-subject-table', 3), + inventoryObjects: Object.freeze([ + profileArtifact('inventory-internal', 4), + profileArtifact('inventory-leaf', 5), + ]), + }); +if (flattenedProfileArtifacts.map((artifact) => artifact.objectKind).join(',') + !== 'agent-profile-head,profile-bundle,owned-subject-table,inventory-internal,inventory-leaf') { + throw new Error('historical producer artifact-flattening export changed behavior'); +} + if ( typeof root.DKGAgent !== 'function' || typeof legacyAgent.DKGAgent !== 'function' diff --git a/packages/agent/src/system-records/agent-profile-producer-v1.ts b/packages/agent/src/system-records/agent-profile-producer-v1.ts index 6a81b6a266..b47867ed6e 100644 --- a/packages/agent/src/system-records/agent-profile-producer-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-v1.ts @@ -28,6 +28,9 @@ import { } from './agent-profile-producer-signing-v1.js'; export * from './agent-profile-producer-api-v1.js'; +export { + flattenAgentProfileProducerPublicationArtifactsV1, +} from './agent-profile-producer-artifacts-v1-internal.js'; /** * Author one local profile record. No protocol, timer, queue, or independent diff --git a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts index 54cadc431a..ba6a57ea25 100644 --- a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts +++ b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts @@ -9,8 +9,6 @@ import type { } from '../src/system-records/agent-profile-producer-api-v1.js'; // @ts-expect-error phase dependency DTOs are not exported by the producer entrypoint. import type { AgentProfileProducerPreparationDependenciesV1 as LeakedPreparationDeps } from '../src/system-records/agent-profile-producer-v1.js'; -// @ts-expect-error artifact flattening is an inventory/store implementation detail. -import { flattenAgentProfileProducerPublicationArtifactsV1 as leakedFlattenArtifacts } from '../src/system-records/agent-profile-producer-v1.js'; declare const dependencies: AgentProfileProducerPreparationDependenciesV1; declare const prepared: PreparedAgentProfileV1; @@ -24,7 +22,6 @@ void prepareAgentProfileProductionV1(dependencies, prepared, publication); declare const leaked: LeakedPreparationDeps; void leaked; -void leakedFlattenArtifacts; // @ts-expect-error package exports block the preparation implementation phase. type PublishedPreparationPhase = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-preparation-v1.js'); From f1ae8ecdc766f71f212c22dbefeafd94d4eae64d Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Sun, 9 Aug 2026 00:31:10 +0200 Subject: [PATCH 12/12] refactor(agent): collapse producer phase adapters --- .../agent-profile-producer-commit-v1.ts | 8 ++-- .../agent-profile-producer-inventory-v1.ts | 4 +- .../agent-profile-producer-preparation-v1.ts | 12 +++--- .../agent-profile-producer-signing-v1.ts | 4 +- .../agent-profile-producer-v1.ts | 42 +++++-------------- ...ile-producer-phase-boundaries.typecheck.ts | 8 +--- 6 files changed, 26 insertions(+), 52 deletions(-) diff --git a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts index 88862e4029..6d7b93879d 100644 --- a/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-commit-v1.ts @@ -7,9 +7,9 @@ import type { AgentProfileProductionInventoryV1 } from './agent-profile-producer import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; -export interface AgentProfileProducerCommitDependenciesV1 { +interface AgentProfileProducerCommitContextV1 { readonly store: Pick; - readonly producer: Pick; + readonly install: CreateAgentProfileProducerOptionsV1['install']; } type AgentProfileProducerCommitPreparationV1 = Pick< @@ -28,7 +28,7 @@ type AgentProfileProducerCommitSigningV1 = Pick< >; export async function commitAgentProfileProductionV1( - dependencies: AgentProfileProducerCommitDependenciesV1, + dependencies: AgentProfileProducerCommitContextV1, preparation: AgentProfileProducerCommitPreparationV1, signed: AgentProfileProducerCommitSigningV1, inventoryPlan: AgentProfileProductionInventoryV1, @@ -44,7 +44,7 @@ export async function commitAgentProfileProductionV1( let committed = false; try { signal.throwIfAborted(); - await dependencies.producer.install({ + await dependencies.install({ head: preparation.head, envelope: signed.envelope, canonicalProjectionBytes: preparation.projectionBytes, diff --git a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts index 47c9dd46ac..1db86f7873 100644 --- a/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-inventory-v1.ts @@ -30,7 +30,7 @@ import type { SignedAgentProfileProductionV1 } from './agent-profile-producer-si import { flattenAgentProfileProducerPublicationArtifactsV1 } from './agent-profile-producer-artifacts-v1-internal.js'; import { systemRecordArtifactKeyV1 } from './artifact-v1.js'; -export interface AgentProfileProducerInventoryDependenciesV1 { +interface AgentProfileProducerInventoryContextV1 { readonly networkId: NetworkIdV1; readonly peerSigner: SystemRecordPeerSignerV1; readonly store: Pick; @@ -46,7 +46,7 @@ export interface AgentProfileProductionInventoryV1 { } export async function prepareAgentProfileProductionInventoryV1( - dependencies: AgentProfileProducerInventoryDependenciesV1, + dependencies: AgentProfileProducerInventoryContextV1, preparation: AgentProfileProductionPreparationV1, signed: SignedAgentProfileProductionV1, signal: AbortSignal, diff --git a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts index e9cd683b4e..a4b5ed5152 100644 --- a/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-preparation-v1.ts @@ -43,13 +43,13 @@ import type { } from './agent-profile-producer-api-v1.js'; const UTF8 = new TextEncoder(); -export interface AgentProfileProducerPreparationDependenciesV1 { +interface AgentProfileProducerPreparationContextV1 { readonly networkId: NetworkIdV1; readonly publicationDeployment: Readonly; readonly peerId: string; readonly peerPublicKey: SystemRecordPeerSignerV1['publicKey']; readonly evmIssuer: string; - readonly clock: Pick; + readonly nowMs: CreateAgentProfileProducerOptionsV1['nowMs']; readonly store: Pick; } @@ -60,7 +60,7 @@ export interface PreparedProfileProjectionSnapshotV1 { } export interface AgentProfileProductionPreparationV1 { - readonly snapshot: ReturnType; + readonly snapshot: ReturnType; readonly verifierNowMs: number; readonly projectionQuads: readonly Readonly[]; readonly projectionBytes: Uint8Array; @@ -74,7 +74,7 @@ export interface AgentProfileProductionPreparationV1 { } export async function prepareAgentProfileProductionV1( - dependencies: AgentProfileProducerPreparationDependenciesV1, + dependencies: AgentProfileProducerPreparationContextV1, input: PreparedProfileProjectionSnapshotV1, inputPublication: AgentProfilePublicationBindingV1, ): Promise { @@ -87,7 +87,7 @@ export async function prepareAgentProfileProductionV1( 'assertionFinalizedAt', ); const assertionFinalizedAtMs = Date.parse(publication.seal.assertionFinalizedAt); - const verifierNowMs = producerNowMs(dependencies.clock.nowMs?.() ?? Date.now()); + const verifierNowMs = producerNowMs(dependencies.nowMs?.() ?? Date.now()); if (Date.parse(issuedAt) > verifierNowMs + SYSTEM_RECORD_MAX_CLOCK_SKEW_MS) { throw new Error('agent-profile issuedAt exceeds the future clock-skew bound'); } @@ -213,7 +213,7 @@ export async function prepareAgentProfileProductionV1( export function snapshotAgentProfileProductionInputV1( dependencies: Pick< - AgentProfileProducerPreparationDependenciesV1, + AgentProfileProducerPreparationContextV1, 'peerId' | 'peerPublicKey' | 'evmIssuer' >, prepared: PreparedAgentProfileV1, diff --git a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts index 692621abe7..09541d9d8f 100644 --- a/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-signing-v1.ts @@ -9,7 +9,7 @@ import type { EvmPersonalMessageSignerV1 } from '../evm-message-signer-v1.js'; import type { SystemRecordPeerSignerV1 } from './agent-profile-producer-api-v1.js'; import type { AgentProfileProductionPreparationV1 } from './agent-profile-producer-preparation-v1.js'; -export interface AgentProfileProducerSigningDependenciesV1 { +interface AgentProfileProducerSigningContextV1 { readonly peerSigner: SystemRecordPeerSignerV1; readonly evmSigner: EvmPersonalMessageSignerV1; } @@ -25,7 +25,7 @@ type AgentProfileProducerSigningInputV1 = Pick< >; export async function signAgentProfileProductionV1( - dependencies: AgentProfileProducerSigningDependenciesV1, + dependencies: AgentProfileProducerSigningContextV1, preparation: AgentProfileProducerSigningInputV1, signal: AbortSignal, ): Promise { diff --git a/packages/agent/src/system-records/agent-profile-producer-v1.ts b/packages/agent/src/system-records/agent-profile-producer-v1.ts index b47867ed6e..9b77a73e9f 100644 --- a/packages/agent/src/system-records/agent-profile-producer-v1.ts +++ b/packages/agent/src/system-records/agent-profile-producer-v1.ts @@ -1,10 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { PreparedAgentProfileV1 } from '../profile.js'; -import { - commitAgentProfileProductionV1, - type AgentProfileProducerCommitDependenciesV1, -} from './agent-profile-producer-commit-v1.js'; +import { commitAgentProfileProductionV1 } from './agent-profile-producer-commit-v1.js'; import type { AgentProfileProducerLeaseV1, AgentProfileProducerPublicationV1, @@ -12,20 +9,13 @@ import type { AgentProfilePublicationBindingV1, CreateAgentProfileProducerOptionsV1, } from './agent-profile-producer-api-v1.js'; -import { - prepareAgentProfileProductionInventoryV1, - type AgentProfileProducerInventoryDependenciesV1, -} from './agent-profile-producer-inventory-v1.js'; +import { prepareAgentProfileProductionInventoryV1 } from './agent-profile-producer-inventory-v1.js'; import { prepareAgentProfileProductionV1, snapshotAgentProfileProductionInputV1, - type AgentProfileProducerPreparationDependenciesV1, type PreparedProfileProjectionSnapshotV1, } from './agent-profile-producer-preparation-v1.js'; -import { - signAgentProfileProductionV1, - type AgentProfileProducerSigningDependenciesV1, -} from './agent-profile-producer-signing-v1.js'; +import { signAgentProfileProductionV1 } from './agent-profile-producer-signing-v1.js'; export * from './agent-profile-producer-api-v1.js'; export { @@ -39,27 +29,17 @@ export { export function createAgentProfileProducerV1( options: CreateAgentProfileProducerOptionsV1, ): AgentProfileProducerV1 { - const preparationDependencies: AgentProfileProducerPreparationDependenciesV1 = Object.freeze({ + const context = Object.freeze({ networkId: options.networkId, publicationDeployment: options.publicationDeployment, peerId: options.peerSigner.peerId, peerPublicKey: options.peerSigner.publicKey, evmIssuer: options.evmSigner.address, - clock: options, + nowMs: options.nowMs, store: options.store, - }); - const signingDependencies: AgentProfileProducerSigningDependenciesV1 = Object.freeze({ peerSigner: options.peerSigner, evmSigner: options.evmSigner, - }); - const inventoryDependencies: AgentProfileProducerInventoryDependenciesV1 = Object.freeze({ - networkId: options.networkId, - peerSigner: options.peerSigner, - store: options.store, - }); - const commitDependencies: AgentProfileProducerCommitDependenciesV1 = Object.freeze({ - store: options.store, - producer: options, + install: options.install, }); let active = false; const completePrepared = async ( @@ -69,23 +49,23 @@ export function createAgentProfileProducerV1( ): Promise => { signal.throwIfAborted(); const preparation = await prepareAgentProfileProductionV1( - preparationDependencies, + context, input, publication, ); const signed = await signAgentProfileProductionV1( - signingDependencies, + context, preparation, signal, ); const inventoryPlan = await prepareAgentProfileProductionInventoryV1( - inventoryDependencies, + context, preparation, signed, signal, ); return commitAgentProfileProductionV1( - commitDependencies, + context, preparation, signed, inventoryPlan, @@ -97,7 +77,7 @@ export function createAgentProfileProducerV1( async prepare(prepared: PreparedAgentProfileV1): Promise { if (active) throw new Error('agent-profile producer is busy'); const projectionSnapshot = snapshotAgentProfileProductionInputV1( - preparationDependencies, + context, prepared, ); active = true; diff --git a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts index ba6a57ea25..faa6113ef2 100644 --- a/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts +++ b/packages/agent/test/agent-profile-producer-phase-boundaries.typecheck.ts @@ -1,16 +1,13 @@ import type { PreparedAgentProfileV1 } from '../src/profile.js'; import { - type AgentProfileProducerPreparationDependenciesV1, type PreparedProfileProjectionSnapshotV1, prepareAgentProfileProductionV1, } from '../src/system-records/agent-profile-producer-preparation-v1.js'; import type { AgentProfilePublicationBindingV1, } from '../src/system-records/agent-profile-producer-api-v1.js'; -// @ts-expect-error phase dependency DTOs are not exported by the producer entrypoint. -import type { AgentProfileProducerPreparationDependenciesV1 as LeakedPreparationDeps } from '../src/system-records/agent-profile-producer-v1.js'; -declare const dependencies: AgentProfileProducerPreparationDependenciesV1; +declare const dependencies: Parameters[0]; declare const prepared: PreparedAgentProfileV1; declare const validated: PreparedProfileProjectionSnapshotV1; declare const publication: AgentProfilePublicationBindingV1; @@ -20,9 +17,6 @@ void prepareAgentProfileProductionV1(dependencies, validated, publication); // @ts-expect-error preparation accepts a snapshotted projection plan, not a raw profile. void prepareAgentProfileProductionV1(dependencies, prepared, publication); -declare const leaked: LeakedPreparationDeps; -void leaked; - // @ts-expect-error package exports block the preparation implementation phase. type PublishedPreparationPhase = typeof import('@origintrail-official/dkg-agent/dist/system-records/agent-profile-producer-preparation-v1.js'); // @ts-expect-error package exports block the internal artifact helper.