diff --git a/tests/epiphany-operator-status-v2.test.mjs b/tests/epiphany-operator-status-v2.test.mjs new file mode 100644 index 0000000..7da0cac --- /dev/null +++ b/tests/epiphany-operator-status-v2.test.mjs @@ -0,0 +1,28 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { readFile,mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import { createRequire } from "node:module"; +import { signPurpose,trustAnchor } from "../tools/bifrost-signed-identity.mjs"; +import { enrollOperatorIdentity } from "../tools/epiphany-operator-commands.mjs"; +import { EPIPHANY_RESULT_SIGNING_PURPOSE,verifySealedReceipt } from "../tools/epiphany-operator-cultnet-rudp.mjs"; +import { parseOperatorResult,parseOperatorStatusV2,resultReceiptTuple } from "../tools/epiphany-operator-command-documents.mjs"; + +const requireCult=createRequire(resolve("..","CultLib","packages","cultnet-ts","package.json")); +const msgpack=requireCult("@msgpack/msgpack"); +const fixtureRoot=process.env.EPIPHANY_OPERATOR_RUST_FIXTURE??resolve("..","EpiphanyAgent",".epiphany-smoke","operator-command-interop-rust"); +async function golden(){const fixture=JSON.parse(await readFile(resolve(fixtureRoot,"protocol.json"),"utf8")).operatorStatusV2MigrationFixture;if(fixture)return fixture;const unavailable=(daemonId,healthContract)=>({daemonId,healthContract,availability:"unavailable",unavailableReason:"required-provider-record-missing",state:null,reasonCode:null,providerObservedAtUnixMillis:null,evaluatedAtUnixMillis:null,expiresAtUnixMillis:null,releaseId:null,releaseWitnessSha256:null,sourceCommit:null,deploymentId:null,projectionSha256:null});return {schemaVersion:"epiphany.operator.status.v2",releaseId:"release-fixture",releaseWitnessSha256:`sha256-${"a".repeat(64)}`,sourceCommit:"b".repeat(40),deploymentId:"deployment-fixture",coordinatorSnapshot:"coordinator-snapshot/sleeping",coordinatorState:"coordinator-state/ready",coordinatorAction:"coordinator-action/none",brakeStatus:"engaged",residentStatus:"resident-self/idle",pressureCount:0,pendingReviewCount:0,providerSetStatus:"incomplete",providers:[unavailable("yggdrasil-epiphany","epiphany.cultnet-rudp-runtime-health"),unavailable("yggdrasil-bifrost-persona-feedback","bifrost.cultnet-rudp-persona-feedback-health")],privateStateExposed:false};} +function admission(){return {schemaName:"bifrost.operator_command.delivery",schemaVersion:"bifrost.operator_command.delivery.v1",admissionId:"admission-status",packet:{commandId:"command-status",nonce:"nonce",sourceEventId:"event",sourceActorId:"actor",discordGuildId:"guild",discordChannelId:"ops",discordMessageId:"message",targetRuntimeId:"epiphany-yggdrasil",issuedAt:"2026-07-19T12:00:00Z",expiresAt:"2026-07-19T12:01:00Z",command:{kind:"status"}},packetSha256:`sha256-${"1".repeat(64)}`,sourceObserverId:"voidbot",sourceObserverRuntimeId:"voidbot-yggdrasil",provider:"bifrost",bifrostAdmissionReceiptId:"receipt",authority:"exact_operator_command_only",providerIdentityId:"bifrost",providerSignature:new Uint8Array(64)};} +async function sealed(statusV2){ + const a=admission(),root=await mkdtemp(resolve(tmpdir(),"bifrost-status-v2-")),identity=await enrollOperatorIdentity(resolve(root,"executor.seed")); + const result={schemaVersion:"epiphany.operator_command.status_result.v2",resultId:"result-status",commandId:a.packet.commandId,packetSha256:a.packetSha256,targetRuntimeId:a.packet.targetRuntimeId,disposition:"observed",consequenceKind:"operator-snapshot",consequenceRef:"snapshot",completedAt:"2026-07-19T12:00:01Z",privateStateExposed:false,operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"none",brakeStatus:"engaged",detail:"bounded",reviews:[],reviewCandidateId:"",reviewDecision:"",statusV2}; + const receipt={schemaVersion:"epiphany.operator_command.sealed_result.v1",result,resultPayloadSha256:`sha256-${createHash("sha256").update(msgpack.encode(result)).digest("hex")}`,commandId:result.commandId,packetSha256:result.packetSha256,targetRuntimeId:result.targetRuntimeId,completedAt:result.completedAt,providerIdentityId:identity.identityId,privateStateExposed:false,executorSignature:new Uint8Array()}; + receipt.executorSignature=new Uint8Array(signPurpose(identity,EPIPHANY_RESULT_SIGNING_PURPOSE,msgpack.encode(resultReceiptTuple(receipt))));return {a,identity,receipt}; +} + +test("exactly parses the Rust Status-v2 golden and preserves partial providers",async()=>{const value=await golden();assert.deepEqual(parseOperatorStatusV2(structuredClone(value)),value);const partial=structuredClone(value),p=partial.providers[0];Object.assign(p,{availability:"authenticated-current",unavailableReason:"",state:"healthy",reasonCode:"ok",providerObservedAtUnixMillis:1,evaluatedAtUnixMillis:2,expiresAtUnixMillis:3,releaseId:value.releaseId,releaseWitnessSha256:value.releaseWitnessSha256,sourceCommit:value.sourceCommit,deploymentId:value.deploymentId,projectionSha256:`sha256-${"3".repeat(64)}`});assert.equal(parseOperatorStatusV2(partial).providerSetStatus,"incomplete");}); +test("rejects extra, missing, private, overlong, reordered, and false-complete Status-v2 shapes",async()=>{const value=await golden(),cases=[{...value,privateKey:"x"},(()=>{const x=structuredClone(value);delete x.residentStatus;return x;})(),{...value,privateStateExposed:true},{...value,residentStatus:"x".repeat(257)},{...value,providers:[...value.providers].reverse()},{...value,providerSetStatus:"complete-authenticated"}];for(const hostile of cases)assert.throws(()=>parseOperatorStatusV2(hostile));}); +test("verifies the signed Status-v2 receipt and rejects nested mutation",async()=>{const built=await sealed(await golden()),trusted=trustAnchor(built.identity,"2026-07-19T11:00:00Z");assert.equal(verifySealedReceipt({receipt:built.receipt,admission:built.a,trustedIdentity:trusted,encode:msgpack.encode}),built.receipt);const mutated=structuredClone(built.receipt);mutated.result.statusV2.pressureCount++;assert.throws(()=>verifySealedReceipt({receipt:mutated,admission:built.a,trustedIdentity:trusted,encode:msgpack.encode}),/digest|signature/);}); +test("non-Status v1 remains exact and cannot smuggle statusV2",()=>{const result={schemaVersion:"epiphany.operator_command.result.v1",resultId:"r",commandId:"c",packetSha256:"p",targetRuntimeId:"t",disposition:"observed",consequenceKind:"snapshot",consequenceRef:"",completedAt:"2026-07-19T12:00:01Z",privateStateExposed:false,operatorStatus:"",stateStatus:"",coordinatorAction:"",brakeStatus:"",detail:"",reviews:[],reviewCandidateId:"",reviewDecision:"",statusV2:{}};assert.throws(()=>parseOperatorResult(result),/unknown or missing/);}); diff --git a/tools/epiphany-operator-command-documents.mjs b/tools/epiphany-operator-command-documents.mjs index 638cd2d..beccc8f 100644 --- a/tools/epiphany-operator-command-documents.mjs +++ b/tools/epiphany-operator-command-documents.mjs @@ -5,6 +5,7 @@ export const operatorAdmissionSchema="bifrost.operator_command.delivery.v1"; export const legacyOperatorAdmissionSchema="bifrost.operator_command.delivery.v0"; export const operatorResultType="epiphany.operator_command.result"; export const operatorResultSchema="epiphany.operator_command.result.v1"; +export const operatorStatusResultSchema="epiphany.operator_command.status_result.v2"; export const legacyOperatorResultSchema="epiphany.operator_command.result.v0"; export const operatorResultReceiptType="epiphany.operator_command.sealed_result"; export const operatorResultReceiptSchema="epiphany.operator_command.sealed_result.v1"; @@ -17,17 +18,30 @@ export const operatorDiscordDeliverySchema="bifrost.discord.epiphany_operator_de export function buildOperatorDefinitions(defineDocumentType){const make=(type,schemaId,name,parse)=>defineDocumentType({type,schemaName:type,schemaId,schemaVersion:schemaId,contentHash:schemaId,global:false,name,schema:{parse}});return {request:make(operatorRequestType,operatorRequestSchema,"requestId",parseOperatorRequest),admission:make(operatorAdmissionType,operatorAdmissionSchema,"admissionId",value=>parseOperatorAdmission(value,{allowLegacy:true})),result:make(operatorResultType,operatorResultSchema,"resultId",value=>parseOperatorResult(value,{allowLegacy:true})),sealedResult:make(operatorResultReceiptType,operatorResultReceiptSchema,"commandId",value=>parseOperatorResultReceipt(value,{allowLegacy:true})),terminal:make(operatorTerminalType,operatorTerminalSchema,"requestId",parseOperatorTerminal),discordDelivery:make(operatorDiscordDeliveryType,operatorDiscordDeliverySchema,"deliveryId",parseOperatorDiscordDelivery)};} export function parseOperatorRequest(value){const v=exactRecord(value,["schemaName","schemaVersion","requestId","commandId","nonce","sourceEventId","sourceActorDiscordId","discordGuildId","discordChannelId","discordMessageId","targetRuntimeId","issuedAt","expiresAt","producerId","producerRuntimeId","authorityClass","status","command"],"operator request"); exact(v.schemaName,operatorRequestType,"schemaName");exact(v.schemaVersion,operatorRequestSchema,"schemaVersion"); for(const k of ["requestId","commandId","nonce","sourceEventId","sourceActorDiscordId","discordGuildId","discordChannelId","discordMessageId","targetRuntimeId","producerRuntimeId"])text(v[k],k); exact(v.producerId,"voidbot","producerId");exact(v.authorityClass,"operator_request_only","authorityClass");exact(v.status,"pending","status"); const issued=date(v.issuedAt,"issuedAt"),expires=date(v.expiresAt,"expiresAt");if(expires<=issued||expires-issued>60000)throw new Error("operator request TTL must be positive and at most 60 seconds.");parseCommand(v.command);return v;} export function parseOperatorAdmission(v,{allowLegacy=false}={}){v=exactRecord(v,["schemaName","schemaVersion","admissionId","packet","packetSha256","sourceObserverId","sourceObserverRuntimeId","provider","bifrostAdmissionReceiptId","authority","providerIdentityId","providerSignature"],"operator admission");exact(v.schemaName,operatorAdmissionType,"schemaName");if(v.schemaVersion!==operatorAdmissionSchema&&!(allowLegacy&&v.schemaVersion===legacyOperatorAdmissionSchema))throw new Error("schemaVersion must be the current admission schema.");for(const k of ["admissionId","packetSha256","sourceObserverRuntimeId","bifrostAdmissionReceiptId","providerIdentityId"])text(v[k],k);exact(v.sourceObserverId,"voidbot","sourceObserverId");exact(v.provider,"bifrost","provider");exact(v.authority,"exact_operator_command_only","authority");v.providerSignature=bytes(v.providerSignature,"providerSignature",64);parsePacket(v.packet);if(v.schemaVersion===legacyOperatorAdmissionSchema&&["reviews","review"].includes(v.packet.command.kind))throw new Error("legacy v0 admission cannot carry Mind review commands.");return v;} -export function parseOperatorResult(v,{allowLegacy=false}={}){const legacy=v?.schemaVersion===legacyOperatorResultSchema;if(legacy&&!allowLegacy)throw new Error("legacy result is replay-only.");v=exactRecord(v,legacy?["schemaVersion","resultId","commandId","packetSha256","targetRuntimeId","disposition","consequenceKind","consequenceRef","completedAt","privateStateExposed","operatorStatus","stateStatus","coordinatorAction","brakeStatus","detail"]:["schemaVersion","resultId","commandId","packetSha256","targetRuntimeId","disposition","consequenceKind","consequenceRef","completedAt","privateStateExposed","operatorStatus","stateStatus","coordinatorAction","brakeStatus","detail","reviews","reviewCandidateId","reviewDecision"],"operator result");if(!legacy)exact(v.schemaVersion,operatorResultSchema,"schemaVersion");for(const k of ["resultId","commandId","packetSha256","targetRuntimeId","disposition","consequenceKind","completedAt"])text(v[k],k);if(!["observed","applied","refused"].includes(v.disposition))throw new Error("result disposition is invalid.");if(v.privateStateExposed!==false)throw new Error("operator result must not expose private state.");for(const k of ["consequenceRef","operatorStatus","stateStatus","coordinatorAction","brakeStatus","detail"]){if(typeof v[k]!=="string")throw new Error(`${k} must be a string.`);if(v[k].length>512)throw new Error(`${k} exceeds 512 characters.`);}if(!legacy){for(const k of ["reviewCandidateId","reviewDecision"]){if(typeof v[k]!=="string")throw new Error(`${k} must be a string.`);if(v[k].length>256)throw new Error(`${k} exceeds 256 characters.`);}if(!Array.isArray(v.reviews)||v.reviews.length>10)throw new Error("operator reviews must be a bounded array.");v.reviews.forEach(parseReviewSummary);if(v.reviewDecision&&!['adopt','refuse','hold'].includes(v.reviewDecision))throw new Error("reviewDecision is invalid.");}date(v.completedAt,"completedAt");return v;} +export function parseOperatorResult(v,{allowLegacy=false}={}){ + const legacy=v?.schemaVersion===legacyOperatorResultSchema,status=v?.schemaVersion===operatorStatusResultSchema; + if(legacy&&!allowLegacy)throw new Error("legacy result is replay-only."); + const base=["schemaVersion","resultId","commandId","packetSha256","targetRuntimeId","disposition","consequenceKind","consequenceRef","completedAt","privateStateExposed","operatorStatus","stateStatus","coordinatorAction","brakeStatus","detail"],current=[...base,"reviews","reviewCandidateId","reviewDecision"]; + v=exactRecord(v,legacy?base:status?[...current,"statusV2"]:current,"operator result"); + if(!legacy&&!status)exact(v.schemaVersion,operatorResultSchema,"schemaVersion"); + for(const k of ["resultId","commandId","packetSha256","targetRuntimeId","disposition","consequenceKind","completedAt"])text(v[k],k); + if(!["observed","applied","refused"].includes(v.disposition))throw new Error("result disposition is invalid."); + if(v.privateStateExposed!==false)throw new Error("operator result must not expose private state."); + for(const k of ["consequenceRef","operatorStatus","stateStatus","coordinatorAction","brakeStatus","detail"]){if(typeof v[k]!=="string")throw new Error(`${k} must be a string.`);if(v[k].length>512)throw new Error(`${k} exceeds 512 characters.`);} + if(!legacy){for(const k of ["reviewCandidateId","reviewDecision"]){if(typeof v[k]!=="string")throw new Error(`${k} must be a string.`);if(v[k].length>256)throw new Error(`${k} exceeds 256 characters.`);}if(!Array.isArray(v.reviews)||v.reviews.length>10)throw new Error("operator reviews must be a bounded array.");v.reviews.forEach(parseReviewSummary);if(v.reviewDecision&&!['adopt','refuse','hold'].includes(v.reviewDecision))throw new Error("reviewDecision is invalid.");} + if(status)parseOperatorStatusV2(v.statusV2); + date(v.completedAt,"completedAt");return v; +} export function parseOperatorResultReceipt(v,{allowLegacy=false}={}){v=exactRecord(v,["schemaVersion","result","resultPayloadSha256","commandId","packetSha256","targetRuntimeId","completedAt","providerIdentityId","privateStateExposed","executorSignature"],"operator sealed result receipt");const legacy=v.schemaVersion===legacyOperatorResultReceiptSchema;if(legacy&&!allowLegacy)throw new Error("legacy sealed result is replay-only.");if(!legacy)exact(v.schemaVersion,operatorResultReceiptSchema,"schemaVersion");parseOperatorResult(v.result,{allowLegacy});if(legacy!== (v.result.schemaVersion===legacyOperatorResultSchema))throw new Error("sealed result and embedded result versions differ.");for(const k of ["resultPayloadSha256","commandId","packetSha256","targetRuntimeId","completedAt","providerIdentityId"])text(v[k],k);date(v.completedAt,"completedAt");if(v.privateStateExposed!==false)throw new Error("operator sealed result receipt must not expose private state.");v.executorSignature=bytes(v.executorSignature,"executorSignature",64);return v;} export function parseOperatorTerminal(v){v=exactRecord(v,["schemaVersion","requestId","commandId","status","failureCode","recordedAt","retryable","privateStateExposed"],"operator terminal disposition");exact(v.schemaVersion,operatorTerminalSchema,"schemaVersion");for(const k of ["requestId","commandId","failureCode","recordedAt"])text(v[k],k);exact(v.status,"refused","status");date(v.recordedAt,"recordedAt");if(v.retryable!==false||v.privateStateExposed!==false)throw new Error("operator terminal disposition must be non-retryable and private-state-free.");return v;} -export function parseOperatorDiscordDelivery(v){v=exactRecord(v,["schemaVersion","deliveryId","requestId","requestPayloadSha256","commandId","discordGuildId","discordChannelId","discordInteractionId","targetRuntimeId","status","disposition","failureCode","detail","operatorStatus","stateStatus","coordinatorAction","brakeStatus","reviews","reviewCandidateId","reviewDecision","sealedResultPayloadSha256","executorSignatureSha256","resultProviderIdentityId","recordedAt","privateStateExposed"],"operator Discord delivery");exact(v.schemaVersion,operatorDiscordDeliverySchema,"schemaVersion");for(const k of ["deliveryId","requestId","requestPayloadSha256","commandId","discordGuildId","discordChannelId","discordInteractionId","targetRuntimeId","status","recordedAt"])text(v[k],k);if(!/^sha256-[0-9a-f]{64}$/.test(v.requestPayloadSha256))throw new Error("operator Discord delivery request digest is invalid.");if(!["completed","refused"].includes(v.status))throw new Error("operator Discord delivery status is invalid.");for(const k of ["disposition","failureCode","detail","operatorStatus","stateStatus","coordinatorAction","brakeStatus","reviewCandidateId","reviewDecision","sealedResultPayloadSha256","executorSignatureSha256","resultProviderIdentityId"])if(typeof v[k]!=="string")throw new Error(`${k} must be a string.`);if(!Array.isArray(v.reviews)||v.reviews.length>10)throw new Error("operator Discord delivery reviews must be bounded.");v.reviews.forEach(parseReviewSummary);if(v.reviewDecision&&!['adopt','refuse','hold'].includes(v.reviewDecision))throw new Error("operator Discord delivery reviewDecision is invalid.");for(const k of ["detail","operatorStatus","stateStatus","coordinatorAction","brakeStatus"])if(v[k].length>512)throw new Error(`operator Discord delivery ${k} exceeds 512 characters.`);if(v.status==="completed"&&(!v.disposition||!/^sha256-[0-9a-f]{64}$/.test(v.sealedResultPayloadSha256)||!/^sha256-[0-9a-f]{64}$/.test(v.executorSignatureSha256)||!v.resultProviderIdentityId))throw new Error("completed operator Discord delivery lacks sealed-result binding.");if(v.status==="refused"&&!v.failureCode)throw new Error("refused operator Discord delivery lacks failure code.");date(v.recordedAt,"recordedAt");if(v.privateStateExposed!==false)throw new Error("operator Discord delivery must not expose private state.");return v;} +export function parseOperatorDiscordDelivery(v){v=exactRecord(v,["schemaVersion","deliveryId","requestId","requestPayloadSha256","commandId","discordGuildId","discordChannelId","discordInteractionId","targetRuntimeId","status","disposition","failureCode","detail","operatorStatus","stateStatus","coordinatorAction","brakeStatus","reviews","reviewCandidateId","reviewDecision","statusV2","sealedResultPayloadSha256","executorSignatureSha256","resultProviderIdentityId","recordedAt","privateStateExposed"],"operator Discord delivery");exact(v.schemaVersion,operatorDiscordDeliverySchema,"schemaVersion");for(const k of ["deliveryId","requestId","requestPayloadSha256","commandId","discordGuildId","discordChannelId","discordInteractionId","targetRuntimeId","status","recordedAt"])text(v[k],k);if(!/^sha256-[0-9a-f]{64}$/.test(v.requestPayloadSha256))throw new Error("operator Discord delivery request digest is invalid.");if(!["completed","refused"].includes(v.status))throw new Error("operator Discord delivery status is invalid.");for(const k of ["disposition","failureCode","detail","operatorStatus","stateStatus","coordinatorAction","brakeStatus","reviewCandidateId","reviewDecision","sealedResultPayloadSha256","executorSignatureSha256","resultProviderIdentityId"])if(typeof v[k]!=="string")throw new Error(`${k} must be a string.`);if(!Array.isArray(v.reviews)||v.reviews.length>10)throw new Error("operator Discord delivery reviews must be bounded.");v.reviews.forEach(parseReviewSummary);if(v.reviewDecision&&!['adopt','refuse','hold'].includes(v.reviewDecision))throw new Error("operator Discord delivery reviewDecision is invalid.");for(const k of ["detail","operatorStatus","stateStatus","coordinatorAction","brakeStatus"])if(v[k].length>512)throw new Error(`operator Discord delivery ${k} exceeds 512 characters.`);if(v.status==="completed"&&(!v.disposition||!/^sha256-[0-9a-f]{64}$/.test(v.sealedResultPayloadSha256)||!/^sha256-[0-9a-f]{64}$/.test(v.executorSignatureSha256)||!v.resultProviderIdentityId))throw new Error("completed operator Discord delivery lacks sealed-result binding.");if(v.status==="refused"&&!v.failureCode)throw new Error("refused operator Discord delivery lacks failure code.");if(v.statusV2!==null)parseOperatorStatusV2(v.statusV2);if(v.status!=="completed"&&v.statusV2!==null)throw new Error("refused delivery cannot carry status v2.");date(v.recordedAt,"recordedAt");if(v.privateStateExposed!==false)throw new Error("operator Discord delivery must not expose private state.");return v;} export function operatorRequestTuple(v){return [v.schemaName,v.schemaVersion,v.requestId,v.commandId,v.nonce,v.sourceEventId,v.sourceActorDiscordId,v.discordGuildId,v.discordChannelId,v.discordMessageId,v.targetRuntimeId,v.issuedAt,v.expiresAt,v.producerId,v.producerRuntimeId,v.authorityClass,v.status,commandTuple(v.command)];} export function packetTuple(p){return [p.commandId,p.nonce,p.sourceEventId,p.sourceActorId,p.discordGuildId,p.discordChannelId,p.discordMessageId,p.targetRuntimeId,p.issuedAt,p.expiresAt,commandTuple(p.command)];} export function admissionTuple(a){return [a.schemaVersion,a.admissionId,packetTuple(a.packet),a.packetSha256,a.sourceObserverId,a.sourceObserverRuntimeId,a.provider,a.bifrostAdmissionReceiptId,a.authority,a.providerIdentityId];} // rmp-serde's compact serializer encodes the nested Rust wire-result struct as // this positional array inside the signed tuple. The separately hashed result // payload is encoded with named fields. -export function wireResultTuple(v){const tuple=[v.schemaVersion,v.resultId,v.commandId,v.packetSha256,v.targetRuntimeId,v.disposition,v.consequenceKind,v.consequenceRef,v.completedAt,v.privateStateExposed,v.operatorStatus,v.stateStatus,v.coordinatorAction,v.brakeStatus,v.detail];return v.schemaVersion===legacyOperatorResultSchema?tuple:[...tuple,v.reviews.map(reviewSummaryTuple),v.reviewCandidateId,v.reviewDecision];} +export function wireResultTuple(v){const tuple=[v.schemaVersion,v.resultId,v.commandId,v.packetSha256,v.targetRuntimeId,v.disposition,v.consequenceKind,v.consequenceRef,v.completedAt,v.privateStateExposed,v.operatorStatus,v.stateStatus,v.coordinatorAction,v.brakeStatus,v.detail];if(v.schemaVersion===legacyOperatorResultSchema)return tuple;const current=[...tuple,v.reviews.map(reviewSummaryTuple),v.reviewCandidateId,v.reviewDecision];return v.schemaVersion===operatorStatusResultSchema?[...current,statusV2Tuple(v.statusV2)]:current;} export function resultReceiptTuple(v){return [v.schemaVersion,wireResultTuple(v.result),v.resultPayloadSha256,v.commandId,v.packetSha256,v.targetRuntimeId,v.completedAt,v.providerIdentityId,v.privateStateExposed];} function parsePacket(v){v=exactRecord(v,["commandId","nonce","sourceEventId","sourceActorId","discordGuildId","discordChannelId","discordMessageId","targetRuntimeId","issuedAt","expiresAt","command"],"operator packet");for(const k of ["commandId","nonce","sourceEventId","sourceActorId","discordGuildId","discordChannelId","discordMessageId","targetRuntimeId"])text(v[k],k);date(v.issuedAt,"issuedAt");date(v.expiresAt,"expiresAt");parseCommand(v.command);return v;} function parseCommand(v){if(!v||typeof v!=="object"||Array.isArray(v))throw new Error("command must be an object.");if(["status","wake","reviews"].includes(v.kind))return exactRecord(v,["kind"],"command");if(v.kind==="sleep"){exactRecord(v,["kind","reason"],"command");text(v.reason,"reason");if(v.reason.length>512)throw new Error("sleep reason exceeds 512 characters.");return v;}if(v.kind==="directive"){exactRecord(v,["kind","objective"],"command");text(v.objective,"objective");if(v.objective.length>4096)throw new Error("directive objective exceeds 4096 characters.");return v;}if(v.kind==="review"){exactRecord(v,["kind","mindRequestId","candidateId","candidateSha256","expectedModelRevision","expectedModelHash","decision"],"command");for(const k of ["mindRequestId","candidateId"]){text(v[k],k);if(v[k].length>256)throw new Error(`${k} exceeds 256 characters.`);}hex64(v.candidateSha256,"candidateSha256");hex64(v.expectedModelHash,"expectedModelHash");if(!Number.isSafeInteger(v.expectedModelRevision)||v.expectedModelRevision<0)throw new Error("expectedModelRevision must be an unsigned safe integer.");if(!["adopt","refuse","hold"].includes(v.decision))throw new Error("review decision is invalid.");return v;}throw new Error("command kind is invalid.");} @@ -37,6 +51,33 @@ function commandTuple(v){if(v.kind==="sleep")return ["sleep",v.reason];if(v.kind export const commandTupleForInterop=commandTuple; function parseReviewSummary(v){exactRecord(v,["mindRequestId","candidateId","candidateSha256","modelRevision","modelHash","frontierItemId","requestedAt"],"review summary");for(const k of ["mindRequestId","candidateId","frontierItemId"]){text(v[k],k);if(v[k].length>256)throw new Error(`review ${k} exceeds 256 characters.`);}hex64(v.candidateSha256,"candidateSha256");hex64(v.modelHash,"modelHash");if(!Number.isSafeInteger(v.modelRevision)||v.modelRevision<0)throw new Error("review modelRevision is invalid.");date(v.requestedAt,"requestedAt");return v;} function reviewSummaryTuple(v){return [v.mindRequestId,v.candidateId,v.candidateSha256,v.modelRevision,v.modelHash,v.frontierItemId,v.requestedAt];} +export function parseOperatorStatusV2(v){ + v=exactRecord(v,["schemaVersion","releaseId","releaseWitnessSha256","sourceCommit","deploymentId","coordinatorSnapshot","coordinatorState","coordinatorAction","brakeStatus","residentStatus","pressureCount","pendingReviewCount","providerSetStatus","providers","privateStateExposed"],"operator status v2"); + exact(v.schemaVersion,"epiphany.operator.status.v2","status schemaVersion"); + for(const k of ["releaseId","deploymentId","coordinatorSnapshot","coordinatorState","coordinatorAction","brakeStatus","residentStatus"]){text(v[k],k);if(v[k].length>256)throw new Error(`${k} exceeds 256 characters.`);} + digest(v.releaseWitnessSha256,"releaseWitnessSha256");if(!/^[0-9a-f]{40}$/.test(v.sourceCommit))throw new Error("sourceCommit must be a lowercase 40-character commit."); + for(const k of ["pressureCount","pendingReviewCount"])if(!Number.isSafeInteger(v[k])||v[k]<0||v[k]>1_000_000)throw new Error(`${k} is invalid.`); + if(!["complete-authenticated","incomplete"].includes(v.providerSetStatus))throw new Error("providerSetStatus is invalid."); + if(!Array.isArray(v.providers)||v.providers.length!==2)throw new Error("operator status v2 requires exactly two providers."); + const policies=[["yggdrasil-epiphany","epiphany.cultnet-rudp-runtime-health",true],["yggdrasil-bifrost-persona-feedback","bifrost.cultnet-rudp-persona-feedback-health",false]]; + v.providers.forEach((p,i)=>parseStatusProvider(p,...policies[i])); + if(v.providers.every(p=>p.availability==="authenticated-current")!==(v.providerSetStatus==="complete-authenticated"))throw new Error("providerSetStatus does not match provider availability."); + if(v.privateStateExposed!==false)throw new Error("operator status v2 must not expose private state.");return v; +} +function parseStatusProvider(v,daemonId,contract,hasLineage){ + v=exactRecord(v,["daemonId","healthContract","availability","unavailableReason","state","reasonCode","providerObservedAtUnixMillis","evaluatedAtUnixMillis","expiresAtUnixMillis","releaseId","releaseWitnessSha256","sourceCommit","deploymentId","projectionSha256"],"operator status provider");exact(v.daemonId,daemonId,"daemonId");exact(v.healthContract,contract,"healthContract"); + if(!["authenticated-current","unavailable"].includes(v.availability))throw new Error("provider availability is invalid.");if(typeof v.unavailableReason!=="string"||v.unavailableReason.length>256)throw new Error("unavailableReason is invalid."); + for(const k of ["state","reasonCode","releaseId","sourceCommit","deploymentId"])if(v[k]!==null&&(typeof v[k]!=="string"||!v[k]||v[k].length>256))throw new Error(`${k} is invalid.`); + for(const k of ["providerObservedAtUnixMillis","evaluatedAtUnixMillis","expiresAtUnixMillis"])if(v[k]!==null&&(!Number.isSafeInteger(v[k])||v[k]<0))throw new Error(`${k} is invalid.`); + for(const k of ["releaseWitnessSha256","projectionSha256"])if(v[k]!==null)digest(v[k],k);if(v.sourceCommit!==null&&!/^[0-9a-f]{40}$/.test(v.sourceCommit))throw new Error("provider sourceCommit is invalid."); + const observed=["state","reasonCode","providerObservedAtUnixMillis","evaluatedAtUnixMillis","expiresAtUnixMillis","releaseId","releaseWitnessSha256","sourceCommit","deploymentId","projectionSha256"]; + if(v.availability==="unavailable"){if(!v.unavailableReason||observed.some(k=>v[k]!==null))throw new Error("unavailable provider must carry only its reason.");} + else{if(v.unavailableReason||["state","reasonCode","providerObservedAtUnixMillis","evaluatedAtUnixMillis","expiresAtUnixMillis","projectionSha256"].some(k=>v[k]===null))throw new Error("authenticated provider lacks current bounded state.");if(hasLineage){if(["releaseId","releaseWitnessSha256","sourceCommit","deploymentId"].some(k=>v[k]===null))throw new Error("Epiphany provider lacks release lineage.");}else if(["releaseId","releaseWitnessSha256","sourceCommit","deploymentId"].some(k=>v[k]!==null))throw new Error("Bifrost provider must not claim Epiphany release lineage.");} + return v; +} +function statusProviderTuple(v){return [v.daemonId,v.healthContract,v.availability,v.unavailableReason,v.state,v.reasonCode,v.providerObservedAtUnixMillis,v.evaluatedAtUnixMillis,v.expiresAtUnixMillis,v.releaseId,v.releaseWitnessSha256,v.sourceCommit,v.deploymentId,v.projectionSha256];} +function statusV2Tuple(v){return [v.schemaVersion,v.releaseId,v.releaseWitnessSha256,v.sourceCommit,v.deploymentId,v.coordinatorSnapshot,v.coordinatorState,v.coordinatorAction,v.brakeStatus,v.residentStatus,v.pressureCount,v.pendingReviewCount,v.providerSetStatus,v.providers.map(statusProviderTuple),v.privateStateExposed];} +function digest(v,label){if(typeof v!=="string"||!/^sha256-[0-9a-f]{64}$/.test(v))throw new Error(`${label} must be a sha256 digest.`);return v;} function hex64(v,label){if(typeof v!=="string"||!/^[0-9a-fA-F]{64}$/.test(v))throw new Error(`${label} must be 64 hexadecimal characters.`);return v;} function exactRecord(v,keys,label){if(!v||typeof v!=="object"||Array.isArray(v))throw new Error(`${label} must be an object.`);const actual=Object.keys(v).sort(),expected=[...keys].sort();if(actual.join("\0")!==expected.join("\0"))throw new Error(`${label} has unknown or missing fields.`);return v;} function text(v,label){if(typeof v!=="string"||!v.trim())throw new Error(`${label} is required.`);return v;} diff --git a/tools/epiphany-operator-cultnet-rudp.mjs b/tools/epiphany-operator-cultnet-rudp.mjs index dfb9445..a6c6248 100644 --- a/tools/epiphany-operator-cultnet-rudp.mjs +++ b/tools/epiphany-operator-cultnet-rudp.mjs @@ -64,6 +64,7 @@ export async function loadEpiphanyOperatorTrustAnchor(path,{decode}={}){const ru export function verifySealedReceipt({receipt,admission,trustedIdentity,encode}){ parseOperatorAdmission(admission,{allowLegacy:true});parseOperatorResultReceipt(receipt,{allowLegacy:true});const anchor=parseTrustAnchor(trustedIdentity); + if(receipt.result.schemaVersion==="epiphany.operator_command.status_result.v2"&&admission.packet.command.kind!=="status")throw new Error("Epiphany Status-v2 result is not bound to a Status command."); if(receipt.providerIdentityId!==anchor.identityId)throw new Error("Epiphany sealed result used an untrusted host identity."); if(receipt.commandId!==admission.packet.commandId||receipt.packetSha256!==admission.packetSha256||receipt.targetRuntimeId!==admission.packet.targetRuntimeId)throw new Error("Epiphany sealed result receipt is not bound to the admitted command."); if(receipt.result.commandId!==receipt.commandId||receipt.result.packetSha256!==receipt.packetSha256||receipt.result.targetRuntimeId!==receipt.targetRuntimeId||receipt.result.completedAt!==receipt.completedAt)throw new Error("Epiphany sealed result receipt substituted its embedded application result."); diff --git a/tools/epiphany-operator-discord-outbox.mjs b/tools/epiphany-operator-discord-outbox.mjs index 4a8ef6d..a3b2bb9 100644 --- a/tools/epiphany-operator-discord-outbox.mjs +++ b/tools/epiphany-operator-discord-outbox.mjs @@ -10,8 +10,8 @@ export class EpiphanyOperatorDiscordOutbox { static async open(path,{cultcacheRuntime,encode}={}){const runtime=cultcacheRuntime??loadCultCacheRuntime(),definition=buildOperatorDefinitions(runtime.defineDocumentType).discordDelivery,cache=runtime.CultCache.builder().withDocumentType(definition).withGenericStore(new runtime.SingleFileMessagePackBackingStore(path)).build();if(typeof encode!=="function")throw new Error("operator Discord outbox requires the canonical MessagePack encoder");await cache.pullAllBackingStores();return new EpiphanyOperatorDiscordOutbox(cache,definition,encode);} get(requestId){return this.cache.get(this.definition,requestId);} isExactDelivery(request){const value=this.get(request.requestId);return Boolean(value&&value.requestPayloadSha256===this.requestDigest(request)&&value.commandId===request.commandId&&value.discordGuildId===request.discordGuildId&&value.discordChannelId===request.discordChannelId&&value.discordInteractionId===request.sourceEventId&&value.targetRuntimeId===request.targetRuntimeId);} - async publishCompleted(request,receipt){validateReviewProjection(request,receipt.result);const value=this.base(request,{status:"completed",disposition:receipt.result.disposition,failureCode:"",detail:receipt.result.detail,operatorStatus:receipt.result.operatorStatus,stateStatus:receipt.result.stateStatus,coordinatorAction:receipt.result.coordinatorAction,brakeStatus:receipt.result.brakeStatus,reviews:receipt.result.reviews??[],reviewCandidateId:receipt.result.reviewCandidateId??"",reviewDecision:receipt.result.reviewDecision??"",sealedResultPayloadSha256:receipt.resultPayloadSha256,executorSignatureSha256:`sha256-${createHash("sha256").update(receipt.executorSignature).digest("hex")}`,resultProviderIdentityId:receipt.providerIdentityId,recordedAt:receipt.completedAt});return this.put(value);} - async publishRefused(request,terminal){const value=this.base(request,{status:"refused",disposition:"",failureCode:terminal.failureCode,detail:terminal.failureCode,operatorStatus:"",stateStatus:"",coordinatorAction:"",brakeStatus:"",reviews:[],reviewCandidateId:"",reviewDecision:"",sealedResultPayloadSha256:"",executorSignatureSha256:"",resultProviderIdentityId:"",recordedAt:terminal.recordedAt});return this.put(value);} + async publishCompleted(request,receipt){validateReviewProjection(request,receipt.result);const value=this.base(request,{status:"completed",disposition:receipt.result.disposition,failureCode:"",detail:receipt.result.detail,operatorStatus:receipt.result.operatorStatus,stateStatus:receipt.result.stateStatus,coordinatorAction:receipt.result.coordinatorAction,brakeStatus:receipt.result.brakeStatus,reviews:receipt.result.reviews??[],reviewCandidateId:receipt.result.reviewCandidateId??"",reviewDecision:receipt.result.reviewDecision??"",statusV2:receipt.result.statusV2??null,sealedResultPayloadSha256:receipt.resultPayloadSha256,executorSignatureSha256:`sha256-${createHash("sha256").update(receipt.executorSignature).digest("hex")}`,resultProviderIdentityId:receipt.providerIdentityId,recordedAt:receipt.completedAt});return this.put(value);} + async publishRefused(request,terminal){const value=this.base(request,{status:"refused",disposition:"",failureCode:terminal.failureCode,detail:terminal.failureCode,operatorStatus:"",stateStatus:"",coordinatorAction:"",brakeStatus:"",reviews:[],reviewCandidateId:"",reviewDecision:"",statusV2:null,sealedResultPayloadSha256:"",executorSignatureSha256:"",resultProviderIdentityId:"",recordedAt:terminal.recordedAt});return this.put(value);} requestDigest(request){return `sha256-${createHash("sha256").update(this.encode(operatorRequestTuple(request))).digest("hex")}`;} base(request,result){return {schemaVersion:"bifrost.discord.epiphany_operator_delivery.v1",deliveryId:request.requestId,requestId:request.requestId,requestPayloadSha256:this.requestDigest(request),commandId:request.commandId,discordGuildId:request.discordGuildId,discordChannelId:request.discordChannelId,discordInteractionId:request.sourceEventId,targetRuntimeId:request.targetRuntimeId,...result,privateStateExposed:false};} async put(value){const current=this.get(value.requestId);if(current&&stable(current)!==stable(value))throw new Error("operator Discord delivery identity collision");if(!current)await this.cache.put(this.definition,value.deliveryId,value);return current??value;}