Own native Persona Discord transport

This commit is contained in:
Meta Vens 2026-07-22 00:34:04 +01:00
commit 548dd9397f
11 changed files with 430 additions and 24 deletions

View file

@ -194,7 +194,17 @@ The command is a dry-run shape probe for public surfaces that are real enough to
### Organizational Persona Feedback Ingress
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.
Bifrost's native Discord gateway is the conversation observer. A direct mention of the Bifrost bot, or a reply to one of its messages, becomes an immutable typed `bifrost.discord.persona_conversation_event.v0` only when the guild and channel have one exact Persona target binding. Bot-authored, ambient, empty, over-1200-character, unbound, and ambiguously bound messages do not enter the conversation path. The 1200-character cap is the shared signed alpha boundary with Epiphany. The observation store is Bifrost-private working state, never an operator-command or actuator queue, and insert collisions must contain exactly equal content. VoidBot does not observe, schedule, or answer for the target Persona.
Persona replies cross back through two explicit CultMesh surfaces. Epiphany is
the sole writer of `epiphany.persona_discord_delivery_request.v0`; Bifrost
reads that surface, authenticates the dedicated Persona-mouth signer, journals
external execution before touching Discord, and is the sole writer of
`bifrost.persona_discord_delivery_receipt.v0`. Epiphany reads receipts but
never receives the Discord token or write access to Bifrost private state.
`unknown` is terminal for automatic transport retry: an external post may have
happened, so restart recovery must not manufacture a duplicate. Failed and
unknown outcomes remain Persona pressure for explicit resolution.
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.
@ -208,7 +218,7 @@ The public trust-anchor artifact is a Rust-compatible six-element MessagePack tu
The same seed also exposes a separately domain-derived `gamecult.provider_health_identity.trust_anchor.v1` through `export-provider-health-anchor` (or `--provider-health-trust-anchor-out` during first enrollment). This works with the already-enrolled service key and creates the public artifact without replacing an existing file. Persona-feedback health uses that provider-health identity, not the feedback host identity. One `serve` process owns one UUID incarnation and a strictly increasing sequence. Each pulse is the exact 17-field `idunn.signed_daemon_health.v1` tuple, signed over its empty-signature canonical MessagePack form with the `gamecult.provider-health.signature.v1` domain and `idunn.signed_daemon_health.v1` purpose. It carries only the closed provider state and a bounded generic reason. The temporary `idunn.daemon_health` dual publication is explicitly tagged and sourced as an unsigned diagnostic; it cannot substitute for signed admission and can be removed after the deployed Idunn trust binding is live.
The signed `bifrost.persona_feedback.delivery.v0` document matches Epiphany's `BifrostPersonaFeedbackAdmission`: its packet digest is `sha256-` plus SHA-256 of the Rust field-order MessagePack packet tuple, and Ed25519 signs the Epiphany host-identity domain message for purpose `bifrost.persona-feedback.delivery.v0` over the exact admission tuple. The observation provenance is `sourceObserverId=voidbot`; the admission provider is separately and explicitly `provider=bifrost`. Authority is `feedback_only`, and the canonical Epiphany repository target is `GameCult/Epiphany`.
The signed `bifrost.persona_feedback.delivery.v0` document matches Epiphany's `BifrostPersonaFeedbackAdmission`: its packet digest is `sha256-` plus SHA-256 of the Rust field-order MessagePack packet tuple, and Ed25519 signs the Epiphany host-identity domain message for purpose `bifrost.persona-feedback.delivery.v0` over the exact admission tuple. The observation provenance is `sourceObserverId=bifrost-discord`; the admission provider is separately and explicitly `provider=bifrost`. Authority is `feedback_only`, and the canonical Epiphany repository target is `GameCult/Epiphany`.
Each binding selects a dedicated provider-owned CultCache store containing only signed delivery documents. The canonical Epiphany path is `/srv/bifrost/persona-feedback/deliveries.cc`; Windows development defaults to `.bifrost/epiphany-persona-feedback.cc`. Observation inbox, Bifrost-private provider state, and delivery store must have distinct canonical paths and distinct existing file identities. Publication is crash-replayable, not cross-store transactional: the signed delivery is inserted idempotently first, then Bifrost records its private delivery copy and immutable receipt. A crash between those writes leaves the source observation unchanged and eligible for replay. Epiphany may consume the delivery store read-only without gaining access to bindings, actor links, receipts, or signing state. CultNet snapshot export remains available.

View file

@ -0,0 +1,25 @@
import test from "node:test";
import assert from "node:assert/strict";
import {spawnSync} from "node:child_process";
import {chmod,mkdtemp,writeFile} from "node:fs/promises";
import {tmpdir} from "node:os";
import {resolve} from "node:path";
import {createRequire} from "node:module";
import {enrollDomainIdentity} from "../tools/bifrost-signed-identity.mjs";
import {receiptIdentityDomain} from "../tools/persona-discord-delivery-documents.mjs";
import {processPersonaDiscordDeliveries} from "../tools/persona-discord-delivery.mjs";
const epiphany=resolve(import.meta.dirname,"..","..","EpiphanyAgent"),cult=resolve(import.meta.dirname,"..","..","CultLib");
function runtime(){const cr=createRequire(resolve(cult,"packages","cultcache-ts","package.json")),nr=createRequire(resolve(cult,"packages","cultnet-ts","package.json")),cc=cr("cultcache-ts"),mp=nr("@msgpack/msgpack");return {CultCache:cc.CultCache,SingleFileMessagePackBackingStore:cc.SingleFileMessagePackBackingStore,defineDocumentType:cc.defineDocumentType,encode:mp.encode,decode:mp.decode};}
test("Bifrost consumes a Rust-authored request store through its read-only crossing directory",async()=>{
const root=await mkdtemp(resolve(tmpdir(),"rust-bifrost-crossing-")),requestDir=resolve(root,"request"),receiptDir=resolve(root,"receipt"),requestStore=resolve(requestDir,"requests.cc"),anchor=resolve(root,"mouth-anchor.msgpack"),identity=resolve(root,"mouth.cc"),deliveryKey=resolve(root,"delivery.seed"),bridge=resolve(root,"bridge.mjs");
const cargo=spawnSync("cargo",["run","--quiet","--manifest-path",resolve(epiphany,"epiphany-core","Cargo.toml"),"--bin","epiphany-persona-discord-crossing-fixture","--","--request-store",requestStore,"--identity-store",identity,"--request-anchor",anchor],{cwd:epiphany,encoding:"utf8",timeout:180000,windowsHide:true});
assert.equal(cargo.status,0,cargo.stderr||cargo.stdout);
await chmod(requestStore,0o444);
await enrollDomainIdentity(deliveryKey,"delivery",receiptIdentityDomain);
await writeFile(bridge,`console.log(JSON.stringify({action:"discord-post",ok:true,channelId:"123",messageId:"rust-smoke-message",transport:"smoke",crossingReceiptId:"rust-smoke-receipt",url:"https://discord.invalid/rust-smoke"}))`);
const r=runtime(),results=await processPersonaDiscordDeliveries({runtime:r,requestStorePath:requestStore,receiptStorePath:resolve(receiptDir,"receipts.cc"),executionStorePath:resolve(root,"private","executions.cc"),epiphanyTrustAnchorPath:anchor,privateKeyPath:deliveryKey,targetAdmitted:()=>true,requestPermit:async({requestPayloadSha256})=>({permitId:`permit:${requestPayloadSha256}`,expiresAt:new Date(Date.now()+5000).toISOString()}),bridgeCliPath:bridge,bifrostRoot:root});
assert.equal(results[0]?.status,"completed");
assert.equal(results[0]?.requestId,"rust-crossing-smoke-1");
});

View file

@ -0,0 +1,32 @@
import test from "node:test";
import assert from "node:assert/strict";
import {createHash} from "node:crypto";
import {createRequire} from "node:module";
import {mkdtemp,writeFile} from "node:fs/promises";
import {tmpdir} from "node:os";
import {resolve} from "node:path";
import {buildPersonaDiscordDeliveryDefinitions,executionSchema,parseReceipt,parseRequest,receiptIdentityDomain,receiptSignatureDomain,receiptSigningPurpose,receiptSigningTuple,requestIdentityDomain,requestSignatureDomain,requestSigningPurpose,requestSigningTuple} from "../tools/persona-discord-delivery-documents.mjs";
import {processPersonaDiscordDeliveries} from "../tools/persona-discord-delivery.mjs";
import {enrollDomainIdentity,signDomainPurpose,verifyDomainPurpose} from "../tools/bifrost-signed-identity.mjs";
const signature=()=>new Uint8Array(64);
const request=()=>({schemaVersion:"epiphany.persona_discord_delivery_request.v0",requestId:"request-1",effectDocumentId:"effect-1",targetRuntimeId:"epiphany-yggdrasil",personaAgentId:"epiphany.Persona",channelId:"123",replyToMessageId:"456",content:"hello",contentSha256:`sha256-${"a".repeat(64)}`,issuedAt:"2026-07-21T12:00:00Z",expiresAt:"2026-07-21T12:01:00Z",privateStateExposed:false,signerIdentityId:"signer",signerSignature:signature()});
test("Persona delivery request has one exact signature tuple",()=>{const value=parseRequest(request());assert.deepEqual(requestSigningTuple(value),["epiphany.persona_discord_delivery_request.v0","request-1","effect-1","epiphany-yggdrasil","epiphany.Persona","123","456","hello",`sha256-${"a".repeat(64)}`,"2026-07-21T12:00:00Z","2026-07-21T12:01:00Z",false,"signer"]);});
test("Persona delivery receipt terminal states cannot counterfeit delivery",()=>{const base={schemaVersion:"bifrost.persona_discord_delivery_receipt.v0",receiptId:"request-1",requestId:"request-1",requestPayloadSha256:`sha256-${"b".repeat(64)}`,status:"unknown",channelId:"123",replyToMessageId:"456",messageId:"",transport:"",crossingReceiptId:"",receiptUrl:"",completedAt:"2026-07-21T12:00:02Z",providerIdentityId:"bifrost",privateStateExposed:false,providerSignature:signature()};assert.equal(receiptSigningTuple(parseReceipt(base)).length,14);assert.throws(()=>parseReceipt({...base,messageId:"counterfeit"}),/may not claim delivery evidence/);assert.doesNotThrow(()=>parseReceipt({...base,status:"failed"}));assert.doesNotThrow(()=>parseReceipt({...base,status:"completed",messageId:"789",transport:"bifrost.discord-post",crossingReceiptId:"crossing-1",receiptUrl:"https://discord.com/channels/g/123/789"}));});
test("Persona delivery documents refuse privacy and identity substitutions",()=>{assert.throws(()=>parseRequest({...request(),privateStateExposed:true}),/must be false/);const receipt={schemaVersion:"bifrost.persona_discord_delivery_receipt.v0",receiptId:"receipt-2",requestId:"request-1",requestPayloadSha256:`sha256-${"b".repeat(64)}`,status:"failed",channelId:"123",replyToMessageId:"",messageId:"",transport:"",crossingReceiptId:"",receiptUrl:"",completedAt:"2026-07-21T12:00:02Z",providerIdentityId:"bifrost",privateStateExposed:false,providerSignature:signature()};assert.throws(()=>parseReceipt(receipt),/must equal requestId/);});
test("Bifrost authenticates one request, publishes one signed receipt, and replays without reposting",async()=>{const root=await mkdtemp(resolve(tmpdir(),"persona-mouth-")),runtime=loadRuntime(),defs=buildPersonaDiscordDeliveryDefinitions(runtime.defineDocumentType),requestStore=resolve(root,"requests.cc"),receiptStore=resolve(root,"receipts.cc"),executionStore=resolve(root,"private","executions.cc"),requestKey=resolve(root,"request.seed"),deliveryKey=resolve(root,"delivery.seed"),anchorPath=resolve(root,"request-anchor.msgpack"),counterPath=resolve(root,"post-count"),bridgePath=resolve(root,"fake-bridge.mjs"),requestIdentity=await enrollDomainIdentity(requestKey,"request identity",requestIdentityDomain),deliveryIdentity=await enrollDomainIdentity(deliveryKey,"delivery identity",receiptIdentityDomain);await writeFile(anchorPath,runtime.encode(requestAnchor(requestIdentity)));await writeFile(bridgePath,`import{readFileSync,writeFileSync}from'node:fs';const p=${JSON.stringify(counterPath)};let n=0;try{n=Number(readFileSync(p,'utf8'))}catch{}writeFileSync(p,String(n+1));console.log(JSON.stringify({action:'discord-post',ok:true,channelId:'123',messageId:'789',transport:'bifrost.discord-post',crossingReceiptId:'crossing-1',url:'https://discord.com/channels/g/123/789'}));`);const now=Date.now(),value={...request(),issuedAt:new Date(now-1000).toISOString(),expiresAt:new Date(now+60000).toISOString(),signerIdentityId:requestIdentity.identityId};value.contentSha256=`sha256-${createHash("sha256").update(value.content).digest("hex")}`;value.signerSignature=new Uint8Array(signDomainPurpose(requestIdentity,requestSignatureDomain,requestSigningPurpose,runtime.encode(requestSigningTuple(value))));const requests=await open(runtime,defs.request,requestStore);await requests.put(defs.request,value.requestId,value);const options={runtime,requestStorePath:requestStore,receiptStorePath:receiptStore,executionStorePath:executionStore,epiphanyTrustAnchorPath:anchorPath,privateKeyPath:deliveryKey,targetAdmitted:()=>true,bridgeCliPath:bridgePath,bifrostRoot:root,requestPermit:async()=>({permitId:"permit-1"})};let results=await processPersonaDiscordDeliveries(options);assert.equal(results[0].status,"completed");results=await processPersonaDiscordDeliveries(options);assert.equal(results.length,0);const receipts=await open(runtime,defs.receipt,receiptStore),receipt=receipts.get(defs.receipt,value.requestId);assert.equal(receipt.messageId,"789");assert.equal(verifyDomainPurpose(deliveryIdentity.publicKey,receiptSignatureDomain,receiptSigningPurpose,runtime.encode(receiptSigningTuple(receipt)),receipt.providerSignature),true);assert.equal(await import("node:fs/promises").then(fs=>fs.readFile(counterPath,"utf8")),"1");});
test("running journal recovers as terminal unknown without a second Discord post",async()=>{const root=await mkdtemp(resolve(tmpdir(),"persona-mouth-crash-")),runtime=loadRuntime(),defs=buildPersonaDiscordDeliveryDefinitions(runtime.defineDocumentType),requestStore=resolve(root,"requests.cc"),receiptStore=resolve(root,"receipts.cc"),executionStore=resolve(root,"private","executions.cc"),requestIdentity=await enrollDomainIdentity(resolve(root,"request.seed"),"request identity",requestIdentityDomain),deliveryKey=resolve(root,"delivery.seed"),anchorPath=resolve(root,"anchor.msgpack");await enrollDomainIdentity(deliveryKey,"delivery identity",receiptIdentityDomain);await writeFile(anchorPath,runtime.encode(requestAnchor(requestIdentity)));const now=Date.now(),value={...request(),issuedAt:new Date(now-1000).toISOString(),expiresAt:new Date(now+60000).toISOString(),signerIdentityId:requestIdentity.identityId};value.contentSha256=`sha256-${createHash("sha256").update(value.content).digest("hex")}`;value.signerSignature=new Uint8Array(signDomainPurpose(requestIdentity,requestSignatureDomain,requestSigningPurpose,runtime.encode(requestSigningTuple(value))));const requests=await open(runtime,defs.request,requestStore);await requests.put(defs.request,value.requestId,value);const digest=`sha256-${createHash("sha256").update(runtime.encode(requestSigningTuple(value))).digest("hex")}`,executions=await open(runtime,defs.execution,executionStore);await executions.put(defs.execution,value.requestId,{schemaName:"bifrost.persona_discord_delivery_execution",schemaVersion:executionSchema,requestId:value.requestId,requestPayloadSha256:digest,status:"running",recordedAt:new Date().toISOString(),detail:"crash fixture"});const results=await processPersonaDiscordDeliveries({runtime,requestStorePath:requestStore,receiptStorePath:receiptStore,executionStorePath:executionStore,epiphanyTrustAnchorPath:anchorPath,privateKeyPath:deliveryKey,targetAdmitted:()=>true,bridgeCliPath:resolve(root,"must-not-run.mjs"),bifrostRoot:root});assert.equal(results[0].status,"unknown");assert.equal(results[0].messageId,"");});
test("permit intent survives network failure and restart reuses its exact request tuple",async()=>{const f=await deliveryFixture("permit-retry-");const attempts=[];let results=await processPersonaDiscordDeliveries({...f.options,requestPermit:async value=>{attempts.push({...value});throw new Error("permit network unavailable");}});assert.equal(results[0].status,"permit_pending");const executions=await open(f.runtime,f.defs.execution,f.executionStore),pending=executions.get(f.defs.execution,f.value.requestId);assert.equal(pending.status,"permit_pending");assert.equal(pending.permitNonce,attempts[0].nonce);assert.equal(pending.permitIssuedAt,attempts[0].issuedAt);assert.equal(pending.permitExpiresAt,attempts[0].expiresAt);results=await processPersonaDiscordDeliveries({...f.options,requestPermit:async value=>{attempts.push({...value});return {permitId:"permit-after-restart"};}});assert.equal(results[0].status,"completed");assert.equal(attempts.length,2);for(const key of ["requestId","requestPayloadSha256","targetRuntimeId","nonce","issuedAt","expiresAt"])assert.equal(attempts[1][key],attempts[0][key]);assert.equal(await import("node:fs/promises").then(fs=>fs.readFile(f.counterPath,"utf8")),"1");});
test("expired permit intent restarts as terminal unknown without requesting or posting",async()=>{const f=await deliveryFixture("permit-expired-"),digest=`sha256-${createHash("sha256").update(f.runtime.encode(requestSigningTuple(f.value))).digest("hex")}`,executions=await open(f.runtime,f.defs.execution,f.executionStore),issuedAt=new Date(Date.now()-6000).toISOString(),expiresAt=new Date(Date.now()-1000).toISOString();await executions.put(f.defs.execution,f.value.requestId,{schemaName:"bifrost.persona_discord_delivery_execution",schemaVersion:executionSchema,requestId:f.value.requestId,requestPayloadSha256:digest,status:"permit_pending",recordedAt:issuedAt,permitNonce:"persisted-nonce",permitIssuedAt:issuedAt,permitExpiresAt:expiresAt,detail:"restart fixture"});let permitCalls=0;const results=await processPersonaDiscordDeliveries({...f.options,requestPermit:async()=>{permitCalls++;throw new Error("must not request");},bridgeCliPath:resolve(f.root,"must-not-post.mjs")});assert.equal(results[0].status,"unknown");assert.equal(results[0].messageId,"");assert.equal(permitCalls,0);await assert.rejects(import("node:fs/promises").then(fs=>fs.readFile(f.counterPath,"utf8")));});
function loadRuntime(){const cult=resolve(import.meta.dirname,"..","..","CultLib"),cr=createRequire(resolve(cult,"packages","cultcache-ts","package.json")),nr=createRequire(resolve(cult,"packages","cultnet-ts","package.json")),cc=cr("cultcache-ts"),mp=nr("@msgpack/msgpack");return {CultCache:cc.CultCache,SingleFileMessagePackBackingStore:cc.SingleFileMessagePackBackingStore,defineDocumentType:cc.defineDocumentType,encode:mp.encode,decode:mp.decode};}
async function open(runtime,definition,path){const cache=runtime.CultCache.builder().withDocumentType(definition).withGenericStore(new runtime.SingleFileMessagePackBackingStore(path)).build();await cache.pullAllBackingStores();return cache;}
function requestAnchor(identity){return ["gamecult.service_trust_anchor.v1","epiphany-persona-mouth:epiphany-yggdrasil:v0","epiphany-persona-mouth","epiphany-yggdrasil",identity.identityId,new Uint8Array(identity.publicKey),"ed25519",requestSigningPurpose,"epiphany.persona_discord_delivery_request.v0","root",Date.now(),null,false];}
async function deliveryFixture(prefix){const root=await mkdtemp(resolve(tmpdir(),prefix)),runtime=loadRuntime(),defs=buildPersonaDiscordDeliveryDefinitions(runtime.defineDocumentType),requestStore=resolve(root,"requests.cc"),receiptStore=resolve(root,"receipts.cc"),executionStore=resolve(root,"private","executions.cc"),deliveryKey=resolve(root,"delivery.seed"),anchorPath=resolve(root,"request-anchor.msgpack"),counterPath=resolve(root,"post-count"),bridgeCliPath=resolve(root,"fake-bridge.mjs"),requestIdentity=await enrollDomainIdentity(resolve(root,"request.seed"),"request identity",requestIdentityDomain);await enrollDomainIdentity(deliveryKey,"delivery identity",receiptIdentityDomain);await writeFile(anchorPath,runtime.encode(requestAnchor(requestIdentity)));await writeFile(bridgeCliPath,`import{readFileSync,writeFileSync}from'node:fs';const p=${JSON.stringify(counterPath)};let n=0;try{n=Number(readFileSync(p,'utf8'))}catch{}writeFileSync(p,String(n+1));console.log(JSON.stringify({action:'discord-post',ok:true,channelId:'123',messageId:'789',transport:'bifrost.discord-post',crossingReceiptId:'crossing-1',url:'https://discord.com/channels/g/123/789'}));`);const now=Date.now(),value={...request(),issuedAt:new Date(now-1000).toISOString(),expiresAt:new Date(now+60000).toISOString(),signerIdentityId:requestIdentity.identityId};value.contentSha256=`sha256-${createHash("sha256").update(value.content).digest("hex")}`;value.signerSignature=new Uint8Array(signDomainPurpose(requestIdentity,requestSignatureDomain,requestSigningPurpose,runtime.encode(requestSigningTuple(value))));const requests=await open(runtime,defs.request,requestStore);await requests.put(defs.request,value.requestId,value);return {root,runtime,defs,value,executionStore,counterPath,options:{runtime,requestStorePath:requestStore,receiptStorePath:receiptStore,executionStorePath:executionStore,epiphanyTrustAnchorPath:anchorPath,privateKeyPath:deliveryKey,targetAdmitted:()=>true,bridgeCliPath,bifrostRoot:root}};}

