diff --git a/docs/bridge.md b/docs/bridge.md index a5e4944..2d906fa 100644 --- a/docs/bridge.md +++ b/docs/bridge.md @@ -196,9 +196,9 @@ The command is a dry-run shape probe for public surfaces that are real enough to VoidBot remains the Discord observer and mention scheduler. A Face registry entry may opt into a remote `remotePersonaFeedbackTarget`; only then does VoidBot insert an immutable typed `voidbot.discord.persona_feedback_event.v0` observation into the dedicated `BIFROST_PERSONA_FEEDBACK_OBSERVATION_STORE_PATH`. This is an observation inbox, never an actuator queue. Insert collisions must contain exactly equal content. Its canonical path and physical file identity must differ from the outbound Discord-command provider store and all Bifrost-private state. Local Persona mention handling is committed first; observation export runs independently with three bounded idempotent attempts and cannot suppress the local response path. -Discord operator control is a separate authority. VoidBot may insert only a strict `voidbot.discord.epiphany_operator_request.v0` document containing one of `status`, `sleep`, `wake`, or `directive`, a fixed target runtime, Discord provenance, and a positive expiry of at most 60 seconds. Bifrost independently requires the exact configured Discord owner and the exact persisted guild/actor mapping from its private actor-link store. The actor link is the admitted mapping authority; its Heimdall reference remains provenance until a signed live Heimdall claim surface exists and is not represented as authorization. Bifrost derives the Rust field-order packet tuple, hashes its canonical MessagePack bytes, and signs the exact `BifrostOperatorCommandAdmission` tuple for purpose `bifrost.operator-command.delivery.v0` with a dedicated operator Ed25519 seed. The feedback seed is not reused. +Discord operator control is a separate authority. VoidBot may insert only a strict `voidbot.discord.epiphany_operator_request.v1` document containing one of `status`, `sleep`, `wake`, `directive`, `reviews`, or `review`, a fixed target runtime, Discord provenance, and a positive expiry of at most 60 seconds. Bifrost independently requires the exact configured Discord owner and the exact persisted guild/actor mapping from its private actor-link store. The actor link is the admitted mapping authority; its Heimdall reference remains provenance until a signed live Heimdall claim surface exists and is not represented as authorization. Bifrost derives the Rust field-order packet tuple, hashes its canonical MessagePack bytes, and signs the exact `BifrostOperatorCommandAdmission` tuple for purpose `bifrost.operator-command.delivery.v1` with a dedicated operator Ed25519 seed. The feedback seed is not reused. -Bifrost does not mount an Epiphany store and has no argv or shell escape hatch. One supervised worker serializes requests across the CultNet RUDP boundary and requires the trusted Epiphany executor anchor. Success requires a host-signed `epiphany.operator_command.sealed_result.v0` whose payload digest, command id, packet digest, target runtime, completion time, and executor identity verify exactly; a transport acknowledgement is not success. Admissions and sealed results are persisted in the typed Bifrost operator ledger. Permanent pre-admission refusals are separately terminalized there, while transport failures remain retryable. An expired request cannot mint a new admission, but an exact admission already durably recorded before expiry may be retransmitted to recover Epiphany's one idempotent result without depending on mutable actor-link or signer state. The worker publishes a separate immutable Discord-delivery projection bound to the original request digest and interaction id; it exposes only bounded result/refusal fields and sealed-result fingerprints, never raw signatures or private state. VoidBot reads that outbox and owns Discord response/checkpoint state. Bifrost never manufactures success. Ordinary Persona messages remain `feedback_only` and cannot enter this path. +Bifrost does not mount an Epiphany store and has no argv or shell escape hatch. One supervised worker serializes requests across the CultNet RUDP boundary and requires the trusted Epiphany executor anchor. Success requires a host-signed `epiphany.operator_command.sealed_result.v1` whose payload digest, command id, packet digest, target runtime, completion time, and executor identity verify exactly; a transport acknowledgement is not success. Admissions and sealed results are persisted in the typed Bifrost operator ledger. Permanent pre-admission refusals are separately terminalized there, while transport failures remain retryable. An expired request cannot mint a new admission, but an exact admission already durably recorded before expiry may be retransmitted to recover Epiphany's one idempotent result without depending on mutable actor-link or signer state. The worker publishes a separate immutable Discord-delivery projection bound to the original request digest and interaction id; it exposes only bounded result/refusal fields and sealed-result fingerprints, never raw signatures or private state. VoidBot reads that outbox and owns Discord response/checkpoint state. Bifrost never manufactures success. Ordinary Persona messages remain `feedback_only` and cannot enter this path. Durable v0 admissions/results are replay-only and cannot acquire the review vocabulary. Bifrost admits an observation only when guild, channel, target runtime, canonical repository, Persona, producer, and producer runtime match a preconfigured `bifrost.persona_feedback.target_binding.v0` document in Bifrost's private provider store. That binding also owns source visibility, data classification, and the delivery-store target. Bifrost never infers public visibility from Discord identifiers. It classifies the author using its own `bifrost.discord.actor_link.v0` state: a match yields `linked_governance_feedback`; absence yields `unlinked_social_feedback`. Neither class grants adoption, work, release, or deployment authority. diff --git a/docs/epiphany-operator-boundary.md b/docs/epiphany-operator-boundary.md index 83f0651..9c18847 100644 --- a/docs/epiphany-operator-boundary.md +++ b/docs/epiphany-operator-boundary.md @@ -5,7 +5,7 @@ - Owner: Bifrost alone authenticates a Discord operator request, resolves the persisted Discord-to-Bifrost actor link, and signs an expiring Epiphany admission. Epiphany alone decides and applies the named command. -- Inputs: one strict `voidbot.discord.epiphany_operator_request.v0`, the exact +- Inputs: one strict `voidbot.discord.epiphany_operator_request.v1`, the exact configured Discord owner, a persisted Bifrost actor link, the deployment's operator signing identity, and the configured Epiphany runtime/host trust anchor. diff --git a/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs b/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs index f7f8f91..0188028 100644 --- a/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs +++ b/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs @@ -22,7 +22,7 @@ public sealed class ProviderAdvertisementTests AssertEndpoint(root, "crossing-receipts", "bifrost.crossing_receipt.v1", "bridge"); AssertEndpoint(root, "patron-support-intake", "bifrost.patron_support_event.v0", "patronage"); AssertEndpoint(root, "github-webhooks", "bifrost.work_item.v0", "github"); - AssertEndpoint(root, "epiphany-operator-requests", "voidbot.discord.epiphany_operator_request.v0", "operator"); + AssertEndpoint(root, "epiphany-operator-requests", "voidbot.discord.epiphany_operator_request.v1", "operator"); AssertEndpoint(root, "epiphany-operator-results", "epiphany.operator_command.sealed_result.v1", "epiphany"); Assert.Equal("asgard.yggdrasil.bifrost", root.GetProperty("locatedService").GetString()); AssertSchemaPurpose(root, "bifrost.crossing_receipt.v1", "canonical Bifrost-owned public crossing receipt"); diff --git a/tests/epiphany-operator-command.test.mjs b/tests/epiphany-operator-command.test.mjs index 29b77ef..109aef4 100644 --- a/tests/epiphany-operator-command.test.mjs +++ b/tests/epiphany-operator-command.test.mjs @@ -4,14 +4,14 @@ import {admissionTuple,packetTuple,parseOperatorRequest} from "../tools/epiphany import {verifyPurpose} from "../tools/bifrost-signed-identity.mjs"; const cultLibRoot=process.env.VOIDBOT_CULTLIB_ROOT||"E:/Projects/CultLib",requireCult=createRequire(resolve(cultLibRoot,"packages/cultnet-ts/package.json")),{encode}=requireCult("@msgpack/msgpack"); const actorLink={guildId:"guild",discordAuthorId:"owner",bifrostActorId:"actor",heimdallActorRef:"heimdall:actor:1"}; -function request(command={kind:"status"}){return {schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v0",requestId:"interaction-1",commandId:"interaction-1",nonce:"interaction-1",sourceEventId:"interaction-1",sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"channel",discordMessageId:"interaction-1",targetRuntimeId:"epiphany-yggdrasil",issuedAt:"2026-07-19T10:00:00.000Z",expiresAt:"2026-07-19T10:00:30.000Z",producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command};} +function request(command={kind:"status"}){return {schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v1",requestId:"interaction-1",commandId:"interaction-1",nonce:"interaction-1",sourceEventId:"interaction-1",sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"channel",discordMessageId:"interaction-1",targetRuntimeId:"epiphany-yggdrasil",issuedAt:"2026-07-19T10:00:00.000Z",expiresAt:"2026-07-19T10:00:30.000Z",producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command};} function result(admission){const value={schemaVersion:"epiphany.operator_command.result.v1",resultId:"result-1",commandId:admission.packet.commandId,packetSha256:admission.packetSha256,targetRuntimeId:admission.packet.targetRuntimeId,disposition:"observed",consequenceKind:"operator-snapshot",consequenceRef:"snapshot-1",completedAt:"2026-07-19T10:00:01.000Z",privateStateExposed:false,operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged",detail:"bounded",reviews:[],reviewCandidateId:"",reviewDecision:""},receipt={schemaVersion:"epiphany.operator_command.sealed_result.v1",result:value,resultPayloadSha256:"sha256-result",commandId:value.commandId,packetSha256:value.packetSha256,targetRuntimeId:value.targetRuntimeId,completedAt:value.completedAt,providerIdentityId:"epiphany-host",privateStateExposed:false,executorSignature:new Uint8Array(64)};return {providerIdentityId:"epiphany-host",result:value,receipt};} const transport=(execute)=>({trustedIdentityId:"epiphany-host",execute,verifyReceipt:(_admission,receipt)=>receipt}); class TestLedger{constructor(){this.requests=new Map();this.admissions=new Map();this.completions=new Map();this.terminals=new Map();}admit(id,request,value){const oldRequest=this.requests.get(id),old=this.admissions.get(id);if(oldRequest&&JSON.stringify(oldRequest)!==JSON.stringify(request)||old&&JSON.stringify(old)!==JSON.stringify(value))throw new Error("operator request identity collision");this.requests.set(id,request);this.admissions.set(id,value);}complete(_id,result,_provider,receipt){this.completions.set(result.commandId,receipt??{...result,result,providerIdentityId:"epiphany-host"});}getRequest(id){return this.requests.get(id);}getAdmission(id){return this.admissions.get(id);}getCompletion(id){return this.completions.get(id);}terminalize(value){this.terminals.set(value.requestId,value);}getTerminal(id){return this.terminals.get(id);}} async function identity(){const d=await mkdtemp(resolve(tmpdir(),"bifrost-operator-"));return enrollOperatorIdentity(resolve(d,"operator.seed"));} test("signs the exact Rust packet and admission tuples",async()=>{const id=await identity(),out=await admitOperatorRequest({request:request(),ownerDiscordId:"owner",actorLink,identity:id,encode,now:new Date("2026-07-19T10:00:01Z"),ledger:new TestLedger(),transport:transport(async a=>result(a))});assert.equal(out.admission.packetSha256,`sha256-${(await import("node:crypto")).createHash("sha256").update(encode(packetTuple(out.admission.packet))).digest("hex")}`);assert.ok(verifyPurpose(id.publicKey,OPERATOR_SIGNING_PURPOSE,encode(admissionTuple(out.admission)),out.admission.providerSignature));assert.equal(out.result.disposition,"observed");}); test("requires exact owner and admitted Bifrost actor link",async()=>{const id=await identity();for(const args of [{ownerDiscordId:"other",actorLink},{ownerDiscordId:"owner",actorLink:null}])await assert.rejects(()=>admitOperatorRequest({request:request(),identity:id,encode,now:new Date("2026-07-19T10:00:01Z"),ledger:new TestLedger(),transport:transport(async a=>result(a)),...args}));const provenanceOnly={...actorLink,heimdallActorRef:""};assert.equal((await admitOperatorRequest({request:request(),ownerDiscordId:"owner",actorLink:provenanceOnly,identity:id,encode,now:new Date("2026-07-19T10:00:01Z"),ledger:new TestLedger(),transport:transport(async a=>result(a))})).result.disposition,"observed");}); -test("strict request, ttl, command bounds, and expiry fail closed",()=>{assert.throws(()=>parseOperatorRequest({...request(),extra:true}));assert.throws(()=>parseOperatorRequest({...request(),expiresAt:"2026-07-19T10:01:01Z"}));assert.throws(()=>parseOperatorRequest(request({kind:"argv",args:["whoami"]})));assert.throws(()=>parseOperatorRequest(request({kind:"sleep",reason:""})));}); +test("strict request, ttl, command bounds, and expiry fail closed",()=>{assert.throws(()=>parseOperatorRequest({...request(),schemaVersion:"voidbot.discord.epiphany_operator_request.v0"}));assert.throws(()=>parseOperatorRequest({...request(),extra:true}));assert.throws(()=>parseOperatorRequest({...request(),expiresAt:"2026-07-19T10:01:01Z"}));assert.throws(()=>parseOperatorRequest(request({kind:"argv",args:["whoami"]})));assert.throws(()=>parseOperatorRequest(request({kind:"sleep",reason:""})));}); test("does not synthesize success without an authenticated transport result",async()=>{const id=await identity(),base={request:request(),ownerDiscordId:"owner",actorLink,identity:id,encode,now:new Date("2026-07-19T10:00:01Z"),ledger:new TestLedger()};await assert.rejects(()=>admitOperatorRequest(base),/authenticated Epiphany/);await assert.rejects(()=>admitOperatorRequest({...base,transport:transport(async a=>({...result(a),providerIdentityId:"impostor"}))}),/authenticated Epiphany/);await assert.rejects(()=>admitOperatorRequest({...base,transport:transport(async a=>({...result(a),result:{...result(a).result,packetSha256:"sha256-wrong"}}))}),/not bound/);}); test("retransmits an exact durable admission after expiry without mutable actor or signer state",async()=>{const id=await identity(),ledger=new TestLedger(),r=request(),base={request:r,ownerDiscordId:"owner",actorLink,identity:id,encode,ledger};await assert.rejects(()=>admitOperatorRequest({...base,now:new Date("2026-07-19T10:00:01Z"),transport:transport(async()=>{throw new Error("lost result")})}),/lost result/);let calls=0;const recovered=await admitOperatorRequest({...base,ownerDiscordId:"rotated-owner",actorLink:null,identity:null,now:new Date("2026-07-19T10:02:00Z"),transport:transport(async a=>{calls++;return result(a)})});assert.equal(calls,1);assert.equal(recovered.recovered,true);const fresh={...r,requestId:"interaction-2",commandId:"interaction-2",nonce:"interaction-2",sourceEventId:"interaction-2",discordMessageId:"interaction-2"};await assert.rejects(()=>admitOperatorRequest({...base,request:fresh,ledger:new TestLedger(),now:new Date("2026-07-19T10:02:00Z"),transport:transport(async a=>result(a))}),error=>error.code==="expired-new-request");}); test("does not hide command-id collisions behind an existing completion",async()=>{const id=await identity(),ledger=new TestLedger(),base={ownerDiscordId:"owner",actorLink,identity:id,encode,ledger,now:new Date("2026-07-19T10:00:01Z"),transport:transport(async a=>result(a))};await admitOperatorRequest({...base,request:request()});const collision={...request(),requestId:"interaction-alien",nonce:"alien",sourceEventId:"alien",discordMessageId:"alien"};await assert.rejects(()=>admitOperatorRequest({...base,request:collision}),error=>error.code==="command-id-collision");}); diff --git a/tests/epiphany-operator-discord-outbox.test.mjs b/tests/epiphany-operator-discord-outbox.test.mjs index 6cc74b3..11b6202 100644 --- a/tests/epiphany-operator-discord-outbox.test.mjs +++ b/tests/epiphany-operator-discord-outbox.test.mjs @@ -10,6 +10,6 @@ import { resolve } from "node:path"; import { EpiphanyOperatorDiscordOutbox } from "../tools/epiphany-operator-discord-outbox.mjs"; const cultLibRoot=process.env.VOIDBOT_CULTLIB_ROOT||"E:/Projects/CultLib",requireCache=createRequire(resolve(cultLibRoot,"packages/cultcache-ts/package.json")),requireNet=createRequire(resolve(cultLibRoot,"packages/cultnet-ts/package.json")),cache=requireCache("cultcache-ts"),{encode}=requireNet("@msgpack/msgpack"); -function request(id="request-1",command={kind:"status"}){return {schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v0",requestId:id,commandId:id,nonce:id,sourceEventId:id,sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"ops",discordMessageId:id,targetRuntimeId:"epiphany-yggdrasil",issuedAt:"2026-07-19T12:00:00Z",expiresAt:"2026-07-19T12:01:00Z",producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command};} +function request(id="request-1",command={kind:"status"}){return {schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v1",requestId:id,commandId:id,nonce:id,sourceEventId:id,sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"ops",discordMessageId:id,targetRuntimeId:"epiphany-yggdrasil",issuedAt:"2026-07-19T12:00:00Z",expiresAt:"2026-07-19T12:01:00Z",producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command};} function receipt(id="request-1"){return {schemaVersion:"epiphany.operator_command.sealed_result.v1",result:{disposition:"observed",detail:"bounded",operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged",reviews:[],reviewCandidateId:"candidate-1",reviewDecision:"hold"},resultPayloadSha256:`sha256-${"1".repeat(64)}`,providerIdentityId:"executor",executorSignature:new Uint8Array(64),completedAt:"2026-07-19T12:00:01Z",commandId:id};} test("Discord outbox publishes immutable request-bound completed and refused deliveries",async()=>{const root=await mkdtemp(resolve(tmpdir(),"operator-outbox-")),path=resolve(root,"deliveries.cc"),outbox=await EpiphanyOperatorDiscordOutbox.open(path,{cultcacheRuntime:cache,encode}),r=request("request-1",{kind:"review",mindRequestId:"mind-1",candidateId:"candidate-1",candidateSha256:"1".repeat(64),expectedModelRevision:41,expectedModelHash:"2".repeat(64),decision:"hold"}),completed=await outbox.publishCompleted(r,receipt());assert.equal(completed.deliveryId,r.requestId);assert.match(completed.requestPayloadSha256,/^sha256-[0-9a-f]{64}$/);assert.match(completed.executorSignatureSha256,/^sha256-[0-9a-f]{64}$/);assert.equal(completed.operatorStatus,"sleeping");assert.equal(completed.brakeStatus,"engaged");assert.equal(completed.schemaVersion,"bifrost.discord.epiphany_operator_delivery.v1");assert.equal(completed.commandId,r.commandId);assert.deepEqual(completed.reviews,[]);assert.equal(completed.reviewCandidateId,"candidate-1");assert.equal(completed.reviewDecision,"hold");assert.equal(outbox.isExactDelivery(r),true);assert.equal(outbox.isExactDelivery({...r,discordChannelId:"alien"}),false);assert.equal(completed.privateStateExposed,false);await outbox.publishCompleted(r,receipt());await assert.rejects(()=>outbox.publishCompleted({...r,command:{...r.command,candidateId:"alien"}},receipt()),/does not bind/);await assert.rejects(()=>outbox.publishCompleted(r,{...receipt(),result:{...receipt().result,disposition:"applied",detail:"changed"}}),/collision/);const refusedRequest=request("request-2"),refused=await outbox.publishRefused(refusedRequest,{failureCode:"foreign-owner",recordedAt:"2026-07-19T12:00:02Z"});assert.equal(refused.status,"refused");assert.equal(refused.failureCode,"foreign-owner");assert.equal(refused.sealedResultPayloadSha256,"");assert.deepEqual(refused.reviews,[]);assert.equal(refused.reviewCandidateId,"");assert.equal(refused.reviewDecision,"");const reopened=await EpiphanyOperatorDiscordOutbox.open(path,{cultcacheRuntime:cache,encode});assert.equal(reopened.get(r.requestId).status,"completed");assert.equal(reopened.get(refusedRequest.requestId).status,"refused");}); diff --git a/tests/epiphany-operator-ledger.test.mjs b/tests/epiphany-operator-ledger.test.mjs index 1aed71f..be7b739 100644 --- a/tests/epiphany-operator-ledger.test.mjs +++ b/tests/epiphany-operator-ledger.test.mjs @@ -9,4 +9,4 @@ import { DurableOperatorLedger } from "../tools/epiphany-operator-ledger.mjs"; const cultLibRoot=process.env.VOIDBOT_CULTLIB_ROOT||"E:/Projects/CultLib",requireCult=createRequire(resolve(cultLibRoot,"packages/cultcache-ts/package.json")),runtime=requireCult("cultcache-ts"); function admission(){return {schemaName:"bifrost.operator_command.delivery",schemaVersion:"bifrost.operator_command.delivery.v0",admissionId:"admission-1",packet:{commandId:"command-1",nonce:"nonce-1",sourceEventId:"event-1",sourceActorId:"actor",discordGuildId:"guild",discordChannelId:"ops",discordMessageId:"message-1",targetRuntimeId:"epiphany-yggdrasil",issuedAt:"2026-07-19T12:00:00Z",expiresAt:"2026-07-19T12:01:00Z",command:{kind:"status"}},packetSha256:"sha256-packet",sourceObserverId:"voidbot",sourceObserverRuntimeId:"voidbot-yggdrasil",provider:"bifrost",bifrostAdmissionReceiptId:"receipt-1",authority:"exact_operator_command_only",providerIdentityId:"bifrost",providerSignature:new Uint8Array(64)};} function receipt(){const result={schemaVersion:"epiphany.operator_command.result.v0",resultId:"result-1",commandId:"command-1",packetSha256:"sha256-packet",targetRuntimeId:"epiphany-yggdrasil",disposition:"observed",consequenceKind:"operator-snapshot",consequenceRef:"snapshot-1",completedAt:"2026-07-19T12:00:01Z",privateStateExposed:false,operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged",detail:"bounded"};return {schemaVersion:"epiphany.operator_command.sealed_result.v0",result,resultPayloadSha256:"sha256-result",commandId:result.commandId,packetSha256:result.packetSha256,targetRuntimeId:result.targetRuntimeId,completedAt:result.completedAt,providerIdentityId:"epiphany",privateStateExposed:false,executorSignature:new Uint8Array(64)};} -test("durable operator ledger survives restart and refuses admission/result collisions",async()=>{const root=await mkdtemp(resolve(tmpdir(),"operator-ledger-")),path=resolve(root,"ledger.cc"),first=await DurableOperatorLedger.open(path,{cultcacheRuntime:runtime}),a=admission(),request={schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v0",requestId:a.admissionId,commandId:a.packet.commandId,nonce:a.packet.nonce,sourceEventId:a.packet.sourceEventId,sourceActorDiscordId:"owner",discordGuildId:a.packet.discordGuildId,discordChannelId:a.packet.discordChannelId,discordMessageId:a.packet.discordMessageId,targetRuntimeId:a.packet.targetRuntimeId,issuedAt:a.packet.issuedAt,expiresAt:a.packet.expiresAt,producerId:"voidbot",producerRuntimeId:a.sourceObserverRuntimeId,authorityClass:"operator_request_only",status:"pending",command:a.packet.command},r=receipt(),terminal={schemaVersion:"bifrost.operator_command.terminal.v0",requestId:"rejected-1",commandId:"rejected-1",status:"refused",failureCode:"foreign-owner",recordedAt:"2026-07-19T12:00:00Z",retryable:false,privateStateExposed:false};await first.admit(a.admissionId,request,a);await first.complete(a.admissionId,r.result,r.providerIdentityId,r);await first.terminalize(terminal);const reopened=await DurableOperatorLedger.open(path,{cultcacheRuntime:runtime});assert.equal(reopened.getRequest(a.admissionId).sourceActorDiscordId,"owner");assert.equal(reopened.getAdmission(a.admissionId).packetSha256,a.packetSha256);assert.equal(reopened.getCompletion(r.commandId).result.resultId,r.result.resultId);assert.equal(reopened.getTerminal(terminal.requestId).failureCode,"foreign-owner");await reopened.admit(a.admissionId,request,a);await reopened.complete(a.admissionId,r.result,r.providerIdentityId,r);await reopened.terminalize(terminal);await assert.rejects(()=>reopened.admit(a.admissionId,{...request,sourceActorDiscordId:"alien"},a),/collision/);await assert.rejects(()=>reopened.admit(a.admissionId,request,{...a,packetSha256:"sha256-alien"}),/collision/);await assert.rejects(()=>reopened.complete(a.admissionId,{...r.result,detail:"alien"},r.providerIdentityId,{...r,result:{...r.result,detail:"alien"}}),/collision/);await assert.rejects(()=>reopened.complete(a.admissionId,{...r.result,packetSha256:"sha256-alien"},r.providerIdentityId,{...r,packetSha256:"sha256-alien",result:{...r.result,packetSha256:"sha256-alien"}}),/exact durable admission/);await assert.rejects(()=>reopened.terminalize({...terminal,failureCode:"alien"}),/collision/);}); +test("durable operator ledger survives restart and refuses admission/result collisions",async()=>{const root=await mkdtemp(resolve(tmpdir(),"operator-ledger-")),path=resolve(root,"ledger.cc"),first=await DurableOperatorLedger.open(path,{cultcacheRuntime:runtime}),a=admission(),request={schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v1",requestId:a.admissionId,commandId:a.packet.commandId,nonce:a.packet.nonce,sourceEventId:a.packet.sourceEventId,sourceActorDiscordId:"owner",discordGuildId:a.packet.discordGuildId,discordChannelId:a.packet.discordChannelId,discordMessageId:a.packet.discordMessageId,targetRuntimeId:a.packet.targetRuntimeId,issuedAt:a.packet.issuedAt,expiresAt:a.packet.expiresAt,producerId:"voidbot",producerRuntimeId:a.sourceObserverRuntimeId,authorityClass:"operator_request_only",status:"pending",command:a.packet.command},r=receipt(),terminal={schemaVersion:"bifrost.operator_command.terminal.v0",requestId:"rejected-1",commandId:"rejected-1",status:"refused",failureCode:"foreign-owner",recordedAt:"2026-07-19T12:00:00Z",retryable:false,privateStateExposed:false};await first.admit(a.admissionId,request,a);await first.complete(a.admissionId,r.result,r.providerIdentityId,r);await first.terminalize(terminal);const reopened=await DurableOperatorLedger.open(path,{cultcacheRuntime:runtime});assert.equal(reopened.getRequest(a.admissionId).sourceActorDiscordId,"owner");assert.equal(reopened.getAdmission(a.admissionId).packetSha256,a.packetSha256);assert.equal(reopened.getCompletion(r.commandId).result.resultId,r.result.resultId);assert.equal(reopened.getTerminal(terminal.requestId).failureCode,"foreign-owner");await reopened.admit(a.admissionId,request,a);await reopened.complete(a.admissionId,r.result,r.providerIdentityId,r);await reopened.terminalize(terminal);await assert.rejects(()=>reopened.admit(a.admissionId,{...request,sourceActorDiscordId:"alien"},a),/collision/);await assert.rejects(()=>reopened.admit(a.admissionId,request,{...a,packetSha256:"sha256-alien"}),/collision/);await assert.rejects(()=>reopened.complete(a.admissionId,{...r.result,detail:"alien"},r.providerIdentityId,{...r,result:{...r.result,detail:"alien"}}),/collision/);await assert.rejects(()=>reopened.complete(a.admissionId,{...r.result,packetSha256:"sha256-alien"},r.providerIdentityId,{...r,packetSha256:"sha256-alien",result:{...r.result,packetSha256:"sha256-alien"}}),/exact durable admission/);await assert.rejects(()=>reopened.terminalize({...terminal,failureCode:"alien"}),/collision/);}); diff --git a/tests/epiphany-operator-v1.test.mjs b/tests/epiphany-operator-v1.test.mjs index 9410f45..f803c24 100644 --- a/tests/epiphany-operator-v1.test.mjs +++ b/tests/epiphany-operator-v1.test.mjs @@ -23,7 +23,7 @@ const review = { function request(command) { return { schemaName: "voidbot.discord.epiphany_operator_request", - schemaVersion: "voidbot.discord.epiphany_operator_request.v0", + schemaVersion: "voidbot.discord.epiphany_operator_request.v1", requestId: "request-1", commandId: "command-1", nonce: "nonce-1", diff --git a/tests/epiphany-operator-worker.test.mjs b/tests/epiphany-operator-worker.test.mjs index ad16be9..8ed7a79 100644 --- a/tests/epiphany-operator-worker.test.mjs +++ b/tests/epiphany-operator-worker.test.mjs @@ -18,5 +18,5 @@ import { EpiphanyOperatorDiscordOutbox } from "../tools/epiphany-operator-discor const cultLibRoot=process.env.VOIDBOT_CULTLIB_ROOT||"E:/Projects/CultLib",requireCache=createRequire(resolve(cultLibRoot,"packages/cultcache-ts/package.json")),requireNet=createRequire(resolve(cultLibRoot,"packages/cultnet-ts/package.json")),cache=requireCache("cultcache-ts"),cultnet=requireNet("cultnet-ts"),msgpack=requireNet("@msgpack/msgpack"); test("operator identity enrollment emits only a compact public trust anchor",async()=>{const root=await mkdtemp(resolve(tmpdir(),"operator-enroll-")),key=resolve(root,"private","operator.seed"),anchor=resolve(root,"public","operator.msgpack"),run=spawn(process.execPath,[resolve("tools/epiphany-operator-worker.mjs"),"enroll-identity","--private-key",key,"--trust-anchor-out",anchor],{cwd:resolve("."),stdio:["ignore","pipe","pipe"]}),output=await collect(run);assert.equal(output.code,0,output.stderr);const pulse=JSON.parse(output.stdout.trim()),decoded=msgpack.decode(await readFile(anchor));assert.equal(pulse.schemaVersion,"bifrost.epiphany_operator_identity_enrollment.v0");assert.equal(pulse.privateStateExposed,false);assert.equal(decoded[0],"epiphany.host_identity_trust_anchor.v0");assert.equal(decoded.length,6);if(process.platform!=="win32"){assert.equal((await stat(key)).mode&0o777,0o600);assert.equal((await stat(anchor)).mode&0o777,0o640);}}); -test("supervised worker consumes the typed inbox and durably records the sealed result",async()=>{const root=await mkdtemp(resolve(tmpdir(),"operator-worker-")),requestPath=resolve(root,"requests.cc"),privatePath=resolve(root,"private.cc"),ledgerPath=resolve(root,"ledger.cc"),deliveryPath=resolve(root,"deliveries.cc"),keyPath=resolve(root,"bifrost.seed"),anchorPath=resolve(root,"executor.msgpack"),defs=buildOperatorDefinitions(cache.defineDocumentType),feedback=buildFeedbackDefinitions(cache.defineDocumentType),requests=cache.CultCache.builder().withDocumentType(defs.request).withGenericStore(new cache.SingleFileMessagePackBackingStore(requestPath)).build(),privateCache=cache.CultCache.builder().withDocumentType(feedback.actorLink).withGenericStore(new cache.SingleFileMessagePackBackingStore(privatePath)).build(),issued=new Date(),expires=new Date(issued.getTime()+60000),request={schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v0",requestId:"interaction-1",commandId:"interaction-1",nonce:"interaction-1",sourceEventId:"interaction-1",sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"ops",discordMessageId:"interaction-1",targetRuntimeId:"epiphany-yggdrasil",issuedAt:issued.toISOString(),expiresAt:expires.toISOString(),producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command:{kind:"status"}};await requests.put(defs.request,request.requestId,request);await privateCache.put(feedback.actorLink,"guild:owner",{schemaName:"bifrost.discord.actor_link",schemaVersion:"bifrost.discord.actor_link.v0",linkId:"guild:owner",guildId:"guild",discordAuthorId:"owner",bifrostActorId:"actor",heimdallActorRef:"heimdall:actor:1",linkedAt:issued.toISOString()});await enrollOperatorIdentity(keyPath);const executor=await enrollOperatorIdentity(resolve(root,"executor.seed")),anchor=trustAnchor(executor,issued.toISOString());await writeFile(anchorPath,msgpack.encode([anchor.schemaVersion,anchor.identityId,anchor.publicKey,anchor.assurance,anchor.identityCreatedAt,anchor.sourceIdentityRecordSha256]));const socket=createSocket("udp4");await new Promise((ok,fail)=>{socket.once("error",fail);socket.bind(0,"127.0.0.1",ok)});const server=new cultnet.CultNetRudpSocketTransportConnection({runtimeId:request.targetRuntimeId,socket,mode:"server",connectionId:0xe91f0001,resendDelayMs:25,resendPollMs:5});server.on("frame",frame=>{const wire=msgpack.decode(frame.payload),admission=msgpack.decode(wire.document.payload),completedAt=new Date(issued.getTime()+1000).toISOString(),result={schemaVersion:"epiphany.operator_command.result.v1",resultId:"result-1",commandId:admission.packet.commandId,packetSha256:admission.packetSha256,targetRuntimeId:admission.packet.targetRuntimeId,disposition:"observed",consequenceKind:"operator-snapshot",consequenceRef:"snapshot-1",completedAt,privateStateExposed:false,operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged",detail:"bounded",reviews:[],reviewCandidateId:"",reviewDecision:""},receipt={schemaVersion:"epiphany.operator_command.sealed_result.v1",result,resultPayloadSha256:`sha256-${createHash("sha256").update(msgpack.encode(result)).digest("hex")}`,commandId:result.commandId,packetSha256:result.packetSha256,targetRuntimeId:result.targetRuntimeId,completedAt,providerIdentityId:executor.identityId,privateStateExposed:false,executorSignature:new Uint8Array()};receipt.executorSignature=new Uint8Array(signPurpose(executor,EPIPHANY_RESULT_SIGNING_PURPOSE,msgpack.encode(resultReceiptTuple(receipt))));server.send("schema",msgpack.encode({schemaVersion:"cultnet.document_put_raw.v0",messageId:"result-1",document:{schemaId:"epiphany.operator_command.sealed_result",recordKey:receipt.commandId,storedAt:receipt.completedAt,payloadEncoding:"messagepack",payload:msgpack.encode(receipt),sourceRuntimeId:receipt.targetRuntimeId,sourceAgentId:receipt.providerIdentityId,sourceRole:"epiphany-operator-command-executor",tags:["cultnet.transport.rudp.v0"]}}));});try{const port=socket.address().port,run=spawn(process.execPath,[resolve("tools/epiphany-operator-worker.mjs"),"once","--request-store",requestPath,"--private-store",privatePath,"--ledger-store",ledgerPath,"--delivery-store",deliveryPath,"--private-key",keyPath,"--epiphany-trust-anchor",anchorPath,"--epiphany-rudp",`rudp://127.0.0.1:${port}`,"--owner-discord-id","owner"],{cwd:resolve("."),stdio:["ignore","pipe","pipe"]}),output=await collect(run);assert.equal(output.code,0,output.stderr);const pulse=JSON.parse(output.stdout.trim());assert.equal(pulse.status,"completed");const ledger=await DurableOperatorLedger.open(ledgerPath,{cultcacheRuntime:cache}),outbox=await EpiphanyOperatorDiscordOutbox.open(deliveryPath,{cultcacheRuntime:cache,encode:msgpack.encode});assert.equal(ledger.getCompletion(request.commandId).providerIdentityId,executor.identityId);const delivery=outbox.get(request.requestId);assert.equal(delivery.status,"completed");assert.equal(delivery.discordInteractionId,request.sourceEventId);assert.match(delivery.requestPayloadSha256,/^sha256-[0-9a-f]{64}$/);assert.equal(delivery.privateStateExposed,false);}finally{server.close();}}); +test("supervised worker consumes the typed inbox and durably records the sealed result",async()=>{const root=await mkdtemp(resolve(tmpdir(),"operator-worker-")),requestPath=resolve(root,"requests.cc"),privatePath=resolve(root,"private.cc"),ledgerPath=resolve(root,"ledger.cc"),deliveryPath=resolve(root,"deliveries.cc"),keyPath=resolve(root,"bifrost.seed"),anchorPath=resolve(root,"executor.msgpack"),defs=buildOperatorDefinitions(cache.defineDocumentType),feedback=buildFeedbackDefinitions(cache.defineDocumentType),requests=cache.CultCache.builder().withDocumentType(defs.request).withGenericStore(new cache.SingleFileMessagePackBackingStore(requestPath)).build(),privateCache=cache.CultCache.builder().withDocumentType(feedback.actorLink).withGenericStore(new cache.SingleFileMessagePackBackingStore(privatePath)).build(),issued=new Date(),expires=new Date(issued.getTime()+60000),request={schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v1",requestId:"interaction-1",commandId:"interaction-1",nonce:"interaction-1",sourceEventId:"interaction-1",sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"ops",discordMessageId:"interaction-1",targetRuntimeId:"epiphany-yggdrasil",issuedAt:issued.toISOString(),expiresAt:expires.toISOString(),producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command:{kind:"status"}};await requests.put(defs.request,request.requestId,request);await privateCache.put(feedback.actorLink,"guild:owner",{schemaName:"bifrost.discord.actor_link",schemaVersion:"bifrost.discord.actor_link.v0",linkId:"guild:owner",guildId:"guild",discordAuthorId:"owner",bifrostActorId:"actor",heimdallActorRef:"heimdall:actor:1",linkedAt:issued.toISOString()});await enrollOperatorIdentity(keyPath);const executor=await enrollOperatorIdentity(resolve(root,"executor.seed")),anchor=trustAnchor(executor,issued.toISOString());await writeFile(anchorPath,msgpack.encode([anchor.schemaVersion,anchor.identityId,anchor.publicKey,anchor.assurance,anchor.identityCreatedAt,anchor.sourceIdentityRecordSha256]));const socket=createSocket("udp4");await new Promise((ok,fail)=>{socket.once("error",fail);socket.bind(0,"127.0.0.1",ok)});const server=new cultnet.CultNetRudpSocketTransportConnection({runtimeId:request.targetRuntimeId,socket,mode:"server",connectionId:0xe91f0001,resendDelayMs:25,resendPollMs:5});server.on("frame",frame=>{const wire=msgpack.decode(frame.payload),admission=msgpack.decode(wire.document.payload),completedAt=new Date(issued.getTime()+1000).toISOString(),result={schemaVersion:"epiphany.operator_command.result.v1",resultId:"result-1",commandId:admission.packet.commandId,packetSha256:admission.packetSha256,targetRuntimeId:admission.packet.targetRuntimeId,disposition:"observed",consequenceKind:"operator-snapshot",consequenceRef:"snapshot-1",completedAt,privateStateExposed:false,operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged",detail:"bounded",reviews:[],reviewCandidateId:"",reviewDecision:""},receipt={schemaVersion:"epiphany.operator_command.sealed_result.v1",result,resultPayloadSha256:`sha256-${createHash("sha256").update(msgpack.encode(result)).digest("hex")}`,commandId:result.commandId,packetSha256:result.packetSha256,targetRuntimeId:result.targetRuntimeId,completedAt,providerIdentityId:executor.identityId,privateStateExposed:false,executorSignature:new Uint8Array()};receipt.executorSignature=new Uint8Array(signPurpose(executor,EPIPHANY_RESULT_SIGNING_PURPOSE,msgpack.encode(resultReceiptTuple(receipt))));server.send("schema",msgpack.encode({schemaVersion:"cultnet.document_put_raw.v0",messageId:"result-1",document:{schemaId:"epiphany.operator_command.sealed_result",recordKey:receipt.commandId,storedAt:receipt.completedAt,payloadEncoding:"messagepack",payload:msgpack.encode(receipt),sourceRuntimeId:receipt.targetRuntimeId,sourceAgentId:receipt.providerIdentityId,sourceRole:"epiphany-operator-command-executor",tags:["cultnet.transport.rudp.v0"]}}));});try{const port=socket.address().port,run=spawn(process.execPath,[resolve("tools/epiphany-operator-worker.mjs"),"once","--request-store",requestPath,"--private-store",privatePath,"--ledger-store",ledgerPath,"--delivery-store",deliveryPath,"--private-key",keyPath,"--epiphany-trust-anchor",anchorPath,"--epiphany-rudp",`rudp://127.0.0.1:${port}`,"--owner-discord-id","owner"],{cwd:resolve("."),stdio:["ignore","pipe","pipe"]}),output=await collect(run);assert.equal(output.code,0,output.stderr);const pulse=JSON.parse(output.stdout.trim());assert.equal(pulse.status,"completed");const ledger=await DurableOperatorLedger.open(ledgerPath,{cultcacheRuntime:cache}),outbox=await EpiphanyOperatorDiscordOutbox.open(deliveryPath,{cultcacheRuntime:cache,encode:msgpack.encode});assert.equal(ledger.getCompletion(request.commandId).providerIdentityId,executor.identityId);const delivery=outbox.get(request.requestId);assert.equal(delivery.status,"completed");assert.equal(delivery.discordInteractionId,request.sourceEventId);assert.match(delivery.requestPayloadSha256,/^sha256-[0-9a-f]{64}$/);assert.equal(delivery.privateStateExposed,false);}finally{server.close();}}); function collect(child){return new Promise(resolveExit=>{let stdout="",stderr="";child.stdout.on("data",x=>stdout+=x);child.stderr.on("data",x=>stderr+=x);child.on("exit",code=>resolveExit({code,stdout,stderr}));});} diff --git a/tools/epiphany-operator-command-documents.mjs b/tools/epiphany-operator-command-documents.mjs index 8d80e88..638cd2d 100644 --- a/tools/epiphany-operator-command-documents.mjs +++ b/tools/epiphany-operator-command-documents.mjs @@ -1,5 +1,5 @@ export const operatorRequestType="voidbot.discord.epiphany_operator_request"; -export const operatorRequestSchema="voidbot.discord.epiphany_operator_request.v0"; +export const operatorRequestSchema="voidbot.discord.epiphany_operator_request.v1"; export const operatorAdmissionType="bifrost.operator_command.delivery"; export const operatorAdmissionSchema="bifrost.operator_command.delivery.v1"; export const legacyOperatorAdmissionSchema="bifrost.operator_command.delivery.v0"; diff --git a/tools/provider-advertisement.mjs b/tools/provider-advertisement.mjs index 9ef52f6..5dd4465 100644 --- a/tools/provider-advertisement.mjs +++ b/tools/provider-advertisement.mjs @@ -164,7 +164,7 @@ const canonicalService = `${rootVerse}.bifrost`; const locatedService = `${rootVerse}.${currentMachine}.bifrost`; const motionSurfaceCultMeshUri = `cultmesh://${locatedService}/eve/governance/surface`; const motionCommandCultMeshUri = `cultmesh://${locatedService}/commands/motion`; -const epiphanyOperatorRequestSchemaId = "voidbot.discord.epiphany_operator_request.v0"; +const epiphanyOperatorRequestSchemaId = "voidbot.discord.epiphany_operator_request.v1"; const epiphanyOperatorAdmissionSchemaId = "bifrost.operator_command.delivery.v1"; const epiphanyOperatorResultSchemaId = "epiphany.operator_command.result.v1"; const epiphanyOperatorSealedResultSchemaId = "epiphany.operator_command.sealed_result.v1"; @@ -506,7 +506,7 @@ function buildAdvertisement(options) { "does not touch secrets in this advertisement", ]), boundary("epiphany-operator", "Bifrost admission; Epiphany execution", [ - "admit exact owner-only status, sleep, wake, and directive requests", + "admit exact owner-only status, sleep, wake, directive, reviews, and review requests", "require the exact configured Discord owner and persisted Bifrost actor-link mapping; Heimdall reference is provenance until signed claims are live", "sign Rust-compatible expiring operator admissions with a dedicated Ed25519 identity", "relay only Epiphany-origin results bound to the admitted command",