View file

@ -7,6 +7,7 @@ import { mkdtemp, stat, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { buildFeedbackDefinitions } from "../tools/persona-feedback-documents.mjs";
import { conversationFromDiscordMessage, discordGatewayReady } from "../tools/bifrost-discord-persona-ingress.mjs";
const root=resolve(import.meta.dirname,".."), cult=resolve(root,"..","CultLib");
const cr=createRequire(resolve(cult,"packages","cultcache-ts","package.json"));
@ -18,7 +19,7 @@ test("Bifrost admits bound feedback as pressure only and exports CultNet",async(
const dir=await mkdtemp(resolve(tmpdir(),"bifrost-feedback-")), store=resolve(dir,"provider.cc"), observationStore=resolve(dir,"observations.cc"), deliveryStore=resolve(dir,"delivery.cc"), unusedDeliveryDefault=resolve(dir,"unused-delivery.cc"), out=resolve(dir,"feedback.msgpack"), key=resolve(dir,"identity.seed"), anchor=resolve(dir,"anchor.msgpack"),providerAnchor=resolve(dir,"provider-health-anchor.msgpack");
cliNoStore("enroll-identity","--private-key",key,"--trust-anchor-out",anchor);
cliNoStore("export-provider-health-anchor","--private-key",key,"--trust-anchor-out",providerAnchor);
cli("bind-target",store,"--guild-id","g","--channel-id","c","--persona-id","epiphany","--repo","GameCult/Epiphany","--runtime-id","epiphany-yggdrasil","--producer-runtime-id","voidbot-yggdrasil","--source-visibility","organization","--data-classification","organization_feedback","--delivery-store",deliveryStore);
cli("bind-target",store,"--guild-id","g","--channel-id","c","--persona-id","epiphany","--repo","GameCult/Epiphany","--runtime-id","epiphany-yggdrasil","--producer-runtime-id","bifrost-discord-yggdrasil","--source-visibility","organization","--data-classification","organization_feedback","--delivery-store",deliveryStore);
await putEvent(observationStore,event("one","human-one"));
const sourceBefore=await readEvent(observationStore,"one");
const served=cli("serve",store,"--observation-store",observationStore,"--once","true","--private-key",key,"--delivery-store",unusedDeliveryDefault,"--interval-ms","1");
@ -35,7 +36,7 @@ test("Bifrost admits bound feedback as pressure only and exports CultNet",async(
assert.equal(Object.keys(first.delivery.providerSignature).length,64);
const signed=(await n.get(defs.delivery,"one"))?.value??await n.get(defs.delivery,"one"), trust=decode(await readFile(anchor));
assert.equal(signed.packetSha256,`sha256-${createHash("sha256").update(encode(packetTuple(signed.packet))).digest("hex")}`);
assert.equal(signed.sourceObserverId,"voidbot");assert.equal(signed.provider,"bifrost");
assert.equal(signed.sourceObserverId,"bifrost-discord");assert.equal(signed.provider,"bifrost");
const payload=encode([signed.schemaVersion,signed.admissionId,packetTuple(signed.packet),signed.packetSha256,signed.sourceObserverId,signed.sourceObserverRuntimeId,signed.provider,signed.bifrostAdmissionReceiptId,signed.authority,signed.providerIdentityId]);
const purpose=Buffer.from("bifrost.persona-feedback.delivery.v0"),domain=Buffer.from("epiphany.host-incarnation.signature.v0\0"),message=Buffer.concat([domain,u64(purpose.length),purpose,u64(payload.length),payload]);
assert.equal(trust.length,6); assert.equal(trust[0],"epiphany.host_identity_trust_anchor.v0"); assert.equal(trust[1],signed.providerIdentityId);
@ -54,22 +55,71 @@ test("Bifrost admits bound feedback as pressure only and exports CultNet",async(
const deliveryCache=cr("cultcache-ts").CultCache.builder().withDocumentType(defs.delivery).withGenericStore(new (cr("cultcache-ts").SingleFileMessagePackBackingStore)(deliveryStore)).build();await deliveryCache.pullAllBackingStores();assert.equal(deliveryCache.getAll(defs.delivery).length,2);
const beforeStatus=await Promise.all([store,observationStore,deliveryStore,key].map(path=>readFile(path)));
const readiness=cli("status",store,"--observation-store",observationStore,"--private-key",key);
assert.equal(readiness.schemaVersion,"bifrost.persona_feedback.readiness.v0");assert.equal(readiness.status,"ready");assert.equal(readiness.bindingCount,1);assert.equal(readiness.privateStateExposed,false);
assert.equal(readiness.schemaVersion,"bifrost.persona_feedback.readiness.v0");assert.equal(readiness.status,"ready");assert.equal(readiness.bindingCount,1);assert.equal(readiness.pendingFailedCount,0);assert.equal(readiness.privateStateExposed,false);
const staged=cli("status",store,"--observation-store",observationStore,"--private-key",key,"--epiphany-persona-mouth-trust-anchor",resolve(dir,"missing-mouth-anchor.msgpack"),"--epiphany-persona-permit-trust-anchor",resolve(dir,"missing-permit-anchor.msgpack"),"--epiphany-persona-permit-rudp","rudp://127.0.0.1:9","--persona-delivery-private-key",resolve(dir,"missing-delivery.seed"));
assert.equal(staged.ingressReady,true);assert.deepEqual(staged.ingressReasons,[]);assert.equal(staged.personaDelivery.ready,false);assert.equal(staged.status,"degraded");
const afterStatus=await Promise.all([store,observationStore,deliveryStore,key].map(path=>readFile(path)));assert.deepEqual(afterStatus,beforeStatus);
const Store=cr("cultcache-ts").SingleFileMessagePackBackingStore;
assert.deepEqual([...new Set((await new Store(observationStore).pullAll()).map(entry=>entry.type))],["voidbot.discord.persona_feedback_event"]);
assert.equal((await new Store(store).pullAll()).some(entry=>entry.type==="voidbot.discord.persona_feedback_event"),false);
assert.deepEqual([...new Set((await new Store(observationStore).pullAll()).map(entry=>entry.type))],["bifrost.discord.persona_conversation_event"]);
assert.equal((await new Store(store).pullAll()).some(entry=>entry.type==="bifrost.discord.persona_conversation_event"),false);
assert.deepEqual([...new Set((await new Store(deliveryStore).pullAll()).map(entry=>entry.type))],["bifrost.persona_feedback.delivery"]);
await assert.rejects(stat(unusedDeliveryDefault));
});
test("Bifrost rejects an event without exact target binding",async()=>{const dir=await mkdtemp(resolve(tmpdir(),"bifrost-feedback-")),store=resolve(dir,"provider.cc"),observationStore=resolve(dir,"observations.cc");await putEvent(observationStore,event("wrong","human","other-runtime"));const r=raw("process",store,"--observation-store",observationStore,"--event-id","wrong");assert.notEqual(r.status,0);assert.match(r.stderr,/No exact Bifrost target binding/);assert.equal((await readEvent(observationStore,"wrong")).status,"pending");});
test("Bifrost rejects aliased observation, private, and delivery stores",async()=>{const dir=await mkdtemp(resolve(tmpdir(),"bifrost-feedback-")),store=resolve(dir,"provider.cc"),observations=resolve(dir,"observations.cc");await putEvent(observations,event("alias","human"));const r=raw("bind-target",store,"--observation-store",observations,"--guild-id","g","--channel-id","c","--persona-id","epiphany","--repo","GameCult/Epiphany","--runtime-id","epiphany-yggdrasil","--producer-runtime-id","voidbot-yggdrasil","--source-visibility","organization","--data-classification","organization_feedback","--delivery-store",store);assert.notEqual(r.status,0);assert.match(r.stderr,/distinct canonical paths/);});
test("Bifrost rejects aliased observation, private, and delivery stores",async()=>{const dir=await mkdtemp(resolve(tmpdir(),"bifrost-feedback-")),store=resolve(dir,"provider.cc"),observations=resolve(dir,"observations.cc");await putEvent(observations,event("alias","human"));const r=raw("bind-target",store,"--observation-store",observations,"--guild-id","g","--channel-id","c","--persona-id","epiphany","--repo","GameCult/Epiphany","--runtime-id","epiphany-yggdrasil","--producer-runtime-id","bifrost-discord-yggdrasil","--source-visibility","organization","--data-classification","organization_feedback","--delivery-store",store);assert.notEqual(r.status,0);assert.match(r.stderr,/distinct canonical paths/);});
test("Bifrost rejects mismatched classification and malformed observations",async()=>{const dir=await mkdtemp(resolve(tmpdir(),"bifrost-feedback-")),store=resolve(dir,"provider.cc"),observations=resolve(dir,"observations.cc"),delivery=resolve(dir,"delivery.cc");const mismatch=raw("bind-target",store,"--observation-store",observations,"--guild-id","g","--channel-id","c","--persona-id","epiphany","--repo","GameCult/Epiphany","--runtime-id","epiphany-yggdrasil","--producer-runtime-id","voidbot-yggdrasil","--source-visibility","private","--data-classification","public_feedback","--delivery-store",delivery);assert.notEqual(mismatch.status,0);assert.match(mismatch.stderr,/exact admitted pair/);for(const malformed of [{observedAt:"yesterday"},{addressingMode:"ambient"},{authorityClass:"work"},{status:"admitted"},{payloadHash:"not-a-digest"}])await assert.rejects(putEvent(observations,{...event(`bad-${Object.keys(malformed)[0]}`,"human"),...malformed}));});
test("Bifrost rejects mismatched classification and malformed observations",async()=>{const dir=await mkdtemp(resolve(tmpdir(),"bifrost-feedback-")),store=resolve(dir,"provider.cc"),observations=resolve(dir,"observations.cc"),delivery=resolve(dir,"delivery.cc");const mismatch=raw("bind-target",store,"--observation-store",observations,"--guild-id","g","--channel-id","c","--persona-id","epiphany","--repo","GameCult/Epiphany","--runtime-id","epiphany-yggdrasil","--producer-runtime-id","bifrost-discord-yggdrasil","--source-visibility","private","--data-classification","public_feedback","--delivery-store",delivery);assert.notEqual(mismatch.status,0);assert.match(mismatch.stderr,/exact admitted pair/);for(const malformed of [{observedAt:"yesterday"},{addressingMode:"ambient"},{authorityClass:"work"},{status:"admitted"},{payloadHash:"not-a-digest"},{producerId:"voidbot"}])await assert.rejects(putEvent(observations,{...event(`bad-${Object.keys(malformed)[0]}`,"human"),...malformed}));});
function event(id,author,targetRuntimeId="epiphany-yggdrasil"){const content=`feedback ${id}`;return {schemaName:"voidbot.discord.persona_feedback_event",schemaVersion:"voidbot.discord.persona_feedback_event.v0",eventId:id,observedAt:new Date().toISOString(),guildId:"g",channelId:"c",messageId:`m-${id}`,authorId:author,authorName:author,addressingMode:"role",targetPersonaId:"epiphany",targetRepoName:"GameCult/Epiphany",targetRuntimeId,content,payloadHash:createHash("sha256").update(content).digest("hex"),producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"feedback_only",status:"pending"};}
test("native Discord ingress admits only direct human address",()=>{
const base={id:"m1",guild_id:"g",channel_id:"c",timestamp:"2026-07-21T20:00:00.000Z",author:{id:"human",username:"Human",bot:false},content:"<@42> hello Epiphany",mentions:[{id:"42"}]};
const admitted=conversationFromDiscordMessage(base,"42");assert.equal(admitted.content,"hello Epiphany");assert.equal(admitted.addressingMode,"text");assert.match(admitted.payloadHash,/^[a-f0-9]{64}$/);
assert.equal(conversationFromDiscordMessage({...base,author:{id:"bot",bot:true}},"42"),null);
assert.equal(conversationFromDiscordMessage({...base,mentions:[],content:"ambient chatter"},"42"),null);
assert.equal(conversationFromDiscordMessage({...base,mentions:[{id:"42"}],content:"<@42> "},"42"),null);
assert.equal(conversationFromDiscordMessage({...base,content:`<@42> ${"x".repeat(1201)}`},"42"),null);
assert.equal(conversationFromDiscordMessage({...base,content:`<@42> ${"x".repeat(1200)}`},"42").content.length,1200);
assert.equal(Buffer.byteLength(conversationFromDiscordMessage({...base,content:`<@42> ${"😀".repeat(300)}`},"42").content,"utf8"),1200);
assert.equal(conversationFromDiscordMessage({...base,content:`<@42> ${"😀".repeat(301)}`},"42"),null);
assert.equal(conversationFromDiscordMessage({...base,timestamp:"not-a-time"},"42"),null);
const reply=conversationFromDiscordMessage({...base,mentions:[],content:"following up",referenced_message:{author:{id:"42"}}},"42");assert.equal(reply.addressingMode,"reply");
const commandShaped=conversationFromDiscordMessage({...base,content:"<@42> /epiphany wake"},"42");assert.equal(commandShaped.content,"/epiphany wake");assert.equal(commandShaped.addressingMode,"text");
});
test("Discord gateway health fails closed while disconnected or heartbeat-stale",()=>{
const now=1_000_000;
assert.equal(discordGatewayReady({connected:false,sessionId:"s",lastHeartbeatAckAtMillis:now},now),false);
assert.equal(discordGatewayReady({connected:true,sessionId:"",lastHeartbeatAckAtMillis:now},now),false);
assert.equal(discordGatewayReady({connected:true,sessionId:"s",lastHeartbeatAckAtMillis:0},now),false);
assert.equal(discordGatewayReady({connected:true,sessionId:"s",lastHeartbeatAckAtMillis:now-120001},now),false);
assert.equal(discordGatewayReady({connected:true,sessionId:"s",lastHeartbeatAckAtMillis:now-120000},now),true);
});
test("serve drains a persisted observation left by a failed prior process",async()=>{
const dir=await mkdtemp(resolve(tmpdir(),"bifrost-feedback-recovery-")),store=resolve(dir,"provider.cc"),observations=resolve(dir,"observations.cc"),delivery=resolve(dir,"delivery.cc"),key=resolve(dir,"identity.seed"),anchor=resolve(dir,"anchor.msgpack");
cli("bind-target",store,"--guild-id","g","--channel-id","c","--persona-id","epiphany","--repo","GameCult/Epiphany","--runtime-id","epiphany-yggdrasil","--producer-runtime-id","bifrost-discord-yggdrasil","--source-visibility","organization","--data-classification","organization_feedback","--delivery-store",delivery);
await putEvent(observations,event("crash-recovery","human"));
const failed=raw("process",store,"--observation-store",observations,"--event-id","crash-recovery","--private-key",key,"--delivery-store",delivery);assert.notEqual(failed.status,0);
cliNoStore("enroll-identity","--private-key",key,"--trust-anchor-out",anchor);
const recovered=cli("serve",store,"--observation-store",observations,"--once","true","--private-key",key,"--delivery-store",delivery,"--interval-ms","1");assert.equal(recovered.scanned,1);assert.equal(recovered.results[0].ok,true);
const n=await CultMesh.createNode(store,{documents:[defs.binding,defs.actorLink,defs.receipt,defs.delivery]});await n.cache?.pullAllBackingStores?.();assert.ok((await n.get(defs.receipt,"crash-recovery")));
});
test("serve durably accounts for failed pending observations and clears them after admission",async()=>{
const dir=await mkdtemp(resolve(tmpdir(),"bifrost-feedback-failure-")),store=resolve(dir,"provider.cc"),observations=resolve(dir,"observations.cc"),delivery=resolve(dir,"delivery.cc"),key=resolve(dir,"identity.seed"),anchor=resolve(dir,"anchor.msgpack");
cli("bind-target",store,"--guild-id","g","--channel-id","c","--persona-id","epiphany","--repo","GameCult/Epiphany","--runtime-id","epiphany-yggdrasil","--producer-runtime-id","bifrost-discord-yggdrasil","--source-visibility","organization","--data-classification","organization_feedback","--delivery-store",delivery);
await putEvent(observations,event("retry-evidence","human"));
const failed=cli("serve",store,"--observation-store",observations,"--once","true","--private-key",key,"--delivery-store",delivery,"--interval-ms","1");
assert.equal(failed.results[0].ok,false);assert.equal(failed.readiness.status,"degraded");assert.equal(failed.readiness.pendingFailedCount,1);assert.match(failed.readiness.reasons[0],/identity|pendingFailures/);
let n=await CultMesh.createNode(store,{documents:[defs.binding,defs.actorLink,defs.receipt,defs.delivery,defs.pendingFailure]});await n.cache?.pullAllBackingStores?.();
const evidence=(await n.get(defs.pendingFailure,"retry-evidence"))?.value??await n.get(defs.pendingFailure,"retry-evidence");assert.equal(evidence.attemptCount,1);assert.equal(evidence.privateStateExposed,false);assert.ok(Buffer.byteLength(evidence.lastError,"utf8")<=512);
cliNoStore("enroll-identity","--private-key",key,"--trust-anchor-out",anchor);
const recovered=cli("serve",store,"--observation-store",observations,"--once","true","--private-key",key,"--delivery-store",delivery,"--interval-ms","1");assert.equal(recovered.results[0].ok,true);assert.equal(recovered.readiness.pendingFailedCount,0);assert.equal(recovered.readiness.status,"ready");
n=await CultMesh.createNode(store,{documents:[defs.binding,defs.actorLink,defs.receipt,defs.delivery,defs.pendingFailure]});await n.cache?.pullAllBackingStores?.();assert.equal(await n.get(defs.pendingFailure,"retry-evidence"),undefined);
});
function event(id,author,targetRuntimeId="epiphany-yggdrasil"){const content=`feedback ${id}`;return {schemaName:"bifrost.discord.persona_conversation_event",schemaVersion:"bifrost.discord.persona_conversation_event.v0",eventId:id,observedAt:new Date().toISOString(),guildId:"g",channelId:"c",messageId:`m-${id}`,authorId:author,authorName:author,addressingMode:"role",targetPersonaId:"epiphany",targetRepoName:"GameCult/Epiphany",targetRuntimeId,content,payloadHash:createHash("sha256").update(content).digest("hex"),producerId:"bifrost-discord",producerRuntimeId:"bifrost-discord-yggdrasil",authorityClass:"feedback_only",status:"pending"};}
async function putEvent(store,value){const n=await CultMesh.createNode(store,{documents:[defs.event]});await n.put(defs.event,value.eventId,value);await n.flush?.();}
async function readEvent(store,id){const n=await CultMesh.createNode(store,{documents:[defs.event]});await n.cache?.pullAllBackingStores?.();return (await n.get(defs.event,id))?.value??await n.get(defs.event,id);}
function raw(command,store,...args){return spawnSync(process.execPath,[resolve(root,"tools","persona-feedback.mjs"),command,"--store",store,...args],{cwd:root,encoding:"utf8"});}

View file

@ -0,0 +1,107 @@
import { createHash } from "node:crypto";
const GATEWAY_VERSION = 10;
const GATEWAY_INTENTS = (1 << 0) | (1 << 9) | (1 << 15);
export function conversationFromDiscordMessage(message, botUserId) {
if (!message || typeof message !== "object" || message.author?.bot === true) return null;
const guildId = text(message.guild_id);
const channelId = text(message.channel_id);
const messageId = text(message.id);
const authorId = text(message.author?.id);
const observedAt = Date.parse(String(message.timestamp ?? ""));
if (!guildId || !channelId || !messageId || !authorId || !botUserId || !Number.isFinite(observedAt)) return null;
const mentioned = Array.isArray(message.mentions)
&& message.mentions.some((mention) => String(mention?.id ?? "") === botUserId);
const replied = String(message.referenced_message?.author?.id ?? "") === botUserId;
if (!mentioned && !replied) return null;
const content = String(message.content ?? "")
.replace(new RegExp(`<@!?${escapeRegex(botUserId)}>`, "g"), " ")
.replace(/\s+/g, " ")
.trim();
if (!content || Buffer.byteLength(content, "utf8") > 1200) return null;
return {
guildId,
channelId,
messageId,
authorId,
authorName: text(message.member?.nick) || text(message.author?.global_name) || text(message.author?.username) || authorId,
content,
addressingMode: mentioned ? "text" : "reply",
observedAt: new Date(observedAt).toISOString(),
payloadHash: createHash("sha256").update(content, "utf8").digest("hex"),
};
}
export function discordGatewayReady(state, nowMillis = Date.now(), maxAckAgeMillis = 120000) {
return state?.connected === true
&& text(state.sessionId) !== ""
&& Number.isFinite(state.lastHeartbeatAckAtMillis)
&& state.lastHeartbeatAckAtMillis > 0
&& nowMillis >= state.lastHeartbeatAckAtMillis
&& nowMillis - state.lastHeartbeatAckAtMillis <= maxAckAgeMillis;
}
export async function serveDiscordPersonaIngress({ token, runtimeId, onConversation, onReady = () => {}, onDisconnect = () => {}, onState = () => {} }) {
if (!text(token)) throw new Error("Bifrost Discord Persona ingress requires a bot token.");
if (!text(runtimeId)) throw new Error("Bifrost Discord Persona ingress requires a runtime id.");
if (typeof onConversation !== "function") throw new Error("Bifrost Discord Persona ingress requires a conversation sink.");
onState({connected:false,sessionId:"",lastHeartbeatAckAtMillis:0,phase:"discovering"});
const gateway = await discordJson("https://discord.com/api/v10/gateway/bot", token);
if (!text(gateway?.url)) throw new Error("Discord gateway discovery returned no URL.");
onState({connected:false,sessionId:"",lastHeartbeatAckAtMillis:0,phase:"discovered"});
let sequence = null, sessionId = "", resumeUrl = gateway.url, botUserId = "", stopped = false;
while (!stopped) {
try {
const result = await gatewaySession({ token, runtimeId, url: resumeUrl, sequence, sessionId, botUserId, onConversation, onReady, onState });
sequence = result.sequence; sessionId = result.sessionId; resumeUrl = result.resumeUrl; botUserId = result.botUserId;
stopped = result.stopped;
} catch (error) {
if (error?.fatal === true) throw error;
onState({connected:false,sessionId,botUserId,lastHeartbeatAckAtMillis:0,phase:"disconnected"});
onDisconnect(error);
await delay(2000);
}
}
}
async function gatewaySession(state) {
const socket = new WebSocket(`${state.url}/?v=${GATEWAY_VERSION}&encoding=json`);
let sequence=state.sequence,sessionId=state.sessionId,resumeUrl=state.url,botUserId=state.botUserId,heartbeatTimer=null,heartbeatAck=true,stopped=false;
await new Promise((resolve, reject) => {
const cleanup=()=>{if(heartbeatTimer)clearInterval(heartbeatTimer);};
socket.addEventListener("error",()=>{cleanup();reject(new Error("Discord gateway socket failed."));},{once:true});
socket.addEventListener("close",(event)=>{cleanup();state.onState({connected:false,sessionId,botUserId,lastHeartbeatAckAtMillis:0,phase:"disconnected"});if([4004,4013,4014].includes(event.code)){stopped=true;const error=new Error(`Discord permanently rejected the Bifrost gateway session (close ${event.code}).`);error.fatal=true;reject(error);return;}resolve();},{once:true});
socket.addEventListener("message",async event=>{
try {
const frame=JSON.parse(String(event.data));
if(Number.isInteger(frame.s))sequence=frame.s;
if(frame.op===10){
const interval=Number(frame.d?.heartbeat_interval);if(!Number.isFinite(interval)||interval<1000)throw new Error("Discord gateway supplied an invalid heartbeat interval.");
const heartbeat=()=>{if(!heartbeatAck){socket.close(4000,"heartbeat timeout");return;}heartbeatAck=false;socket.send(JSON.stringify({op:1,d:sequence}));};
heartbeatTimer=setInterval(heartbeat,interval);heartbeat();
if(sessionId&&sequence!==null)socket.send(JSON.stringify({op:6,d:{token:state.token,session_id:sessionId,seq:sequence}}));
else socket.send(JSON.stringify({op:2,d:{token:state.token,intents:GATEWAY_INTENTS,properties:{os:process.platform,browser:"bifrost",device:"bifrost"}}}));
} else if(frame.op===11){heartbeatAck=true;state.onState({connected:true,sessionId,botUserId,lastHeartbeatAckAtMillis:Date.now(),phase:"heartbeat-acknowledged"});}
else if(frame.op===7)socket.close(4000,"server reconnect");
else if(frame.op===9){sessionId="";sequence=null;socket.close(4000,"invalid session");}
else if(frame.op===0&&frame.t==="READY"){
sessionId=text(frame.d?.session_id);resumeUrl=text(frame.d?.resume_gateway_url)||state.url;botUserId=text(frame.d?.user?.id);if(!sessionId||!botUserId)throw new Error("Discord READY omitted session or bot identity.");state.onState({connected:true,sessionId,botUserId,lastHeartbeatAckAtMillis:0,phase:"ready-awaiting-heartbeat-ack"});await state.onReady({runtimeId:state.runtimeId,botUserId,sessionId});
} else if(frame.op===0&&frame.t==="RESUMED"){state.onState({connected:true,sessionId,botUserId,lastHeartbeatAckAtMillis:0,phase:"resumed-awaiting-heartbeat-ack"});await state.onReady({runtimeId:state.runtimeId,botUserId,sessionId});}
else if(frame.op===0&&frame.t==="MESSAGE_CREATE"){
const conversation=conversationFromDiscordMessage(frame.d,botUserId);if(conversation)await state.onConversation(conversation);
}
} catch(error){socket.close(4000,"ingress failure");reject(error);}
});
});
return {sequence,sessionId,resumeUrl,botUserId,stopped};
}
async function discordJson(url, token) {
const response=await fetch(url,{headers:{authorization:`Bot ${token}`},redirect:"error",signal:AbortSignal.timeout(15000)});
if(!response.ok)throw new Error(`Discord gateway discovery failed with HTTP ${response.status}.`);
return response.json();
}
function text(value){return typeof value==="string"?value.trim():"";}
function escapeRegex(value){return value.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");}
function delay(ms){return new Promise(resolve=>setTimeout(resolve,ms));}

View file

@ -7,8 +7,13 @@ const SIGNATURE_DOMAIN=Buffer.from("epiphany.host-incarnation.signature.v0\0","u
export async function enrollSignedIdentity(path,label="Bifrost identity") { await mkdir(dirname(path),{recursive:true}); const {privateKey}=generateKeyPairSync("ed25519"),seed=Buffer.from(privateKey.export({format:"jwk"}).d,"base64url"),handle=await open(path,"wx",0o600); try{await handle.writeFile(seed);await handle.sync();}finally{await handle.close();} await chmod(path,0o600); return loadSignedIdentity(path,label); }
export async function loadSignedIdentity(path,label="Bifrost identity") { const info=await stat(path); if(!info.isFile())throw new Error(`${label} must be a regular file.`); if(process.platform!=="win32"&&(info.mode&0o077)!==0)throw new Error(`${label} private key must have mode 0600.`); if(process.platform!=="win32"&&typeof process.geteuid==="function"&&info.uid!==process.geteuid())throw new Error(`${label} private key must be owned by the service user.`); const seed=await readFile(path);if(seed.length!==32)throw new Error(`${label} seed must be exactly 32 bytes.`); const key=createPrivateKey({key:Buffer.concat([Buffer.from("302e020100300506032b657004220420","hex"),seed]),format:"der",type:"pkcs8"}),publicKey=createPublicKey(key).export({format:"der",type:"spki"}).subarray(-32),identityId=createHash("sha256").update(ID_DOMAIN).update(publicKey).digest("hex"); return {identityId,publicKey,key}; }
export async function enrollDomainIdentity(path,label,identityDomain){await enrollSignedIdentity(path,label);return loadDomainIdentity(path,label,identityDomain);}
export async function loadDomainIdentity(path,label,identityDomain){const identity=await loadSignedIdentity(path,label);return {...identity,identityId:createHash("sha256").update(Buffer.from(identityDomain,"utf8")).update(identity.publicKey).digest("hex")};}
export function signPurpose(identity,purpose,payload){return ed25519Sign(null,domainMessage(purpose,payload),identity.key);}
export function verifyPurpose(publicKey,purpose,payload,signature){const key=createPublicKey({key:Buffer.concat([Buffer.from("302a300506032b6570032100","hex"),Buffer.from(publicKey)]),format:"der",type:"spki"});return ed25519Verify(null,domainMessage(purpose,payload),key,signature);}
export function signDomainPurpose(identity,signatureDomain,purpose,payload){return ed25519Sign(null,domainMessageWith(signatureDomain,purpose,payload),identity.key);}
export function verifyDomainPurpose(publicKey,signatureDomain,purpose,payload,signature){const key=createPublicKey({key:Buffer.concat([Buffer.from("302a300506032b6570032100","hex"),Buffer.from(publicKey)]),format:"der",type:"spki"});return ed25519Verify(null,domainMessageWith(signatureDomain,purpose,payload),key,signature);}
export function trustAnchor(identity,createdAt){return {schemaVersion:"epiphany.host_identity_trust_anchor.v0",identityId:identity.identityId,publicKey:new Uint8Array(identity.publicKey),assurance:"os_service_file_bound",identityCreatedAt:createdAt,sourceIdentityRecordSha256:`sha256-${createHash("sha256").update(identity.publicKey).digest("hex")}`};}
function domainMessage(purpose,payload){const p=Buffer.from(purpose,"utf8");return Buffer.concat([SIGNATURE_DOMAIN,u64(p.length),p,u64(payload.length),payload]);}
function domainMessageWith(signatureDomain,purpose,payload){const p=Buffer.from(purpose,"utf8");return Buffer.concat([Buffer.from(signatureDomain,"utf8"),u64(p.length),p,u64(payload.length),payload]);}
function u64(value){const b=Buffer.alloc(8);b.writeBigUInt64BE(BigInt(value));return b;}

View file

@ -0,0 +1,42 @@
export const requestType = "epiphany.persona_discord_delivery_request";
export const requestSchema = "epiphany.persona_discord_delivery_request.v0";
export const requestSigningPurpose = "epiphany.persona-discord.delivery-request.v0";
export const requestIdentityDomain = "epiphany.persona-discord-delivery-request.identity.v0\0";
export const requestSignatureDomain = "epiphany.persona-discord-delivery-request.signature.v0\0";
export const receiptType = "bifrost.persona_discord_delivery_receipt";
export const receiptSchema = "bifrost.persona_discord_delivery_receipt.v0";
export const receiptSigningPurpose = "bifrost.persona-discord.delivery-receipt.v0";
export const receiptIdentityDomain = "bifrost.persona-discord-delivery-receipt.identity.v0\0";
export const receiptSignatureDomain = "bifrost.persona-discord-delivery-receipt.signature.v0\0";
export const executionType = "bifrost.persona_discord_delivery_execution";
export const executionSchema = "bifrost.persona_discord_delivery_execution.v0";
export function buildPersonaDiscordDeliveryDefinitions(defineDocumentType) {
const make=(type,schema,name,parse,members)=>defineDocumentType({type,schemaName:type,schemaId:schema,schemaVersion:schema,contentHash:schema,global:false,name,schema:{parse},members});
return {
request:make(requestType,requestSchema,"requestId",parseRequest,requestMembers),
receipt:make(receiptType,receiptSchema,"receiptId",parseReceipt,receiptMembers),
execution:make(executionType,executionSchema,"requestId",parseExecution),
};
}
const requestMembers = ["schemaVersion","requestId","effectDocumentId","targetRuntimeId","personaAgentId","channelId","replyToMessageId","content","contentSha256","issuedAt","expiresAt","privateStateExposed","signerIdentityId","signerSignature"].map((memberName,slot)=>({slot,memberName,typeName:memberName==="privateStateExposed"?"boolean":memberName==="signerSignature"?"byte[]":"string",isName:memberName==="requestId"}));
const receiptMembers = ["schemaVersion","receiptId","requestId","requestPayloadSha256","status","channelId","replyToMessageId","messageId","transport","crossingReceiptId","receiptUrl","completedAt","providerIdentityId","privateStateExposed","providerSignature"].map((memberName,slot)=>({slot,memberName,typeName:memberName==="privateStateExposed"?"boolean":memberName==="providerSignature"?"byte[]":"string",isName:memberName==="receiptId"}));
export function requestSigningTuple(v){return [v.schemaVersion,v.requestId,v.effectDocumentId,v.targetRuntimeId,v.personaAgentId,v.channelId,v.replyToMessageId,v.content,v.contentSha256,v.issuedAt,v.expiresAt,v.privateStateExposed,v.signerIdentityId];}
export function receiptSigningTuple(v){return [v.schemaVersion,v.receiptId,v.requestId,v.requestPayloadSha256,v.status,v.channelId,v.replyToMessageId,v.messageId,v.transport,v.crossingReceiptId,v.receiptUrl,v.completedAt,v.providerIdentityId,v.privateStateExposed];}
export function parseRequest(value){const v=typed(value,requestMembers,"Persona Discord delivery request");closed(v,["schemaName","schemaVersion","requestId","effectDocumentId","targetRuntimeId","personaAgentId","channelId","replyToMessageId","content","contentSha256","issuedAt","expiresAt","privateStateExposed","signerIdentityId","signerSignature"],"request");exact(v.schemaVersion,requestSchema,"request schema");for(const key of ["requestId","effectDocumentId","targetRuntimeId","personaAgentId","channelId","contentSha256","issuedAt","expiresAt","signerIdentityId"])text(v[key],key);content(v.content);optionalText(v.replyToMessageId,"replyToMessageId");if(v.privateStateExposed!==false)throw new Error("request privateStateExposed must be false.");bytes64(v.signerSignature,"signerSignature");sha(v.contentSha256,"contentSha256");const issued=date(v.issuedAt,"issuedAt"),expires=date(v.expiresAt,"expiresAt");if(expires<=issued||expires-issued>120000)throw new Error("request lifetime must be positive and at most 120 seconds.");return v;}
export function parseReceipt(value){const v=record(value,"Persona Discord delivery receipt");exact(v.schemaVersion,receiptSchema,"receipt schema");for(const key of ["receiptId","requestId","requestPayloadSha256","status","channelId","completedAt","providerIdentityId"])text(v[key],key);for(const key of ["replyToMessageId","messageId","transport","crossingReceiptId","receiptUrl"])optionalText(v[key],key);if(v.receiptId!==v.requestId)throw new Error("receiptId must equal requestId.");if(!["completed","failed","unknown"].includes(v.status))throw new Error("receipt status is invalid.");if(v.status==="completed"&&[v.messageId,v.transport,v.crossingReceiptId,v.receiptUrl].some(x=>!x))throw new Error("completed receipt lacks exact delivery evidence.");if(v.status!=="completed"&&[v.messageId,v.transport,v.crossingReceiptId,v.receiptUrl].some(Boolean))throw new Error("non-completed receipt may not claim delivery evidence.");if(v.privateStateExposed!==false)throw new Error("receipt privateStateExposed must be false.");bytes64(v.providerSignature,"providerSignature");sha(v.requestPayloadSha256,"requestPayloadSha256");date(v.completedAt,"completedAt");return v;}
export function parseExecution(value){const v=record(value,"Persona Discord execution journal");exact(v.schemaVersion,executionSchema,"execution schema");for(const key of ["requestId","requestPayloadSha256","status","recordedAt"])text(v[key],key);if(!["permit_pending","running","completed","failed","unknown"].includes(v.status))throw new Error("execution status is invalid.");if(v.status==="permit_pending"){text(v.permitNonce,"permitNonce");date(v.permitIssuedAt,"permitIssuedAt");date(v.permitExpiresAt,"permitExpiresAt");if(Date.parse(v.permitExpiresAt)<=Date.parse(v.permitIssuedAt)||Date.parse(v.permitExpiresAt)-Date.parse(v.permitIssuedAt)>5000)throw new Error("permit intent lifetime is invalid");}return v;}
function record(v,label){if(!v||typeof v!=="object"||Array.isArray(v))throw new Error(`${label} must be an object.`);return v;}
function typed(v,members,label){if(Array.isArray(v)){if(v.length!==members.length)throw new Error(`${label} tuple has the wrong arity.`);return Object.fromEntries(members.map(member=>[member.memberName,member.typeName==="byte[]"&&Array.isArray(v[member.slot])?Uint8Array.from(v[member.slot]):v[member.slot]]));}return record(v,label);}
function text(v,label){bounded(v,label,256);}
function bounded(v,label,maxBytes){if(typeof v!=="string"||!v.trim()||Buffer.byteLength(v,"utf8")>maxBytes||/\p{Cc}/u.test(v))throw new Error(`${label} is empty, oversized, or contains control characters.`);}
function content(v){if(typeof v!=="string"||!v.trim()||Buffer.byteLength(v,"utf8")>1900||v.includes("\0"))throw new Error("content is empty, exceeds 1900 UTF-8 bytes, or contains NUL.");}
function optionalText(v,label){if(typeof v!=="string")throw new Error(`${label} must be a string.`);}
function exact(v,w,label){if(v!==w)throw new Error(`${label} must be ${w}.`);}
function bytes64(v,label){if(!(v instanceof Uint8Array)||v.length!==64)throw new Error(`${label} must be 64 bytes.`);}
function sha(v,label){if(!/^sha256-[0-9a-f]{64}$/.test(v))throw new Error(`${label} must be lowercase sha256.`);}
function date(v,label){const parsed=Date.parse(v);if(Number.isNaN(parsed))throw new Error(`${label} must be RFC3339.`);return parsed;}
function closed(v,keys,label){const allowed=new Set(keys);for(const key of Object.keys(v))if(!allowed.has(key))throw new Error(`${label} contains unknown field ${key}.`);}

View file

@ -0,0 +1,51 @@
import {createHash,randomBytes} from "node:crypto";
import {spawnSync} from "node:child_process";
import {access,readFile,writeFile,mkdir} from "node:fs/promises";
import {constants} from "node:fs";
import {dirname,resolve} from "node:path";
import {enrollDomainIdentity,loadDomainIdentity,signDomainPurpose,verifyDomainPurpose} from "./bifrost-signed-identity.mjs";
import {buildPersonaDiscordDeliveryDefinitions,executionSchema,receiptIdentityDomain,receiptSchema,receiptSignatureDomain,receiptSigningPurpose,receiptSigningTuple,requestSignatureDomain,requestSigningPurpose,requestSigningTuple} from "./persona-discord-delivery-documents.mjs";
import {requestPersonaDiscordPermit} from "./persona-discord-permit.mjs";
import {permitRequestIdentityDomain,permitRequestPurpose,permitRequestSchema} from "./persona-discord-permit.mjs";
export async function enrollPersonaDiscordDeliveryIdentity({runtime,privateKeyPath,trustAnchorOut}){const identity=await enrollDomainIdentity(privateKeyPath,"Bifrost Persona Discord delivery identity",receiptIdentityDomain),anchor=["gamecult.service_trust_anchor.v1","bifrost-persona-discord-delivery:bifrost-discord-yggdrasil:v0","bifrost-persona-discord-delivery","bifrost-discord-yggdrasil",identity.identityId,new Uint8Array(identity.publicKey),"ed25519",receiptSigningPurpose,"bifrost.persona_discord_delivery_receipt.v0","root",Date.now(),null,false];await mkdir(dirname(trustAnchorOut),{recursive:true});await writeFile(trustAnchorOut,runtime.encode(anchor),{flag:"wx",mode:0o640});return {identityId:identity.identityId,privateKeyPath,trustAnchorOut};}
export async function exportPersonaDiscordPermitRequestAnchor({runtime,privateKeyPath,trustAnchorOut}){const identity=await loadDomainIdentity(privateKeyPath,"Bifrost Persona Discord permit requester identity",permitRequestIdentityDomain),anchor=["gamecult.service_trust_anchor.v1","bifrost-persona-discord-delivery-permit-request:bifrost-discord-yggdrasil:v0","bifrost-persona-discord-delivery","bifrost-discord-yggdrasil",identity.identityId,new Uint8Array(identity.publicKey),"ed25519",permitRequestPurpose,permitRequestSchema,"root",Date.now(),null,false];await mkdir(dirname(trustAnchorOut),{recursive:true});await writeFile(trustAnchorOut,runtime.encode(anchor),{flag:"wx",mode:0o640});return {identityId:identity.identityId,privateKeyPath,trustAnchorOut};}
export async function processPersonaDiscordDeliveries(options){
const {runtime,requestStorePath,receiptStorePath,executionStorePath,epiphanyTrustAnchorPath,privateKeyPath,targetAdmitted}=options,defs=buildPersonaDiscordDeliveryDefinitions(runtime.defineDocumentType);
requireSeparate([requestStorePath,receiptStorePath,executionStorePath]);
const requests=await open(runtime,defs.request,requestStorePath),receipts=await open(runtime,defs.receipt,receiptStorePath),executions=await open(runtime,defs.execution,executionStorePath),anchor=await readAnchor(runtime,epiphanyTrustAnchorPath),identity=await loadDomainIdentity(privateKeyPath,"Bifrost Persona Discord delivery identity",receiptIdentityDomain);
const results=[];
for(const request of requests.cache.getAll(defs.request).sort((a,b)=>String(a.issuedAt).localeCompare(String(b.issuedAt)))){
const unsigned=runtime.encode(requestSigningTuple(request)),digest=`sha256-${sha(unsigned)}`,existingReceipt=unwrap(receipts.cache.get(defs.receipt,request.requestId)),existingExecution=unwrap(executions.cache.get(defs.execution,request.requestId));
if(existingReceipt){if(existingReceipt.requestPayloadSha256!==digest)throw new Error(`Persona Discord request ${request.requestId} conflicts with its terminal receipt.`);continue;}
if(existingExecution){if(existingExecution.requestPayloadSha256!==digest)throw new Error(`Persona Discord request ${request.requestId} conflicts with its execution journal.`);if(existingExecution.status==="running"){results.push(await terminal("unknown",request,digest,"delivery outcome was indeterminate after service interruption",{runtime,defs,receipts,executions,identity}));continue;}if(existingExecution.status!=="permit_pending")continue;}
let failure="";
try{validateRequest(request,unsigned,anchor,targetAdmitted);}catch(error){failure=error instanceof Error?error.message:String(error);}
if(failure){results.push(await terminal("failed",request,digest,failure,{runtime,defs,receipts,executions,identity}));continue;}
if(existingExecution&&Date.parse(existingExecution.permitExpiresAt)<Date.now()){results.push(await terminal("unknown",request,digest,"permit issuance outcome was indeterminate after its durable request window expired",{runtime,defs,receipts,executions,identity}));continue;}
const permitIssuedAt=existingExecution?.permitIssuedAt??new Date().toISOString(),permitExpiresAt=existingExecution?.permitExpiresAt??new Date(Date.parse(permitIssuedAt)+5000).toISOString(),nonce=existingExecution?.permitNonce??randomBytes(16).toString("hex");
if(!existingExecution)await executions.cache.put(defs.execution,request.requestId,{schemaName:"bifrost.persona_discord_delivery_execution",schemaVersion:executionSchema,requestId:request.requestId,requestPayloadSha256:digest,status:"permit_pending",recordedAt:permitIssuedAt,permitNonce:nonce,permitIssuedAt,permitExpiresAt,detail:"permit request intent persisted before network"});
let permit;
try{permit=await (options.requestPermit??requestPersonaDiscordPermit)({endpoint:options.epiphanyPermitEndpoint,requestId:request.requestId,requestPayloadSha256:digest,targetRuntimeId:request.targetRuntimeId,nonce,issuedAt:permitIssuedAt,expiresAt:permitExpiresAt,privateKeyPath,epiphanyPermitAnchorPath:options.epiphanyPermitAnchorPath,runtime:options.permitRuntime,timeoutMs:options.permitTimeoutMs});}
catch(error){results.push({requestId:request.requestId,status:"permit_pending",detail:error instanceof Error?error.message:String(error)});continue;}
await executions.cache.put(defs.execution,request.requestId,{schemaName:"bifrost.persona_discord_delivery_execution",schemaVersion:executionSchema,requestId:request.requestId,requestPayloadSha256:digest,status:"running",recordedAt:new Date().toISOString(),detail:`permit ${permit.permitId} durably consumed before external delivery`});
if(Date.parse(permit.expiresAt)<=Date.now()){results.push(await terminal("failed",request,digest,"Epiphany brake permit expired after durable running journal and before Discord actuation",{runtime,defs,receipts,executions,identity}));continue;}
try{
const posted=post(request,options);
results.push(await terminal("completed",request,digest,"",{runtime,defs,receipts,executions,identity,posted}));
}catch(error){results.push(await terminal("unknown",request,digest,error instanceof Error?error.message:String(error),{runtime,defs,receipts,executions,identity}));}
}
return results;
}
export async function inspectPersonaDiscordDeliveryReadiness(options){try{requireSeparate([options.requestStorePath,options.receiptStorePath,options.executionStorePath]);const anchor=await readAnchor(options.runtime,options.epiphanyTrustAnchorPath),identity=await loadDomainIdentity(options.privateKeyPath,"Bifrost Persona Discord delivery identity",receiptIdentityDomain);if(!options.epiphanyPermitEndpoint)throw new Error("Epiphany permit endpoint is absent");await access(options.epiphanyPermitAnchorPath,constants.R_OK);await access(options.requestStorePath,constants.R_OK);await access(options.receiptStorePath,constants.R_OK|constants.W_OK);return {ready:true,requestSignerIdentityId:anchor.identityId,receiptSignerIdentityId:identity.identityId,reasons:[]};}catch(error){return {ready:false,requestSignerIdentityId:"",receiptSignerIdentityId:"",reasons:[error instanceof Error?error.message:String(error)]};}}
function validateRequest(request,unsigned,anchor,targetAdmitted){if(request.signerIdentityId!==anchor.identityId)throw new Error("request signer is not the pinned Epiphany Persona mouth identity");if(!verifyDomainPurpose(anchor.publicKey,requestSignatureDomain,requestSigningPurpose,unsigned,request.signerSignature))throw new Error("request signature is invalid");if(`sha256-${sha(Buffer.from(request.content,"utf8"))}`!==request.contentSha256)throw new Error("request content digest is invalid");const now=Date.now(),issued=Date.parse(request.issuedAt),expires=Date.parse(request.expiresAt);if(issued>now+30_000)throw new Error("request issuance is in the future");if(expires<=issued||expires<now)throw new Error("request is expired");if(expires-issued>120_000)throw new Error("request lifetime exceeds 120 seconds");if(!targetAdmitted(request))throw new Error("request target is not an exact configured Bifrost Persona binding");}
function post(request,options){const args=[options.bridgeCliPath,"discord-post","--channel-id",request.channelId,"--content",request.content,"--identity",request.personaAgentId,"--source-kind","epiphany_persona_speech","--source-id",request.effectDocumentId,"--authority-ref","epiphany.persona_discord_delivery_request.v0","--epiphany-lane-id","Persona","--epiphany-agent-identity",request.personaAgentId,"--receipt-store",options.crossingReceiptStorePath??resolve(dirname(options.executionStorePath),"persona-discord-crossing-receipts.cc")];if(request.replyToMessageId)args.push("--reply-to-message-id",request.replyToMessageId);const capability=process.env.HEIMDALL_CAPABILITY_REF||process.env.BIFROST_HEIMDALL_CAPABILITY_REF;if(capability)args.push("--heimdall-capability-ref",capability);const result=spawnSync(process.execPath,args,{cwd:options.bifrostRoot,encoding:"utf8",windowsHide:true,env:process.env});if(result.status!==0||result.error)throw new Error(`Bifrost Discord transport returned indeterminate outcome: ${result.error?.message||result.stderr||result.stdout}`);const value=JSON.parse(result.stdout);if(value.action!=="discord-post"||value.ok!==true||value.channelId!==request.channelId||!value.messageId||!value.crossingReceiptId||!value.url)throw new Error("Bifrost Discord transport did not return exact delivery evidence");return value;}
async function terminal(status,request,digest,detail,c){const now=new Date().toISOString(),posted=c.posted??{},receipt={schemaName:"bifrost.persona_discord_delivery_receipt",schemaVersion:receiptSchema,receiptId:request.requestId,requestId:request.requestId,requestPayloadSha256:digest,status,channelId:request.channelId,replyToMessageId:request.replyToMessageId,messageId:status==="completed"?posted.messageId:"",transport:status==="completed"?posted.transport:"",crossingReceiptId:status==="completed"?posted.crossingReceiptId:"",receiptUrl:status==="completed"?posted.url:"",completedAt:now,providerIdentityId:c.identity.identityId,privateStateExposed:false,providerSignature:new Uint8Array()};receipt.providerSignature=new Uint8Array(signDomainPurpose(c.identity,receiptSignatureDomain,receiptSigningPurpose,c.runtime.encode(receiptSigningTuple(receipt))));await c.receipts.cache.put(c.defs.receipt,receipt.receiptId,receipt);await c.executions.cache.put(c.defs.execution,request.requestId,{schemaName:"bifrost.persona_discord_delivery_execution",schemaVersion:executionSchema,requestId:request.requestId,requestPayloadSha256:digest,status,recordedAt:now,detail});return receipt;}
async function open(runtime,definition,path){await mkdir(dirname(path),{recursive:true});const cache=runtime.CultCache.builder().withDocumentType(definition).withGenericStore(new runtime.SingleFileMessagePackBackingStore(path)).build();await cache.pullAllBackingStores();return {cache};}
async function readAnchor(runtime,path){const value=runtime.decode(await readFile(path));if(!Array.isArray(value)||value.length!==13)throw new Error("Epiphany Persona mouth trust anchor must be the exact service-contract tuple");const [schemaVersion,trustAnchorId,serviceId,runtimeId,identityId,rawPublicKey,algorithm,purpose,signedSchema,bindingAuthority,boundAt,expiresAt,privateStateExposed]=value,publicKey=rawPublicKey instanceof Uint8Array?rawPublicKey:Array.isArray(rawPublicKey)?Uint8Array.from(rawPublicKey):null,now=Date.now();if(schemaVersion!=="gamecult.service_trust_anchor.v1"||typeof trustAnchorId!=="string"||serviceId!=="epiphany-persona-mouth"||runtimeId!=="epiphany-yggdrasil"||typeof identityId!=="string"||!publicKey||publicKey.length!==32||algorithm!=="ed25519"||purpose!==requestSigningPurpose||signedSchema!=="epiphany.persona_discord_delivery_request.v0"||bindingAuthority!=="root"||!Number.isSafeInteger(boundAt)||boundAt<=0||boundAt>now||(expiresAt!==null&&(!Number.isSafeInteger(expiresAt)||expiresAt<=boundAt||expiresAt<=now))||privateStateExposed!==false)throw new Error("Epiphany Persona mouth service trust anchor is invalid");return {identityId,publicKey};}
function requireSeparate(paths){const keys=paths.map(p=>resolve(p).toLowerCase());if(new Set(keys).size!==keys.length)throw new Error("Persona Discord crossing and private execution stores must be distinct.");}
function sha(value){return createHash("sha256").update(value).digest("hex");}
function unwrap(value){return Array.isArray(value)?value[0]:value?.value??value;}

View file

@ -0,0 +1,36 @@
import {createHash,randomBytes} from "node:crypto";
import {createSocket} from "node:dgram";
import {createRequire} from "node:module";
import {existsSync} from "node:fs";
import {readFile} from "node:fs/promises";
import {resolve} from "node:path";
import {loadDomainIdentity,signDomainPurpose,verifyDomainPurpose} from "./bifrost-signed-identity.mjs";
export const permitRequestSchema="bifrost.persona_discord_delivery_permit_request.v0";
export const permitSchema="epiphany.persona_discord_delivery_permit.v0";
export const permitRequestPurpose="bifrost.persona-discord.delivery-permit-request.v0";
export const permitPurpose="epiphany.persona-discord.delivery-permit.v0";
export const permitRequestIdentityDomain="bifrost.persona-discord-delivery-permit-request.identity.v0\0";
export const permitRequestSignatureDomain="bifrost.persona-discord-delivery-permit-request.signature.v0\0";
export const permitIdentityDomain="epiphany.persona-discord-delivery-permit.identity.v0\0";
export const permitSignatureDomain="epiphany.persona-discord-delivery-permit.signature.v0\0";
const connectionId=0xe91f0002;
export const permitRequestTuple=v=>[v.schemaVersion,v.requestId,v.requestPayloadSha256,v.targetRuntimeId,v.nonce,v.issuedAt,v.expiresAt,v.requesterIdentityId,v.privateStateExposed];
export const permitTuple=v=>[v.schemaVersion,v.permitId,v.requestId,v.requestPayloadSha256,v.targetRuntimeId,v.nonce,v.requesterIdentityId,v.brakeStateDocumentId,v.brakeStateDocumentSha256,v.brakeObservedAt,v.issuedAt,v.expiresAt,v.providerIdentityId,v.privateStateExposed];
export async function requestPersonaDiscordPermit({endpoint,requestId,requestPayloadSha256,targetRuntimeId,nonce,issuedAt,expiresAt,privateKeyPath,epiphanyPermitAnchorPath,timeoutMs=5000,runtime:provided,socketFactory=createSocket,connectionFactory}){
const runtime=provided??loadRuntime(),identity=await loadDomainIdentity(privateKeyPath,"Bifrost Persona Discord permit requester identity",permitRequestIdentityDomain),anchor=await readAnchor(runtime,epiphanyPermitAnchorPath),now=Date.now();
const request={schemaVersion:permitRequestSchema,requestId,requestPayloadSha256,targetRuntimeId,nonce:nonce??randomBytes(16).toString("hex"),issuedAt:issuedAt??new Date(now).toISOString(),expiresAt:expiresAt??new Date(now+5000).toISOString(),requesterIdentityId:identity.identityId,privateStateExposed:false,requesterSignature:new Uint8Array()};
request.requesterSignature=new Uint8Array(signDomainPurpose(identity,permitRequestSignatureDomain,permitRequestPurpose,runtime.msgpack.encode(permitRequestTuple(request))));
const {host,port}=parseEndpoint(endpoint),socket=socketFactory("udp4");await bind(socket);const connection=(connectionFactory??(o=>new runtime.cultnet.CultNetRudpSocketTransportConnection(o)))({runtimeId:"bifrost-discord-yggdrasil",socket,mode:"client",remoteHost:host,remotePort:port,connectionId,resendDelayMs:50,resendPollMs:10});
try{connection.connect();await connected(connection,timeoutMs);const response=wait(connection,runtime.msgpack,timeoutMs,request);const message={schemaVersion:"cultnet.document_put_raw.v0",messageId:`permit-request-${requestId}`,document:{schemaId:permitRequestSchema,recordKey:requestId,storedAt:request.issuedAt,payloadEncoding:"messagepack",payload:runtime.msgpack.encode(request),sourceRuntimeId:"bifrost-discord-yggdrasil",sourceAgentId:identity.identityId,sourceRole:"bifrost-persona-discord-delivery",tags:["cultnet.transport.rudp.v0"]}};connection.send("schema",runtime.msgpack.encode(runtime.cultnet.encodeCultNetMessageForWire(message,"cultnet.schema.v0")));const permit=await response;validatePermit(permit,request,anchor,runtime.msgpack.encode);return permit;}finally{connection.close();}
}
export function validatePermit(v,request,anchor,encode){if(!v||v.schemaVersion!==permitSchema||v.requestId!==request.requestId||v.requestPayloadSha256!==request.requestPayloadSha256||v.targetRuntimeId!==request.targetRuntimeId||v.nonce!==request.nonce||v.requesterIdentityId!==request.requesterIdentityId||v.providerIdentityId!==anchor.identityId||v.privateStateExposed!==false||!/^sha256-[0-9a-f]{64}$/.test(v.brakeStateDocumentSha256)||!(v.providerSignature instanceof Uint8Array)||v.providerSignature.length!==64)throw new Error("Epiphany permit substituted delivery authority");const issued=Date.parse(v.issuedAt),expires=Date.parse(v.expiresAt),observed=Date.parse(v.brakeObservedAt),now=Date.now();if(!Number.isFinite(issued)||!Number.isFinite(expires)||!Number.isFinite(observed)||expires<=issued||expires-issued>5000||expires<now||observed>issued||issued>now+1000)throw new Error("Epiphany permit is stale or has an invalid lifetime");if(!verifyDomainPurpose(anchor.publicKey,permitSignatureDomain,permitPurpose,encode(permitTuple(v)),v.providerSignature))throw new Error("Epiphany permit signature is invalid");return v;}
async function readAnchor(runtime,path){const a=runtime.msgpack.decode(await readFile(path));if(!Array.isArray(a)||a.length!==13)throw new Error("Epiphany permit anchor must be an exact service anchor");const [schemaVersion,,serviceId,runtimeId,identityId,publicKey,algorithm,purpose,signedSchema,authority,boundAt,expiresAt,privateStateExposed]=a,now=Date.now();if(schemaVersion!=="gamecult.service_trust_anchor.v1"||serviceId!=="epiphany-persona-discord-permit"||runtimeId!=="epiphany-yggdrasil"||!(publicKey instanceof Uint8Array)||publicKey.length!==32||algorithm!=="ed25519"||purpose!==permitPurpose||signedSchema!==permitSchema||authority!=="root"||boundAt>now||(expiresAt!==null&&expiresAt<=now)||privateStateExposed!==false)throw new Error("Epiphany permit anchor is invalid");return {identityId,publicKey};}
function wait(c,m,t,r){return new Promise((ok,no)=>{const done=(f,v)=>{clearTimeout(timer);c.off?.("frame",frame);c.off?.("error",error);f(v)},error=e=>done(no,e),frame=x=>{try{if(x.channelId!=="schema")throw new Error("permit arrived on foreign channel");const w=m.decode(x.payload),d=w?.document,p=m.decode(d?.payload);if(w?.schemaVersion!=="cultnet.document_put_raw.v0"||d?.schemaId!==permitSchema||d?.recordKey!==`permit:${r.requestPayloadSha256}`||d?.payloadEncoding!=="messagepack"||d?.sourceRuntimeId!==r.targetRuntimeId||d?.sourceRole!=="epiphany-persona-discord-permit"||d?.sourceAgentId!==p?.providerIdentityId)throw new Error("permit envelope substituted authority");done(ok,p)}catch(e){done(no,e)}};const timer=setTimeout(()=>done(no,new Error("Timed out waiting for Epiphany Persona Discord permit")),t);c.on("frame",frame);c.on("error",error)})}
async function connected(c,t){const s=Date.now();while(!c.connected){if(Date.now()-s>=t)throw new Error("Timed out establishing permit RUDP session");await new Promise(r=>setTimeout(r,5));}}
function bind(s){return new Promise((ok,no)=>{s.once("error",no);s.once("listening",ok);s.bind(0,"0.0.0.0")})}
function parseEndpoint(v){const u=new URL(v);if(u.protocol!=="rudp:"||!u.hostname||!u.port)throw new Error("permit endpoint must be rudp://host:port");return {host:u.hostname,port:Number(u.port)}}
function loadRuntime(){const root=resolve(import.meta.dirname,".."),cult=resolve(process.env.VOIDBOT_CULTLIB_ROOT||resolve(root,"..","CultLib")),pkg=resolve(cult,"packages","cultnet-ts","package.json");if(!existsSync(pkg))throw new Error("CultNet runtime unavailable");const req=createRequire(pkg);return {cultnet:req("cultnet-ts"),msgpack:req("@msgpack/msgpack")};}

View file

@ -1,5 +1,5 @@
export const feedbackEventType = "voidbot.discord.persona_feedback_event";
export const feedbackEventSchema = "voidbot.discord.persona_feedback_event.v0";
export const feedbackEventType = "bifrost.discord.persona_conversation_event";
export const feedbackEventSchema = "bifrost.discord.persona_conversation_event.v0";
export const feedbackBindingType = "bifrost.persona_feedback.target_binding";
export const feedbackBindingSchema = "bifrost.persona_feedback.target_binding.v0";
export const actorLinkType = "bifrost.discord.actor_link";
@ -8,6 +8,8 @@ export const feedbackReceiptType = "bifrost.persona_feedback.admission_receipt";
export const feedbackReceiptSchema = "bifrost.persona_feedback.admission_receipt.v0";
export const feedbackDeliveryType = "bifrost.persona_feedback.delivery";
export const feedbackDeliverySchema = "bifrost.persona_feedback.delivery.v0";
export const feedbackPendingFailureType = "bifrost.persona_feedback.pending_failure";
export const feedbackPendingFailureSchema = "bifrost.persona_feedback.pending_failure.v0";
// Kept as explicit constructors because CultCache document metadata is part of the wire contract.
export function buildFeedbackDefinitions(defineDocumentType) {
@ -18,6 +20,7 @@ export function buildFeedbackDefinitions(defineDocumentType) {
actorLink: make(actorLinkType, actorLinkSchema, "linkId", parseActorLink),
receipt: make(feedbackReceiptType, feedbackReceiptSchema, "receiptId", parseReceipt),
delivery: make(feedbackDeliveryType, feedbackDeliverySchema, "admissionId", parseDelivery),
pendingFailure: make(feedbackPendingFailureType, feedbackPendingFailureSchema, "failureId", parsePendingFailure),
};
}
@ -25,7 +28,7 @@ export function parseEvent(value) {
const v = record(value, "feedback event");
exact(v.schemaVersion, feedbackEventSchema, "event schema");
for (const key of ["eventId","guildId","channelId","messageId","authorId","addressingMode","targetPersonaId","targetRepoName","targetRuntimeId","content","payloadHash","producerId","producerRuntimeId"]) text(v[key], key);
exact(v.producerId, "voidbot", "producer"); exact(v.authorityClass, "feedback_only", "authorityClass");
exact(v.producerId, "bifrost-discord", "producer"); exact(v.authorityClass, "feedback_only", "authorityClass");
exact(v.status, "pending", "status");
if(!["role","text","reply","broadcast"].includes(v.addressingMode))throw new Error("addressingMode is invalid.");
if(!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(v.observedAt)||Number.isNaN(Date.parse(v.observedAt)))throw new Error("observedAt must be RFC3339.");
@ -35,7 +38,8 @@ export function parseEvent(value) {
export function parseBinding(value) { const v = record(value,"target binding"); exact(v.schemaVersion,feedbackBindingSchema,"binding schema"); for (const k of ["bindingId","guildId","channelId","targetPersonaId","targetRepoName","targetRuntimeId","producerId","producerRuntimeId","sourceVisibility","dataClassification","deliveryStorePath"]) text(v[k],k); const expected={public:"public_feedback",organization:"organization_feedback",private:"private_feedback"}[v.sourceVisibility];if(!expected||v.dataClassification!==expected)throw new Error("sourceVisibility and dataClassification must be the exact admitted pair."); return v; }
export function parseActorLink(value) { const v=record(value,"actor link"); exact(v.schemaVersion,actorLinkSchema,"actor link schema"); for(const k of ["linkId","guildId","discordAuthorId","bifrostActorId","heimdallActorRef"]) text(v[k],k); return v; }
export function parseReceipt(value) { const v=record(value,"receipt"); exact(v.schemaVersion,feedbackReceiptSchema,"receipt schema"); for(const k of ["receiptId","eventId","status","classification","payloadHash","bindingId","recordedAt"]) text(v[k],k); return v; }
export function parseDelivery(value) { const v=record(value,"delivery"); exact(v.schemaVersion,feedbackDeliverySchema,"delivery schema"); for(const k of ["admissionId","packetSha256","sourceObserverId","sourceObserverRuntimeId","provider","bifrostAdmissionReceiptId","authority","providerIdentityId"]) text(v[k],k); exact(v.sourceObserverId,"voidbot","sourceObserverId"); exact(v.provider,"bifrost","provider"); exact(v.authority,"feedback_only","authority"); if(!(v.providerSignature instanceof Uint8Array)||v.providerSignature.length!==64)throw new Error("providerSignature must be 64 bytes."); const p=record(v.packet,"packet"); for(const k of ["feedbackId","sourceEventId","sourceActorId","actorClassification","discordGuildId","discordChannelId","discordMessageId","targetRuntimeId","targetRepository","targetPersonaId","sourceRoomId","feedbackText","contentSha256","sourceVisibility","dataClassification"])text(p[k],k);if(!Array.isArray(p.sourceDiscussionRefs)||p.sourceDiscussionRefs.length===0)throw new Error("sourceDiscussionRefs are required.");if(p.privateStateIncluded!==false)throw new Error("privateStateIncluded describes embedded private machine state and must be false.");return v; }
export function parseDelivery(value) { const v=record(value,"delivery"); exact(v.schemaVersion,feedbackDeliverySchema,"delivery schema"); for(const k of ["admissionId","packetSha256","sourceObserverId","sourceObserverRuntimeId","provider","bifrostAdmissionReceiptId","authority","providerIdentityId"]) text(v[k],k); exact(v.sourceObserverId,"bifrost-discord","sourceObserverId"); exact(v.provider,"bifrost","provider"); exact(v.authority,"feedback_only","authority"); if(!(v.providerSignature instanceof Uint8Array)||v.providerSignature.length!==64)throw new Error("providerSignature must be 64 bytes."); const p=record(v.packet,"packet"); for(const k of ["feedbackId","sourceEventId","sourceActorId","actorClassification","discordGuildId","discordChannelId","discordMessageId","targetRuntimeId","targetRepository","targetPersonaId","sourceRoomId","feedbackText","contentSha256","sourceVisibility","dataClassification"])text(p[k],k);if(!Array.isArray(p.sourceDiscussionRefs)||p.sourceDiscussionRefs.length===0)throw new Error("sourceDiscussionRefs are required.");if(p.privateStateIncluded!==false)throw new Error("privateStateIncluded describes embedded private machine state and must be false.");return v; }
export function parsePendingFailure(value) { const v=record(value,"pending failure"); exact(v.schemaVersion,feedbackPendingFailureSchema,"pending failure schema"); for(const k of ["failureId","eventId","lastError","lastAttemptAt"])text(v[k],k); if(v.failureId!==v.eventId)throw new Error("failureId must equal eventId."); if(!Number.isSafeInteger(v.attemptCount)||v.attemptCount<1||v.attemptCount>1_000_000)throw new Error("attemptCount is invalid."); if(Buffer.byteLength(v.lastError,"utf8")>512)throw new Error("lastError exceeds 512 UTF-8 bytes."); if(!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(v.lastAttemptAt)||Number.isNaN(Date.parse(v.lastAttemptAt)))throw new Error("lastAttemptAt must be RFC3339."); if(v.privateStateExposed!==false)throw new Error("privateStateExposed must be false."); return v; }
function record(value,label){if(!value||typeof value!=="object"||Array.isArray(value))throw new Error(`${label} must be an object.`);return value;}
function text(value,label){if(typeof value!=="string"||!value.trim())throw new Error(`${label} is required.`);return value;}
function exact(value,expected,label){if(value!==expected)throw new Error(`${label} must be ${expected}.`);}

View file

@ -4,15 +4,20 @@ import { createRequire } from "node:module";
import { constants } from "node:fs";
import { access, mkdir, realpath, rm, stat, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { buildFeedbackDefinitions, actorLinkSchema, feedbackBindingSchema, feedbackDeliverySchema, feedbackReceiptSchema } from "./persona-feedback-documents.mjs";
import { buildFeedbackDefinitions, actorLinkSchema, feedbackBindingSchema, feedbackDeliverySchema, feedbackReceiptSchema, feedbackPendingFailureSchema } from "./persona-feedback-documents.mjs";
import { defaultFeedbackIdentityPath, enrollBifrostFeedbackIdentity, loadBifrostFeedbackIdentity, providerHealthTrustAnchor, signFeedbackAdmission, trustAnchor } from "./bifrost-feedback-identity.mjs";
import { createPersonaFeedbackHealthPublisher, publishPersonaFeedbackHealth } from "./persona-feedback-idunn-health.mjs";
import { discordGatewayReady, serveDiscordPersonaIngress } from "./bifrost-discord-persona-ingress.mjs";
import { enrollPersonaDiscordDeliveryIdentity, exportPersonaDiscordPermitRequestAnchor, inspectPersonaDiscordDeliveryReadiness, processPersonaDiscordDeliveries } from "./persona-discord-delivery.mjs";
const root=resolve(import.meta.dirname,".."),projects=resolve(root,".."),cult=resolve(process.env.VOIDBOT_CULTLIB_ROOT||resolve(projects,"CultLib"));
const [command,...rawArgs]=process.argv.slice(2),options=parseArgs(rawArgs);
const privateStorePath=resolve(options.store||(process.platform==="win32"?resolve(root,".bifrost","provider-store.cc"):"/var/lib/gamecult/bifrost/persona-feedback/provider.cc"));
const observationStorePath=resolve(options["observation-store"]||(process.platform==="win32"?resolve(root,".bifrost","persona-feedback-observations.cc"):"/srv/bifrost/persona-feedback/observations.cc"));
const observationStorePath=resolve(options["observation-store"]||(process.platform==="win32"?resolve(root,".bifrost","persona-conversation-observations.cc"):"/srv/bifrost/persona-feedback/observations.cc"));
const deliveryStorePath=resolve(options["delivery-store"]||(process.platform==="win32"?resolve(root,".bifrost","epiphany-persona-feedback.cc"):"/srv/bifrost/persona-feedback/deliveries.cc"));
const personaDeliveryRequestStorePath=resolve(options["persona-delivery-request-store"]||(process.platform==="win32"?resolve(root,".bifrost","persona-discord-requests.cc"):"/srv/bifrost/persona-discord-crossing/requests.cc"));
const personaDeliveryReceiptStorePath=resolve(options["persona-delivery-receipt-store"]||(process.platform==="win32"?resolve(root,".bifrost","persona-discord-receipts.cc"):"/srv/bifrost/persona-discord-crossing/receipts.cc"));
const personaDeliveryExecutionStorePath=resolve(options["persona-delivery-execution-store"]||(process.platform==="win32"?resolve(root,".bifrost","private","persona-discord-executions.cc"):"/var/lib/gamecult/bifrost/persona-feedback/persona-discord-executions.cc"));
const runtime=loadRuntime(),defs=buildFeedbackDefinitions(runtime.defineDocumentType);
try{await main();}catch(error){console.error(error instanceof Error?error.message:String(error));process.exitCode=1;}
@ -22,16 +27,19 @@ async function main(){
if(command==="link-actor")return linkActor();
if(command==="process")return processEvent(requireOption("event-id"));
if(command==="serve")return serve();
if(command==="serve-discord")return serveDiscord();
if(command==="export")return exportSnapshot();
if(command==="enroll-identity")return enrollIdentity();
if(command==="enroll-persona-delivery-identity")return enrollPersonaDeliveryIdentity();
if(command==="export-persona-permit-request-anchor")return exportPersonaPermitRequestAnchor();
if(command==="export-provider-health-anchor")return exportProviderHealthAnchor();
if(command==="status")return status();
throw new Error("Usage: persona-feedback.mjs bind-target|link-actor|process|serve|status|export|enroll-identity|export-provider-health-anchor");
throw new Error("Usage: persona-feedback.mjs bind-target|link-actor|process|serve|serve-discord|status|export|enroll-identity|enroll-persona-delivery-identity|export-provider-health-anchor");
}
async function privateNode(){return runtime.CultMesh.createNode(privateStorePath,{documents:[defs.binding,defs.actorLink,defs.receipt,defs.delivery]});}
async function privateNode(){return runtime.CultMesh.createNode(privateStorePath,{documents:[defs.binding,defs.actorLink,defs.receipt,defs.delivery,defs.pendingFailure]});}
async function observationNode(){return runtime.CultMesh.createNode(observationStorePath,{documents:[defs.event]});}
async function bindTarget(){const n=await privateNode(),binding={schemaName:"bifrost.persona_feedback.target_binding",schemaVersion:feedbackBindingSchema,bindingId:bindingId(options),guildId:requireOption("guild-id"),channelId:requireOption("channel-id"),targetPersonaId:requireOption("persona-id"),targetRepoName:requireOption("repo"),targetRuntimeId:requireOption("runtime-id"),producerId:options["producer-id"]||"voidbot",producerRuntimeId:requireOption("producer-runtime-id"),sourceVisibility:requireOption("source-visibility"),dataClassification:requireOption("data-classification"),deliveryStorePath:resolve(options["delivery-store"]||deliveryStorePath),createdAt:new Date().toISOString()};await assertStoreSeparation(binding.deliveryStorePath);await n.put(defs.binding,binding.bindingId,binding);await n.flush?.();print(binding);}
async function bindTarget(){const n=await privateNode(),binding={schemaName:"bifrost.persona_feedback.target_binding",schemaVersion:feedbackBindingSchema,bindingId:bindingId(options),guildId:requireOption("guild-id"),channelId:requireOption("channel-id"),targetPersonaId:requireOption("persona-id"),targetRepoName:requireOption("repo"),targetRuntimeId:requireOption("runtime-id"),producerId:options["producer-id"]||"bifrost-discord",producerRuntimeId:requireOption("producer-runtime-id"),sourceVisibility:requireOption("source-visibility"),dataClassification:requireOption("data-classification"),deliveryStorePath:resolve(options["delivery-store"]||deliveryStorePath),createdAt:new Date().toISOString()};await assertStoreSeparation(binding.deliveryStorePath);await n.put(defs.binding,binding.bindingId,binding);await n.flush?.();print(binding);}
async function linkActor(){const n=await privateNode(),link={schemaName:"bifrost.discord.actor_link",schemaVersion:actorLinkSchema,linkId:`${requireOption("guild-id")}:${requireOption("discord-author-id")}`,guildId:requireOption("guild-id"),discordAuthorId:requireOption("discord-author-id"),bifrostActorId:requireOption("bifrost-actor-id"),heimdallActorRef:requireOption("heimdall-actor-ref"),linkedAt:new Date().toISOString()};await n.put(defs.actorLink,link.linkId,link);await n.flush?.();print(link);}
async function processEvent(id,emit=true){
@ -45,7 +53,7 @@ async function processEvent(id,emit=true){
const link=unwrap(await privateNodeValue.get(defs.actorLink,`${event.guildId}:${event.authorId}`)),classification=link?"linked_governance_feedback":"unlinked_social_feedback",sourceActorId=link?.bifrostActorId||`discord:${event.guildId}:${event.authorId}`;
const packet={feedbackId:id,sourceEventId:id,sourceActorId,actorClassification:classification,actorLinkRefs:link?[link.heimdallActorRef]:[],discordGuildId:event.guildId,discordChannelId:event.channelId,discordMessageId:event.messageId,targetRuntimeId:event.targetRuntimeId,targetRepository:event.targetRepoName,targetPersonaId:event.targetPersonaId,sourceRoomId:`discord:${event.guildId}:${event.channelId}`,feedbackText:event.content,contentSha256:`sha256-${event.payloadHash}`,sourceDiscussionRefs:[`discord:${event.guildId}:${event.channelId}:${event.messageId}`],sourceVisibility:binding.sourceVisibility,dataClassification:binding.dataClassification,privateStateIncluded:false};
const identity=await loadBifrostFeedbackIdentity(resolve(options["private-key"]||defaultFeedbackIdentityPath()));
const admission={schemaName:"bifrost.persona_feedback.delivery",schemaVersion:feedbackDeliverySchema,admissionId:id,packet,packetSha256:`sha256-${shaBytes(runtime.encode(packetTuple(packet)))}`,sourceObserverId:"voidbot",sourceObserverRuntimeId:event.producerRuntimeId,provider:"bifrost",bifrostAdmissionReceiptId:id,authority:"feedback_only",providerIdentityId:identity.identityId,providerSignature:new Uint8Array()};
const admission={schemaName:"bifrost.persona_feedback.delivery",schemaVersion:feedbackDeliverySchema,admissionId:id,packet,packetSha256:`sha256-${shaBytes(runtime.encode(packetTuple(packet)))}`,sourceObserverId:"bifrost-discord",sourceObserverRuntimeId:event.producerRuntimeId,provider:"bifrost",bifrostAdmissionReceiptId:id,authority:"feedback_only",providerIdentityId:identity.identityId,providerSignature:new Uint8Array()};
admission.providerSignature=new Uint8Array(signFeedbackAdmission(identity,runtime.encode(admissionSigningTuple(admission))));
const receipt={schemaName:"bifrost.persona_feedback.admission_receipt",schemaVersion:feedbackReceiptSchema,receiptId:id,eventId:id,status:"admitted",classification,payloadHash:event.payloadHash,bindingId:binding.bindingId,deliveryId:id,recordedAt:new Date().toISOString(),immutable:true,grantsWorkAuthority:false,grantsAdoptionAuthority:false};
await publishAdmission(admission,binding.deliveryStorePath);await privateNodeValue.put(defs.delivery,id,admission);await privateNodeValue.put(defs.receipt,id,receipt);await privateNodeValue.flush?.();if(emit)print({receipt,delivery:admission,deliveryStore:binding.deliveryStorePath});
@ -53,16 +61,51 @@ async function processEvent(id,emit=true){
async function republishExisting(n,id){const admission=unwrap(await n.get(defs.delivery,id));if(!admission)throw new Error(`Feedback receipt ${id} has no signed admission.`);const binding=unwrap(await n.get(defs.binding,bindingIdFromAdmission(admission)));if(!binding)throw new Error(`Feedback admission ${id} has no target binding.`);await publishAdmission(admission,binding.deliveryStorePath);}
async function publishAdmission(admission,path){await assertStoreSeparation(path);await mkdir(dirname(path),{recursive:true});const cache=runtime.CultCache.builder().withDocumentType(defs.delivery).withGenericStore(new runtime.SingleFileMessagePackBackingStore(path)).build();await cache.pullAllBackingStores();const current=cache.get(defs.delivery,admission.admissionId);if(current&&stable(current)!==stable(admission))throw new Error(`Signed admission ${admission.admissionId} already exists with different immutable content.`);if(!current)await cache.put(defs.delivery,admission.admissionId,admission);}
async function serve(){const interval=positiveInteger(options["interval-ms"]||"5000","interval-ms"),once=options.once==="true",healthEndpoint=options["idunn-rudp-health"]||process.env.BIFROST_IDUNN_RUDP_HEALTH;if(!healthEndpoint&&!once)throw new Error("Persona-feedback serve requires --idunn-rudp-health or BIFROST_IDUNN_RUDP_HEALTH; no localhost default is assumed.");const healthPublisher=healthEndpoint?createPersonaFeedbackHealthPublisher(await loadBifrostFeedbackIdentity(resolve(options["private-key"]||defaultFeedbackIdentityPath()))):null;do{const observations=await observationNode(),privateNodeValue=await privateNode();await observations.cache?.pullAllBackingStores?.();await privateNodeValue.cache?.pullAllBackingStores?.();const admitted=new Set(privateNodeValue.cache.getAll(defs.receipt).map(value=>value.eventId));const pending=observations.cache.getAll(defs.event).filter(event=>!admitted.has(event.eventId)).sort((a,b)=>String(a.observedAt).localeCompare(String(b.observedAt)));const results=[];for(const event of pending){try{await processEvent(event.eventId,false);results.push({eventId:event.eventId,ok:true,error:""});}catch(error){results.push({eventId:event.eventId,ok:false,error:error instanceof Error?error.message:String(error)});}}const readinessState=await readiness();if(healthEndpoint)await publishPersonaFeedbackHealth({endpoint:healthEndpoint,daemonId:"yggdrasil-bifrost-persona-feedback",healthContract:"bifrost.cultnet-rudp-persona-feedback-health",state:readinessState.status==="ready"?"active":"degraded",detail:readinessState.status==="ready"?"ready":"readiness-degraded",publisher:healthPublisher,publishUnsignedDiagnostic:true});if(once){print({ok:results.every(value=>value.ok)&&readinessState.status==="ready",scanned:pending.length,results,readiness:readinessState});return;}await new Promise(done=>setTimeout(done,interval));}while(true);}
async function serve(){const interval=positiveInteger(options["interval-ms"]||"5000","interval-ms"),once=options.once==="true",healthEndpoint=options["idunn-rudp-health"]||process.env.BIFROST_IDUNN_RUDP_HEALTH;if(!healthEndpoint&&!once)throw new Error("Persona-feedback serve requires --idunn-rudp-health or BIFROST_IDUNN_RUDP_HEALTH; no localhost default is assumed.");const healthPublisher=healthEndpoint?createPersonaFeedbackHealthPublisher(await loadBifrostFeedbackIdentity(resolve(options["private-key"]||defaultFeedbackIdentityPath()))):null;do{const results=await drainPendingObservations(),readinessState=await readiness();if(healthEndpoint)await publishPersonaFeedbackHealth({endpoint:healthEndpoint,daemonId:"yggdrasil-bifrost-persona-feedback",healthContract:"bifrost.cultnet-rudp-persona-feedback-health",state:readinessState.status==="ready"?"active":"degraded",detail:readinessState.status==="ready"?"ready":"readiness-degraded",publisher:healthPublisher,publishUnsignedDiagnostic:true});if(once){print({ok:results.every(value=>value.ok)&&readinessState.status==="ready",scanned:results.length,results,readiness:readinessState});return;}await new Promise(done=>setTimeout(done,interval));}while(true);}
async function drainPendingObservations(){const observations=await observationNode(),privateNodeValue=await privateNode();await observations.cache?.pullAllBackingStores?.();await privateNodeValue.cache?.pullAllBackingStores?.();const admitted=new Set(privateNodeValue.cache.getAll(defs.receipt).map(value=>value.eventId));const pending=observations.cache.getAll(defs.event).filter(event=>!admitted.has(event.eventId)).sort((a,b)=>String(a.observedAt).localeCompare(String(b.observedAt)));const results=[];for(const event of pending){try{await processEvent(event.eventId,false);await privateNodeValue.cache.pullAllBackingStores();await privateNodeValue.cache.delete(defs.pendingFailure,event.eventId);results.push({eventId:event.eventId,ok:true,error:""});}catch(error){const message=boundedError(error),previous=unwrap(await privateNodeValue.get(defs.pendingFailure,event.eventId)),failure={schemaName:"bifrost.persona_feedback.pending_failure",schemaVersion:feedbackPendingFailureSchema,failureId:event.eventId,eventId:event.eventId,attemptCount:Math.min((previous?.attemptCount??0)+1,1_000_000),lastError:message,lastAttemptAt:new Date().toISOString(),privateStateExposed:false};await privateNodeValue.put(defs.pendingFailure,event.eventId,failure);results.push({eventId:event.eventId,ok:false,error:message});}}await privateNodeValue.flush?.();return results;}
async function serveDiscord(){
const token=process.env.BIFROST_DISCORD_BOT_TOKEN;
const producerRuntimeId=options["producer-runtime-id"]||process.env.BIFROST_DISCORD_RUNTIME_ID||"bifrost-discord-yggdrasil";
const healthEndpoint=options["idunn-rudp-health"]||process.env.BIFROST_IDUNN_RUDP_HEALTH;if(!healthEndpoint)throw new Error("Bifrost Discord Persona ingress requires --idunn-rudp-health or BIFROST_IDUNN_RUDP_HEALTH.");
const healthPublisher=createPersonaFeedbackHealthPublisher(await loadBifrostFeedbackIdentity(resolve(options["private-key"]||defaultFeedbackIdentityPath())));
let gatewayState={connected:false,sessionId:"",lastHeartbeatAckAtMillis:0,phase:"starting"};
const publishHealth=async()=>{const value=await readiness(),gatewayReady=discordGatewayReady(gatewayState),active=value.status==="ready"&&gatewayReady;await publishPersonaFeedbackHealth({endpoint:healthEndpoint,daemonId:"yggdrasil-bifrost-persona-feedback",healthContract:"bifrost.cultnet-rudp-persona-feedback-health",state:active?"active":"degraded",detail:active?"ready":`discord-${gatewayState.phase||"disconnected"}`,publisher:healthPublisher,publishUnsignedDiagnostic:true});return {...value,gatewayReady,gatewayState};};
const initial=await readiness();if(!initial.ingressReady)throw new Error(`Bifrost Discord Persona ingress is not ready: ${initial.ingressReasons.join("; ")}`);
const recovered=await drainPendingObservations();if(recovered.some(value=>!value.ok))console.error(`Bifrost Persona ingress startup recovery retained ${recovered.filter(value=>!value.ok).length} failed observation(s) for reconnect retry.`);
await drainPersonaDeliveries().catch(error=>console.error(`Bifrost Persona delivery is staged but not yet ready: ${error instanceof Error?error.message:String(error)}`));
await publishHealth();
const healthTimer=setInterval(()=>publishHealth().catch(error=>console.error(`Bifrost Persona-feedback health publication failed: ${error instanceof Error?error.message:String(error)}`)),10000);
let observationBusy=false;const observationTimer=setInterval(async()=>{if(observationBusy)return;observationBusy=true;try{const retried=await drainPendingObservations();if(retried.some(value=>!value.ok))console.error(`Bifrost Persona ingress retained ${retried.filter(value=>!value.ok).length} failed pending observation(s).`);}catch(error){console.error(`Bifrost Persona observation reconciliation failed: ${error instanceof Error?error.message:String(error)}`);}finally{observationBusy=false;}},positiveInteger(options["observation-retry-interval-ms"]||"5000","observation-retry-interval-ms"));
let deliveryBusy=false;const deliveryTimer=setInterval(async()=>{if(deliveryBusy)return;deliveryBusy=true;try{await drainPersonaDeliveries();}catch(error){console.error(`Bifrost Persona delivery poll failed: ${error instanceof Error?error.message:String(error)}`);}finally{deliveryBusy=false;}},positiveInteger(options["persona-delivery-interval-ms"]||"500","persona-delivery-interval-ms"));
try{await serveDiscordPersonaIngress({token,runtimeId:producerRuntimeId,onConversation:conversation=>admitDiscordConversation(conversation,producerRuntimeId),onReady:async state=>{const drained=await drainPendingObservations();print({schemaVersion:"bifrost.discord.persona_ingress_status.v0",status:drained.every(value=>value.ok)?"ready":"degraded",runtimeId:state.runtimeId,botUserId:state.botUserId,recoveredObservationCount:drained.length,privateStateExposed:false});},onState:state=>{gatewayState=state;},onDisconnect:error=>console.error(`Bifrost Discord Persona ingress reconnecting: ${error instanceof Error?error.message:String(error)}`)});}finally{gatewayState={connected:false,sessionId:"",lastHeartbeatAckAtMillis:0,phase:"stopped"};await publishHealth().catch(()=>{});clearInterval(healthTimer);clearInterval(observationTimer);clearInterval(deliveryTimer);}
}
async function drainPersonaDeliveries(){const n=await privateNode();await n.cache?.pullAllBackingStores?.();const bindings=n.cache.getAll(defs.binding);return processPersonaDiscordDeliveries({runtime,permitRuntime:{cultnet:runtime.cultnet,msgpack:runtime.msgpack},requestStorePath:personaDeliveryRequestStorePath,receiptStorePath:personaDeliveryReceiptStorePath,executionStorePath:personaDeliveryExecutionStorePath,epiphanyTrustAnchorPath:resolve(requireOption("epiphany-persona-mouth-trust-anchor")),epiphanyPermitAnchorPath:resolve(requireOption("epiphany-persona-permit-trust-anchor")),epiphanyPermitEndpoint:requireOption("epiphany-persona-permit-rudp"),privateKeyPath:resolve(requireOption("persona-delivery-private-key")),bridgeCliPath:resolve(root,"tools","bifrost-bridge.mjs"),bifrostRoot:root,targetAdmitted:request=>request.personaAgentId==="epiphany.Persona"&&bindings.some(binding=>binding.targetRuntimeId===request.targetRuntimeId&&binding.targetPersonaId==="epiphany"&&binding.channelId===request.channelId)});}
async function admitDiscordConversation(conversation,producerRuntimeId){
const privateNodeValue=await privateNode();await privateNodeValue.cache?.pullAllBackingStores?.();
const bindings=privateNodeValue.cache.getAll(defs.binding).filter(binding=>binding.guildId===conversation.guildId&&binding.channelId===conversation.channelId&&binding.producerId==="bifrost-discord"&&binding.producerRuntimeId===producerRuntimeId);
if(bindings.length===0)return;
if(bindings.length!==1)throw new Error("Discord channel has multiple Bifrost Persona targets; addressed conversation is ambiguous.");
const binding=bindings[0],eventId=`discord:${conversation.guildId}:${conversation.channelId}:${conversation.messageId}`,event={schemaName:"bifrost.discord.persona_conversation_event",schemaVersion:"bifrost.discord.persona_conversation_event.v0",eventId,observedAt:conversation.observedAt,guildId:conversation.guildId,channelId:conversation.channelId,messageId:conversation.messageId,authorId:conversation.authorId,authorName:conversation.authorName,addressingMode:conversation.addressingMode,targetPersonaId:binding.targetPersonaId,targetRepoName:binding.targetRepoName,targetRuntimeId:binding.targetRuntimeId,content:conversation.content,payloadHash:conversation.payloadHash,producerId:"bifrost-discord",producerRuntimeId,authorityClass:"feedback_only",status:"pending"};
const observations=await observationNode();await observations.cache?.pullAllBackingStores?.();const existing=unwrap(await observations.get(defs.event,eventId));
if(existing&&stable(existing)!==stable(event))throw new Error("Discord conversation event identity collision.");
if(!existing){await observations.put(defs.event,eventId,event);await observations.flush?.();}
await processEvent(eventId,false);
}
async function enrollIdentity(){const keyPath=resolve(options["private-key"]||defaultFeedbackIdentityPath()),identity=await enrollBifrostFeedbackIdentity(keyPath),createdAt=new Date().toISOString(),anchor=trustAnchor(identity,createdAt),out=resolve(requireOption("trust-anchor-out")),providerOut=options["provider-health-trust-anchor-out"]?resolve(options["provider-health-trust-anchor-out"]):null;await mkdir(dirname(out),{recursive:true});await writeFile(out,runtime.encode(anchorTuple(anchor)));if(providerOut){if(providerOut===out)throw new Error("Feedback and provider-health trust anchors must use distinct paths.");const providerAnchor=providerHealthTrustAnchor(identity,createdAt);await mkdir(dirname(providerOut),{recursive:true});await writeFile(providerOut,runtime.encode(anchorTuple(providerAnchor)));}print({privateKey:keyPath,trustAnchor:out,identityId:identity.identityId,providerHealthTrustAnchor:providerOut??""});}
async function enrollPersonaDeliveryIdentity(){print(await enrollPersonaDiscordDeliveryIdentity({runtime,privateKeyPath:resolve(requireOption("private-key")),trustAnchorOut:resolve(requireOption("trust-anchor-out"))}));}
async function exportPersonaPermitRequestAnchor(){print(await exportPersonaDiscordPermitRequestAnchor({runtime,privateKeyPath:resolve(requireOption("private-key")),trustAnchorOut:resolve(requireOption("trust-anchor-out"))}));}
async function exportProviderHealthAnchor(){const keyPath=resolve(options["private-key"]||defaultFeedbackIdentityPath()),identity=await loadBifrostFeedbackIdentity(keyPath),out=resolve(requireOption("trust-anchor-out")),anchor=providerHealthTrustAnchor(identity,new Date().toISOString());await mkdir(dirname(out),{recursive:true});await writeFile(out,runtime.encode(anchorTuple(anchor)),{flag:"wx",mode:0o640});print({privateKey:keyPath,trustAnchor:out,identityId:anchor.identityId});}
async function exportSnapshot(){const n=await privateNode();await n.cache?.pullAllBackingStores?.();const out=resolve(requireOption("out")),scratch=`${out}.scratch.cc`,cache=runtime.CultCache.builder().withDocumentType(defs.delivery).withDocumentType(defs.receipt).withGenericStore(new runtime.SingleFileMessagePackBackingStore(scratch)).build();for(const value of n.cache.getAll(defs.delivery))await cache.put(defs.delivery,value.admissionId,value);for(const value of n.cache.getAll(defs.receipt))await cache.put(defs.receipt,value.receiptId,value);const registry=new runtime.CultNetDocumentRegistry([runtime.defineCultNetDocumentBinding({definition:defs.delivery,payloadSchemaVersion:feedbackDeliverySchema}),runtime.defineCultNetDocumentBinding({definition:defs.receipt,payloadSchemaVersion:feedbackReceiptSchema})]),message=registry.createRawSnapshotResponse(cache,options["message-id"]||`persona-feedback-${randomUUID()}`);await mkdir(dirname(out),{recursive:true});await writeFile(out,runtime.encode(message));await rm(scratch,{force:true});print({schemaVersion:message.schemaVersion,messageId:message.messageId,documentCount:message.documents.length,out});}
async function readiness(){const reasons=[];let identity,bindingCount=0;try{identity=await loadBifrostFeedbackIdentity(resolve(options["private-key"]||defaultFeedbackIdentityPath()));}catch(error){reasons.push(`identity:${error instanceof Error?error.message:String(error)}`);}let bindings=[];try{const info=await stat(privateStorePath);if(!info.isFile())throw new Error("private provider store is not a regular file");const cache=runtime.CultCache.builder().withDocumentType(defs.binding).withDocumentType(defs.actorLink).withDocumentType(defs.receipt).withDocumentType(defs.delivery).withGenericStore(new runtime.SingleFileMessagePackBackingStore(privateStorePath)).build();await cache.pullAllBackingStores();bindings=cache.getAll(defs.binding);bindingCount=bindings.length;if(bindingCount===0)reasons.push("bindings:none");}catch(error){reasons.push(`privateStore:${error instanceof Error?error.message:String(error)}`);}for(const binding of bindings){try{await assertStoreSeparation(binding.deliveryStorePath);try{const info=await stat(binding.deliveryStorePath);if(!info.isFile())throw new Error("delivery path is not a regular file");await access(binding.deliveryStorePath,constants.W_OK);}catch(error){if(!error||typeof error!=="object"||error.code!=="ENOENT")throw error;await access(dirname(binding.deliveryStorePath),constants.W_OK);}}catch(error){reasons.push(`binding:${binding.bindingId}:${error instanceof Error?error.message:String(error)}`);}}return {schemaVersion:"bifrost.persona_feedback.readiness.v0",providerIdentityId:identity?.identityId??"",bindingCount,status:reasons.length===0?"ready":"degraded",reasons,privateStateExposed:false};}
async function readiness(){const ingressReasons=[];let identity,bindingCount=0,pendingFailedCount=0;try{identity=await loadBifrostFeedbackIdentity(resolve(options["private-key"]||defaultFeedbackIdentityPath()));}catch(error){ingressReasons.push(`identity:${error instanceof Error?error.message:String(error)}`);}let bindings=[];try{const info=await stat(privateStorePath);if(!info.isFile())throw new Error("private provider store is not a regular file");const cache=runtime.CultCache.builder().withDocumentType(defs.binding).withDocumentType(defs.actorLink).withDocumentType(defs.receipt).withDocumentType(defs.delivery).withDocumentType(defs.pendingFailure).withGenericStore(new runtime.SingleFileMessagePackBackingStore(privateStorePath)).build();await cache.pullAllBackingStores();bindings=cache.getAll(defs.binding);bindingCount=bindings.length;const admitted=new Set(cache.getAll(defs.receipt).map(value=>value.eventId));pendingFailedCount=cache.getAll(defs.pendingFailure).filter(value=>!admitted.has(value.eventId)).length;if(pendingFailedCount>0)ingressReasons.push(`pendingFailures:${pendingFailedCount}`);if(bindingCount===0)ingressReasons.push("bindings:none");}catch(error){ingressReasons.push(`privateStore:${error instanceof Error?error.message:String(error)}`);}for(const binding of bindings){try{await assertStoreSeparation(binding.deliveryStorePath);try{const info=await stat(binding.deliveryStorePath);if(!info.isFile())throw new Error("delivery path is not a regular file");await access(binding.deliveryStorePath,constants.W_OK);}catch(error){if(!error||typeof error!=="object"||error.code!=="ENOENT")throw error;await access(dirname(binding.deliveryStorePath),constants.W_OK);}}catch(error){ingressReasons.push(`binding:${binding.bindingId}:${error instanceof Error?error.message:String(error)}`);}}const personaConfigured=command==="serve-discord"||Boolean(options["epiphany-persona-mouth-trust-anchor"]);let personaDelivery={ready:false,requestSignerIdentityId:"",receiptSignerIdentityId:"",reasons:["not configured"]};if(personaConfigured){personaDelivery=await inspectPersonaDiscordDeliveryReadiness({runtime,requestStorePath:personaDeliveryRequestStorePath,receiptStorePath:personaDeliveryReceiptStorePath,executionStorePath:personaDeliveryExecutionStorePath,epiphanyTrustAnchorPath:resolve(requireOption("epiphany-persona-mouth-trust-anchor")),epiphanyPermitAnchorPath:resolve(requireOption("epiphany-persona-permit-trust-anchor")),epiphanyPermitEndpoint:requireOption("epiphany-persona-permit-rudp"),privateKeyPath:resolve(requireOption("persona-delivery-private-key"))});}const reasons=[...ingressReasons,...(personaConfigured?personaDelivery.reasons.map(reason=>`personaDelivery:${reason}`):[])],ingressReady=ingressReasons.length===0;return {schemaVersion:"bifrost.persona_feedback.readiness.v0",providerIdentityId:identity?.identityId??"",bindingCount,pendingFailedCount,ingressReady,ingressReasons,personaDelivery,status:ingressReady&&(!personaConfigured||personaDelivery.ready)?"ready":"degraded",reasons,privateStateExposed:false};}
async function status(){print(await readiness());}
function loadRuntime(){const cr=createRequire(resolve(cult,"packages","cultcache-ts","package.json")),mr=createRequire(resolve(cult,"packages","cultmesh-ts","package.json")),nr=createRequire(resolve(cult,"packages","cultnet-ts","package.json")),cc=cr("cultcache-ts"),cn=nr("cultnet-ts");return {CultMesh:mr("cultmesh-ts").CultMesh,CultCache:cc.CultCache,SingleFileMessagePackBackingStore:cc.SingleFileMessagePackBackingStore,defineDocumentType:cc.defineDocumentType,CultNetDocumentRegistry:cn.CultNetDocumentRegistry,defineCultNetDocumentBinding:cn.defineCultNetDocumentBinding,encode:nr("@msgpack/msgpack").encode};}
function bindingId(o){return `feedback:${o["guild-id"]}:${o["channel-id"]}:${o["runtime-id"]}:${o.repo}:${o["persona-id"]}:${o["producer-id"]||"voidbot"}:${o["producer-runtime-id"]}`;}
function loadRuntime(){const cr=createRequire(resolve(cult,"packages","cultcache-ts","package.json")),mr=createRequire(resolve(cult,"packages","cultmesh-ts","package.json")),nr=createRequire(resolve(cult,"packages","cultnet-ts","package.json")),cc=cr("cultcache-ts"),cn=nr("cultnet-ts"),mp=nr("@msgpack/msgpack");return {CultMesh:mr("cultmesh-ts").CultMesh,CultCache:cc.CultCache,SingleFileMessagePackBackingStore:cc.SingleFileMessagePackBackingStore,defineDocumentType:cc.defineDocumentType,CultNetDocumentRegistry:cn.CultNetDocumentRegistry,defineCultNetDocumentBinding:cn.defineCultNetDocumentBinding,cultnet:cn,msgpack:mp,encode:mp.encode,decode:mp.decode};}
function bindingId(o){return `feedback:${o["guild-id"]}:${o["channel-id"]}:${o["runtime-id"]}:${o.repo}:${o["persona-id"]}:${o["producer-id"]||"bifrost-discord"}:${o["producer-runtime-id"]}`;}
function bindingIdFromAdmission(admission){const p=admission.packet;return bindingId({"guild-id":p.discordGuildId,"channel-id":p.discordChannelId,"runtime-id":p.targetRuntimeId,repo:p.targetRepository,"persona-id":p.targetPersonaId,"producer-id":admission.sourceObserverId,"producer-runtime-id":admission.sourceObserverRuntimeId});}
function packetTuple(p){return [p.feedbackId,p.sourceEventId,p.sourceActorId,p.actorClassification,p.actorLinkRefs,p.discordGuildId,p.discordChannelId,p.discordMessageId,p.targetRuntimeId,p.targetRepository,p.targetPersonaId,p.sourceRoomId,p.feedbackText,p.contentSha256,p.sourceDiscussionRefs,p.sourceVisibility,p.dataClassification,p.privateStateIncluded];}
function admissionSigningTuple(a){return [a.schemaVersion,a.admissionId,packetTuple(a.packet),a.packetSha256,a.sourceObserverId,a.sourceObserverRuntimeId,a.provider,a.bifrostAdmissionReceiptId,a.authority,a.providerIdentityId];}
@ -72,6 +115,7 @@ function requireOption(name){const value=options[name];if(!value)throw new Error
function unwrap(value){return Array.isArray(value)?value[0]:value?.value??value;}
function shaText(value){return createHash("sha256").update(value,"utf8").digest("hex");}
function shaBytes(value){return createHash("sha256").update(value).digest("hex");}
function boundedError(error){const value=error instanceof Error?error.message:String(error),bytes=Buffer.from(value,"utf8");if(bytes.length<=512)return value;let end=512;while(end>0&&(bytes[end]&0xc0)===0x80)end--;return bytes.subarray(0,end).toString("utf8");}
function stable(value){return JSON.stringify(value,(_key,current)=>current instanceof Uint8Array?Buffer.from(current).toString("hex"):current);}
function positiveInteger(value,label){const parsed=Number(value);if(!Number.isSafeInteger(parsed)||parsed<=0)throw new Error(`${label} must be a positive integer.`);return parsed;}
async function assertStoreSeparation(targetDeliveryStore){const paths=[observationStorePath,privateStorePath,resolve(targetDeliveryStore)],seenPaths=new Set(),seenFiles=new Set();for(const path of paths){const canonical=await canonicalPath(path);const key=process.platform==="win32"?canonical.toLowerCase():canonical;if(seenPaths.has(key))throw new Error("Persona feedback observation inbox, Bifrost private state, and delivery export must use distinct canonical paths.");seenPaths.add(key);try{const info=await stat(path),fileKey=`${info.dev}:${info.ino}`;if(seenFiles.has(fileKey))throw new Error("Persona feedback stores must not alias the same physical file.");seenFiles.add(fileKey);}catch(error){if(!error||typeof error!=="object"||error.code!=="ENOENT")throw error;}}}