diff --git a/.env.local.example b/.env.local.example index fad7427..86bdaf8 100644 --- a/.env.local.example +++ b/.env.local.example @@ -2,6 +2,8 @@ # The default compose.local.yaml is self-contained for local smoke testing. COMPOSE_PROJECT_NAME=bifrost-local +BIFROST_ROOT_VERSE=asgard +BIFROST_MACHINE_ID=yggdrasil # Epiphany operator admission. Endpoint and trust anchor are explicit; there is # no localhost or identity-string fallback. diff --git a/README.md b/README.md index 938e48f..a69f211 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,9 @@ For the full staged roadmap, including contribution points, revenue share, patro - dry-run a Reddit organizing thread with `node tools/bifrost-bridge.mjs reddit-post --title "Thread title" --persona-name Bifrost --content "Thread body" --dry-run true` - print the Eve provider advertisement with `node tools/provider-advertisement.mjs print` - export the Eve provider store with `node tools/provider-advertisement.mjs export --out .bifrost/provider-store.cc` +- inspect the typed six-command Epiphany operator authority map in + `docs/epiphany-operator-boundary.md`; provider advertisement commands require + explicit `BIFROST_ROOT_VERSE` and `BIFROST_MACHINE_ID` deployment identity - inspect the interactive Motion Verse surface at `/eve/governance/surface` while signed in as an active member - container smoke test with `docker compose -f compose.local.yaml up --build` - container smoke tests may hit `/healthz` and `/readyz` as product probes. diff --git a/docs/epiphany-operator-boundary.md b/docs/epiphany-operator-boundary.md new file mode 100644 index 0000000..83f0651 --- /dev/null +++ b/docs/epiphany-operator-boundary.md @@ -0,0 +1,48 @@ +# Epiphany operator boundary + +## Authority map + +- Owner: Bifrost alone authenticates a Discord operator request, resolves the + persisted Discord-to-Bifrost actor link, and signs an expiring Epiphany + admission. Epiphany alone decides and applies the named command. +- Inputs: one strict `voidbot.discord.epiphany_operator_request.v0`, the exact + configured Discord owner, a persisted Bifrost actor link, the deployment's + operator signing identity, and the configured Epiphany runtime/host trust + anchor. +- Outputs: one immutable `bifrost.operator_command.delivery.v1`, one verified + `epiphany.operator_command.sealed_result.v1`, and one bounded + `bifrost.discord.epiphany_operator_delivery.v1` projection. +- Derived state: Discord delivery fields are notification-only projections of + the signed result. Reviews contain at most ten candidate identity/status + summaries. Proposal bodies and private Mind state never cross this boundary. +- Forbidden writers: ordinary Discord conversation, Persona feedback, argv, + shell commands, `/queue-codex`, provider-advertisement metadata, and the + Discord outbox cannot create an Epiphany consequence. +- Shared path: Status, Sleep, Wake, Directive, Reviews, and Review all use the + same request admission, signature, RUDP execution, sealed-result verification, + durable ledger, and Discord projection path. +- Cut line: v0 admissions and sealed results exist only so already-durable + four-command work can drain or replay. Bifrost never mints v0, and v0 cannot + carry Reviews or Review. + +## Review command + +Review binds the exact current Mind candidate with `mindRequestId`, +`candidateId`, bare 64-hex `candidateSha256`, `expectedModelRevision`, bare +64-hex `expectedModelHash`, and one decision: `adopt`, `refuse`, or `hold`. +Bifrost does not infer a decision from conversation and cannot substitute a +candidate after signing. + +## Deployment identity + +Provider discovery reads `BIFROST_ROOT_VERSE` and `BIFROST_MACHINE_ID` from the +deployment environment. The canonical Yggdrasil example config supplies +`asgard` and `yggdrasil`; the provider code has no Starfire or machine fallback. + +## Verification layer + +The Rust-generated fixture owns cross-runtime schema versions, signing +purposes, command vocabulary, bounded review projection, admission bytes, and +sealed-result bytes. Bifrost tests consume that fixture, then run hostile +schema, identity, digest, binding, replay, and tamper cases against the same +ports used by the worker. diff --git a/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs b/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs index 1a026ca..f7f8f91 100644 --- a/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs +++ b/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs @@ -23,7 +23,8 @@ public sealed class ProviderAdvertisementTests AssertEndpoint(root, "patron-support-intake", "bifrost.patron_support_event.v0", "patronage"); AssertEndpoint(root, "github-webhooks", "bifrost.work_item.v0", "github"); AssertEndpoint(root, "epiphany-operator-requests", "voidbot.discord.epiphany_operator_request.v0", "operator"); - AssertEndpoint(root, "epiphany-operator-results", "epiphany.operator_command.sealed_result.v0", "epiphany"); + AssertEndpoint(root, "epiphany-operator-results", "epiphany.operator_command.sealed_result.v1", "epiphany"); + Assert.Equal("asgard.yggdrasil.bifrost", root.GetProperty("locatedService").GetString()); AssertSchemaPurpose(root, "bifrost.crossing_receipt.v1", "canonical Bifrost-owned public crossing receipt"); AssertSchemaPurpose(root, "bifrost.patron_support_event.v0", "current hosted Heimdall-signed patron support fact consumed by Bifrost"); AssertWitness(root, ".bifrost/bridge-receipts.cc", "bifrost.crossing_receipt.v1", "current"); @@ -182,6 +183,8 @@ public sealed class ProviderAdvertisementTests process.StartInfo.Environment["BIFROST_SKIP_LOCAL_ENV"] = "true"; process.StartInfo.Environment["BIFROST_REDDIT_CLIENT_ID"] = string.Empty; process.StartInfo.Environment["BIFROST_REDDIT_REFRESH_TOKEN"] = string.Empty; + process.StartInfo.Environment["BIFROST_ROOT_VERSE"] = "asgard"; + process.StartInfo.Environment["BIFROST_MACHINE_ID"] = "yggdrasil"; process.Start(); var stdout = await process.StandardOutput.ReadToEndAsync(); diff --git a/tests/epiphany-operator-command.test.mjs b/tests/epiphany-operator-command.test.mjs index afb473d..a64ccaa 100644 --- a/tests/epiphany-operator-command.test.mjs +++ b/tests/epiphany-operator-command.test.mjs @@ -5,7 +5,7 @@ import {verifyPurpose} from "../tools/bifrost-signed-identity.mjs"; const requireCult=createRequire(resolve("E:/Projects/CultLib/packages/cultnet-ts/package.json")),{encode}=requireCult("@msgpack/msgpack"); const actorLink={guildId:"guild",discordAuthorId:"owner",bifrostActorId:"actor",heimdallActorRef:"heimdall:actor:1"}; function request(command={kind:"status"}){return {schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v0",requestId:"interaction-1",commandId:"interaction-1",nonce:"interaction-1",sourceEventId:"interaction-1",sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"channel",discordMessageId:"interaction-1",targetRuntimeId:"epiphany-yggdrasil",issuedAt:"2026-07-19T10:00:00.000Z",expiresAt:"2026-07-19T10:00:30.000Z",producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command};} -function result(admission){const value={schemaVersion:"epiphany.operator_command.result.v0",resultId:"result-1",commandId:admission.packet.commandId,packetSha256:admission.packetSha256,targetRuntimeId:admission.packet.targetRuntimeId,disposition:"observed",consequenceKind:"operator-snapshot",consequenceRef:"snapshot-1",completedAt:"2026-07-19T10:00:01.000Z",privateStateExposed:false,operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged",detail:"bounded"},receipt={schemaVersion:"epiphany.operator_command.sealed_result.v0",result:value,resultPayloadSha256:"sha256-result",commandId:value.commandId,packetSha256:value.packetSha256,targetRuntimeId:value.targetRuntimeId,completedAt:value.completedAt,providerIdentityId:"epiphany-host",privateStateExposed:false,executorSignature:new Uint8Array(64)};return {providerIdentityId:"epiphany-host",result:value,receipt};} +function result(admission){const value={schemaVersion:"epiphany.operator_command.result.v1",resultId:"result-1",commandId:admission.packet.commandId,packetSha256:admission.packetSha256,targetRuntimeId:admission.packet.targetRuntimeId,disposition:"observed",consequenceKind:"operator-snapshot",consequenceRef:"snapshot-1",completedAt:"2026-07-19T10:00:01.000Z",privateStateExposed:false,operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged",detail:"bounded",reviews:[],reviewCandidateId:"",reviewDecision:""},receipt={schemaVersion:"epiphany.operator_command.sealed_result.v1",result:value,resultPayloadSha256:"sha256-result",commandId:value.commandId,packetSha256:value.packetSha256,targetRuntimeId:value.targetRuntimeId,completedAt:value.completedAt,providerIdentityId:"epiphany-host",privateStateExposed:false,executorSignature:new Uint8Array(64)};return {providerIdentityId:"epiphany-host",result:value,receipt};} const transport=(execute)=>({trustedIdentityId:"epiphany-host",execute,verifyReceipt:(_admission,receipt)=>receipt}); class TestLedger{constructor(){this.requests=new Map();this.admissions=new Map();this.completions=new Map();this.terminals=new Map();}admit(id,request,value){const oldRequest=this.requests.get(id),old=this.admissions.get(id);if(oldRequest&&JSON.stringify(oldRequest)!==JSON.stringify(request)||old&&JSON.stringify(old)!==JSON.stringify(value))throw new Error("operator request identity collision");this.requests.set(id,request);this.admissions.set(id,value);}complete(_id,result,_provider,receipt){this.completions.set(result.commandId,receipt??{...result,result,providerIdentityId:"epiphany-host"});}getRequest(id){return this.requests.get(id);}getAdmission(id){return this.admissions.get(id);}getCompletion(id){return this.completions.get(id);}terminalize(value){this.terminals.set(value.requestId,value);}getTerminal(id){return this.terminals.get(id);}} async function identity(){const d=await mkdtemp(resolve(tmpdir(),"bifrost-operator-"));return enrollOperatorIdentity(resolve(d,"operator.seed"));} diff --git a/tests/epiphany-operator-cultnet-rudp.test.mjs b/tests/epiphany-operator-cultnet-rudp.test.mjs index 705253d..710b856 100644 --- a/tests/epiphany-operator-cultnet-rudp.test.mjs +++ b/tests/epiphany-operator-cultnet-rudp.test.mjs @@ -9,7 +9,7 @@ import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { enrollOperatorIdentity } from "../tools/epiphany-operator-commands.mjs"; import { resultReceiptTuple } from "../tools/epiphany-operator-command-documents.mjs"; -import { EPIPHANY_RESULT_SIGNING_PURPOSE,EpiphanyOperatorCultNetRudpTransport,verifySealedReceipt } from "../tools/epiphany-operator-cultnet-rudp.mjs"; +import { LEGACY_EPIPHANY_RESULT_SIGNING_PURPOSE as EPIPHANY_RESULT_SIGNING_PURPOSE,EpiphanyOperatorCultNetRudpTransport,verifySealedReceipt } from "../tools/epiphany-operator-cultnet-rudp.mjs"; import { signPurpose,trustAnchor } from "../tools/bifrost-signed-identity.mjs"; const requireCult=createRequire(resolve("E:/Projects/CultLib/packages/cultnet-ts/package.json")); diff --git a/tests/epiphany-operator-discord-outbox.test.mjs b/tests/epiphany-operator-discord-outbox.test.mjs index 2c43d8d..5c11014 100644 --- a/tests/epiphany-operator-discord-outbox.test.mjs +++ b/tests/epiphany-operator-discord-outbox.test.mjs @@ -1,3 +1,6 @@ + + + import test from "node:test"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; @@ -7,6 +10,6 @@ import { resolve } from "node:path"; import { EpiphanyOperatorDiscordOutbox } from "../tools/epiphany-operator-discord-outbox.mjs"; const requireCache=createRequire(resolve("E:/Projects/CultLib/packages/cultcache-ts/package.json")),requireNet=createRequire(resolve("E:/Projects/CultLib/packages/cultnet-ts/package.json")),cache=requireCache("cultcache-ts"),{encode}=requireNet("@msgpack/msgpack"); -function request(id="request-1"){return {schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v0",requestId:id,commandId:id,nonce:id,sourceEventId:id,sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"ops",discordMessageId:id,targetRuntimeId:"epiphany-yggdrasil",issuedAt:"2026-07-19T12:00:00Z",expiresAt:"2026-07-19T12:01:00Z",producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command:{kind:"status"}};} -function receipt(id="request-1"){return {schemaVersion:"epiphany.operator_command.sealed_result.v0",result:{disposition:"observed",detail:"bounded",operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged"},resultPayloadSha256:`sha256-${"1".repeat(64)}`,providerIdentityId:"executor",executorSignature:new Uint8Array(64),completedAt:"2026-07-19T12:00:01Z",commandId:id};} -test("Discord outbox publishes immutable request-bound completed and refused deliveries",async()=>{const root=await mkdtemp(resolve(tmpdir(),"operator-outbox-")),path=resolve(root,"deliveries.cc"),outbox=await EpiphanyOperatorDiscordOutbox.open(path,{cultcacheRuntime:cache,encode}),r=request(),completed=await outbox.publishCompleted(r,receipt());assert.equal(completed.deliveryId,r.requestId);assert.match(completed.requestPayloadSha256,/^sha256-[0-9a-f]{64}$/);assert.match(completed.executorSignatureSha256,/^sha256-[0-9a-f]{64}$/);assert.equal(completed.operatorStatus,"sleeping");assert.equal(completed.brakeStatus,"engaged");assert.equal(outbox.isExactDelivery(r),true);assert.equal(outbox.isExactDelivery({...r,discordChannelId:"alien"}),false);assert.equal(completed.privateStateExposed,false);await outbox.publishCompleted(r,receipt());await assert.rejects(()=>outbox.publishCompleted(r,{...receipt(),result:{...receipt().result,disposition:"applied",detail:"changed"}}),/collision/);const refusedRequest=request("request-2"),refused=await outbox.publishRefused(refusedRequest,{failureCode:"foreign-owner",recordedAt:"2026-07-19T12:00:02Z"});assert.equal(refused.status,"refused");assert.equal(refused.failureCode,"foreign-owner");assert.equal(refused.sealedResultPayloadSha256,"");const reopened=await EpiphanyOperatorDiscordOutbox.open(path,{cultcacheRuntime:cache,encode});assert.equal(reopened.get(r.requestId).status,"completed");assert.equal(reopened.get(refusedRequest.requestId).status,"refused");}); +function request(id="request-1",command={kind:"status"}){return {schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v0",requestId:id,commandId:id,nonce:id,sourceEventId:id,sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"ops",discordMessageId:id,targetRuntimeId:"epiphany-yggdrasil",issuedAt:"2026-07-19T12:00:00Z",expiresAt:"2026-07-19T12:01:00Z",producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command};} +function receipt(id="request-1"){return {schemaVersion:"epiphany.operator_command.sealed_result.v1",result:{disposition:"observed",detail:"bounded",operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged",reviews:[],reviewCandidateId:"candidate-1",reviewDecision:"hold"},resultPayloadSha256:`sha256-${"1".repeat(64)}`,providerIdentityId:"executor",executorSignature:new Uint8Array(64),completedAt:"2026-07-19T12:00:01Z",commandId:id};} +test("Discord outbox publishes immutable request-bound completed and refused deliveries",async()=>{const root=await mkdtemp(resolve(tmpdir(),"operator-outbox-")),path=resolve(root,"deliveries.cc"),outbox=await EpiphanyOperatorDiscordOutbox.open(path,{cultcacheRuntime:cache,encode}),r=request("request-1",{kind:"review",mindRequestId:"mind-1",candidateId:"candidate-1",candidateSha256:"1".repeat(64),expectedModelRevision:41,expectedModelHash:"2".repeat(64),decision:"hold"}),completed=await outbox.publishCompleted(r,receipt());assert.equal(completed.deliveryId,r.requestId);assert.match(completed.requestPayloadSha256,/^sha256-[0-9a-f]{64}$/);assert.match(completed.executorSignatureSha256,/^sha256-[0-9a-f]{64}$/);assert.equal(completed.operatorStatus,"sleeping");assert.equal(completed.brakeStatus,"engaged");assert.equal(completed.schemaVersion,"bifrost.discord.epiphany_operator_delivery.v1");assert.equal(completed.commandId,r.commandId);assert.deepEqual(completed.reviews,[]);assert.equal(completed.reviewCandidateId,"candidate-1");assert.equal(completed.reviewDecision,"hold");assert.equal(outbox.isExactDelivery(r),true);assert.equal(outbox.isExactDelivery({...r,discordChannelId:"alien"}),false);assert.equal(completed.privateStateExposed,false);await outbox.publishCompleted(r,receipt());await assert.rejects(()=>outbox.publishCompleted({...r,command:{...r.command,candidateId:"alien"}},receipt()),/does not bind/);await assert.rejects(()=>outbox.publishCompleted(r,{...receipt(),result:{...receipt().result,disposition:"applied",detail:"changed"}}),/collision/);const refusedRequest=request("request-2"),refused=await outbox.publishRefused(refusedRequest,{failureCode:"foreign-owner",recordedAt:"2026-07-19T12:00:02Z"});assert.equal(refused.status,"refused");assert.equal(refused.failureCode,"foreign-owner");assert.equal(refused.sealedResultPayloadSha256,"");assert.deepEqual(refused.reviews,[]);assert.equal(refused.reviewCandidateId,"");assert.equal(refused.reviewDecision,"");const reopened=await EpiphanyOperatorDiscordOutbox.open(path,{cultcacheRuntime:cache,encode});assert.equal(reopened.get(r.requestId).status,"completed");assert.equal(reopened.get(refusedRequest.requestId).status,"refused");}); diff --git a/tests/epiphany-operator-rust-interop.test.mjs b/tests/epiphany-operator-rust-interop.test.mjs index b5eb5a1..04bfc02 100644 --- a/tests/epiphany-operator-rust-interop.test.mjs +++ b/tests/epiphany-operator-rust-interop.test.mjs @@ -11,4 +11,4 @@ import { loadEpiphanyOperatorTrustAnchor,verifySealedReceipt } from "../tools/ep import { verifyPurpose } from "../tools/bifrost-signed-identity.mjs"; const fixture=process.env.EPIPHANY_OPERATOR_RUST_FIXTURE??resolve("..","EpiphanyAgent",".epiphany-smoke","operator-command-interop-rust"),requireCult=createRequire(resolve("E:/Projects/CultLib/packages/cultnet-ts/package.json")),{decode,encode}=requireCult("@msgpack/msgpack"); -test("consumes and verifies the Rust-emitted operator admission and sealed receipt",{skip:!existsSync(resolve(fixture,"manifest.json"))&&"Rust interop fixture is not present in this standalone checkout"},async()=>{const manifest=JSON.parse(await readFile(resolve(fixture,"manifest.json"),"utf8")),admissionBytes=await readFile(resolve(fixture,manifest.admissionFile)),receiptBytes=await readFile(resolve(fixture,manifest.sealedResultFile));assert.equal(`sha256-${createHash("sha256").update(admissionBytes).digest("hex")}`,manifest.admissionSha256);assert.equal(`sha256-${createHash("sha256").update(receiptBytes).digest("hex")}`,manifest.sealedResultSha256);const admission=parseOperatorAdmission(decode(admissionBytes)),receipt=decode(receiptBytes),bifrostAnchor=await loadEpiphanyOperatorTrustAnchor(resolve(fixture,manifest.bifrostRawTrustAnchorFile),{decode}),executorAnchor=await loadEpiphanyOperatorTrustAnchor(resolve(fixture,manifest.executorRawTrustAnchorFile),{decode});assert.equal(verifyPurpose(bifrostAnchor.publicKey,OPERATOR_SIGNING_PURPOSE,encode(admissionTuple(admission)),admission.providerSignature),true);assert.equal(verifySealedReceipt({receipt,admission,trustedIdentity:executorAnchor,encode}).commandId,admission.packet.commandId);assert.throws(()=>verifySealedReceipt({receipt:{...receipt,packetSha256:"sha256-hostile"},admission,trustedIdentity:executorAnchor,encode}),/not bound/);assert.equal(manifest.rudpConnectionId,0xe91f0001);assert.equal(manifest.privateStateExposed,false);}); +test("consumes and verifies the Rust-emitted operator admission, protocol, and sealed receipt",{skip:!existsSync(resolve(fixture,"manifest.json"))&&"Rust interop fixture is not present in this standalone checkout"},async()=>{const manifest=JSON.parse(await readFile(resolve(fixture,"manifest.json"),"utf8")),protocolBytes=await readFile(resolve(fixture,manifest.protocolFile)),protocol=JSON.parse(protocolBytes),admissionBytes=await readFile(resolve(fixture,manifest.admissionFile)),receiptBytes=await readFile(resolve(fixture,manifest.sealedResultFile));assert.equal(`sha256-${createHash("sha256").update(admissionBytes).digest("hex")}`,manifest.admissionSha256);assert.equal(`sha256-${createHash("sha256").update(receiptBytes).digest("hex")}`,manifest.sealedResultSha256);assert.equal(`sha256-${createHash("sha256").update(protocolBytes).digest("hex")}`,manifest.protocolSha256);assert.equal(protocol.admissionSchemaVersion,"bifrost.operator_command.delivery.v1");assert.equal(protocol.resultSchemaVersion,"epiphany.operator_command.result.v1");assert.equal(protocol.sealedResultSchemaVersion,"epiphany.operator_command.sealed_result.v1");assert.deepEqual(protocol.commands.map(command=>command.kind),["status","sleep","wake","directive","reviews","review"]);assert.deepEqual(protocol.reviewDecisions,["adopt","refuse","hold"]);assert.equal(protocol.boundedReviewResult.reviews.length,1);assert.equal(protocol.boundedReviewResult.privateStateExposed,false);const admission=parseOperatorAdmission(decode(admissionBytes)),receipt=decode(receiptBytes),bifrostAnchor=await loadEpiphanyOperatorTrustAnchor(resolve(fixture,manifest.bifrostRawTrustAnchorFile),{decode}),executorAnchor=await loadEpiphanyOperatorTrustAnchor(resolve(fixture,manifest.executorRawTrustAnchorFile),{decode});assert.equal(verifyPurpose(bifrostAnchor.publicKey,OPERATOR_SIGNING_PURPOSE,encode(admissionTuple(admission)),admission.providerSignature),true);assert.equal(verifySealedReceipt({receipt,admission,trustedIdentity:executorAnchor,encode}).commandId,admission.packet.commandId);assert.throws(()=>verifySealedReceipt({receipt:{...receipt,packetSha256:"sha256-hostile"},admission,trustedIdentity:executorAnchor,encode}),/not bound/);assert.equal(manifest.rudpConnectionId,0xe91f0001);assert.equal(manifest.privateStateExposed,false);}); diff --git a/tests/epiphany-operator-v1.test.mjs b/tests/epiphany-operator-v1.test.mjs new file mode 100644 index 0000000..9410f45 --- /dev/null +++ b/tests/epiphany-operator-v1.test.mjs @@ -0,0 +1,99 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + commandTupleForInterop, + parseOperatorAdmission, + parseOperatorRequest, + parseOperatorResult, + wireResultTuple, +} from "../tools/epiphany-operator-command-documents.mjs"; + +const hex = "a".repeat(64); +const review = { + kind: "review", + mindRequestId: "mind-1", + candidateId: "candidate-1", + candidateSha256: hex, + expectedModelRevision: 41, + expectedModelHash: "b".repeat(64), + decision: "adopt", +}; + +function request(command) { + return { + schemaName: "voidbot.discord.epiphany_operator_request", + schemaVersion: "voidbot.discord.epiphany_operator_request.v0", + requestId: "request-1", + commandId: "command-1", + nonce: "nonce-1", + sourceEventId: "event-1", + sourceActorDiscordId: "owner", + discordGuildId: "guild", + discordChannelId: "ops", + discordMessageId: "message-1", + targetRuntimeId: "epiphany-yggdrasil", + issuedAt: "2026-07-19T12:00:00Z", + expiresAt: "2026-07-19T12:01:00Z", + producerId: "voidbot", + producerRuntimeId: "voidbot-yggdrasil", + authorityClass: "operator_request_only", + status: "pending", + command, + }; +} + +test("six-command v1 accepts bounded Reviews and exact Review identity only", () => { + assert.equal(parseOperatorRequest(request({ kind: "reviews" })).command.kind, "reviews"); + assert.deepEqual(commandTupleForInterop(parseOperatorRequest(request(review)).command), [ + "review", + ["mind-1", "candidate-1", hex, 41, "b".repeat(64), "adopt"], + ]); + for (const hostile of [ + { ...review, candidateSha256: `sha256-${hex}` }, + { ...review, decision: "approve" }, + { ...review, expectedModelRevision: -1 }, + { ...review, proposalText: "private" }, + ]) assert.throws(() => parseOperatorRequest(request(hostile))); +}); + +test("v0 admission is drain-only and cannot acquire review authority", () => { + const packet = { + commandId: "command-1", nonce: "nonce-1", sourceEventId: "event-1", + sourceActorId: "actor", discordGuildId: "guild", discordChannelId: "ops", + discordMessageId: "message-1", targetRuntimeId: "epiphany-yggdrasil", + issuedAt: "2026-07-19T12:00:00Z", expiresAt: "2026-07-19T12:01:00Z", + command: { kind: "reviews" }, + }; + const admission = { + schemaName: "bifrost.operator_command.delivery", + schemaVersion: "bifrost.operator_command.delivery.v0", + admissionId: "request-1", packet, packetSha256: `sha256-${hex}`, + sourceObserverId: "voidbot", sourceObserverRuntimeId: "voidbot-yggdrasil", + provider: "bifrost", bifrostAdmissionReceiptId: "request-1", + authority: "exact_operator_command_only", providerIdentityId: "bifrost", + providerSignature: new Uint8Array(64), + }; + assert.throws(() => parseOperatorAdmission(admission, { allowLegacy: true }), /cannot carry Mind review/); + assert.throws(() => parseOperatorAdmission(admission), /current admission schema/); +}); + +test("v1 result exposes only bounded review identities and status", () => { + const result = { + schemaVersion: "epiphany.operator_command.result.v1", resultId: "result-1", + commandId: "command-1", packetSha256: `sha256-${hex}`, + targetRuntimeId: "epiphany-yggdrasil", disposition: "observed", + consequenceKind: "mind-review-candidates", consequenceRef: "", + completedAt: "2026-07-19T12:00:01Z", privateStateExposed: false, + operatorStatus: "", stateStatus: "", coordinatorAction: "", brakeStatus: "", + detail: "1 pending Mind review candidate(s)", + reviews: [{ mindRequestId: "mind-1", candidateId: "candidate-1", + candidateSha256: hex, modelRevision: 41, + modelHash: "b".repeat(64), frontierItemId: "frontier-1", + requestedAt: "2026-07-19T11:59:00Z" }], + reviewCandidateId: "", reviewDecision: "", + }; + assert.equal(wireResultTuple(parseOperatorResult(result)).length, 18); + assert.throws(() => parseOperatorResult({ ...result, reviews: [...result.reviews, ...Array(10).fill(result.reviews[0])] }), /bounded/); + assert.throws(() => parseOperatorResult({ ...result, proposalText: "private" })); +}); diff --git a/tests/epiphany-operator-worker.test.mjs b/tests/epiphany-operator-worker.test.mjs index c5cd1de..3109b24 100644 --- a/tests/epiphany-operator-worker.test.mjs +++ b/tests/epiphany-operator-worker.test.mjs @@ -1,3 +1,4 @@ + import test from "node:test"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; @@ -16,5 +17,5 @@ import { DurableOperatorLedger } from "../tools/epiphany-operator-ledger.mjs"; import { EpiphanyOperatorDiscordOutbox } from "../tools/epiphany-operator-discord-outbox.mjs"; const requireCache=createRequire(resolve("E:/Projects/CultLib/packages/cultcache-ts/package.json")),requireNet=createRequire(resolve("E:/Projects/CultLib/packages/cultnet-ts/package.json")),cache=requireCache("cultcache-ts"),cultnet=requireNet("cultnet-ts"),msgpack=requireNet("@msgpack/msgpack"); -test("supervised worker consumes the typed inbox and durably records the sealed result",async()=>{const root=await mkdtemp(resolve(tmpdir(),"operator-worker-")),requestPath=resolve(root,"requests.cc"),privatePath=resolve(root,"private.cc"),ledgerPath=resolve(root,"ledger.cc"),deliveryPath=resolve(root,"deliveries.cc"),keyPath=resolve(root,"bifrost.seed"),anchorPath=resolve(root,"executor.msgpack"),defs=buildOperatorDefinitions(cache.defineDocumentType),feedback=buildFeedbackDefinitions(cache.defineDocumentType),requests=cache.CultCache.builder().withDocumentType(defs.request).withGenericStore(new cache.SingleFileMessagePackBackingStore(requestPath)).build(),privateCache=cache.CultCache.builder().withDocumentType(feedback.actorLink).withGenericStore(new cache.SingleFileMessagePackBackingStore(privatePath)).build(),issued=new Date(),expires=new Date(issued.getTime()+60000),request={schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v0",requestId:"interaction-1",commandId:"interaction-1",nonce:"interaction-1",sourceEventId:"interaction-1",sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"ops",discordMessageId:"interaction-1",targetRuntimeId:"epiphany-yggdrasil",issuedAt:issued.toISOString(),expiresAt:expires.toISOString(),producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command:{kind:"status"}};await requests.put(defs.request,request.requestId,request);await privateCache.put(feedback.actorLink,"guild:owner",{schemaName:"bifrost.discord.actor_link",schemaVersion:"bifrost.discord.actor_link.v0",linkId:"guild:owner",guildId:"guild",discordAuthorId:"owner",bifrostActorId:"actor",heimdallActorRef:"heimdall:actor:1",linkedAt:issued.toISOString()});await enrollOperatorIdentity(keyPath);const executor=await enrollOperatorIdentity(resolve(root,"executor.seed")),anchor=trustAnchor(executor,issued.toISOString());await writeFile(anchorPath,msgpack.encode([anchor.schemaVersion,anchor.identityId,anchor.publicKey,anchor.assurance,anchor.identityCreatedAt,anchor.sourceIdentityRecordSha256]));const socket=createSocket("udp4");await new Promise((ok,fail)=>{socket.once("error",fail);socket.bind(0,"127.0.0.1",ok)});const server=new cultnet.CultNetRudpSocketTransportConnection({runtimeId:request.targetRuntimeId,socket,mode:"server",connectionId:0xe91f0001,resendDelayMs:25,resendPollMs:5});server.on("frame",frame=>{const wire=msgpack.decode(frame.payload),admission=msgpack.decode(wire.document.payload),completedAt=new Date(issued.getTime()+1000).toISOString(),result={schemaVersion:"epiphany.operator_command.result.v0",resultId:"result-1",commandId:admission.packet.commandId,packetSha256:admission.packetSha256,targetRuntimeId:admission.packet.targetRuntimeId,disposition:"observed",consequenceKind:"operator-snapshot",consequenceRef:"snapshot-1",completedAt,privateStateExposed:false,operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged",detail:"bounded"},receipt={schemaVersion:"epiphany.operator_command.sealed_result.v0",result,resultPayloadSha256:`sha256-${createHash("sha256").update(msgpack.encode(result)).digest("hex")}`,commandId:result.commandId,packetSha256:result.packetSha256,targetRuntimeId:result.targetRuntimeId,completedAt,providerIdentityId:executor.identityId,privateStateExposed:false,executorSignature:new Uint8Array()};receipt.executorSignature=new Uint8Array(signPurpose(executor,EPIPHANY_RESULT_SIGNING_PURPOSE,msgpack.encode(resultReceiptTuple(receipt))));server.send("schema",msgpack.encode({schemaVersion:"cultnet.document_put_raw.v0",messageId:"result-1",document:{schemaId:"epiphany.operator_command.sealed_result",recordKey:receipt.commandId,storedAt:receipt.completedAt,payloadEncoding:"messagepack",payload:msgpack.encode(receipt),sourceRuntimeId:receipt.targetRuntimeId,sourceAgentId:receipt.providerIdentityId,sourceRole:"epiphany-operator-command-executor",tags:["cultnet.transport.rudp.v0"]}}));});try{const port=socket.address().port,run=spawn(process.execPath,[resolve("tools/epiphany-operator-worker.mjs"),"once","--request-store",requestPath,"--private-store",privatePath,"--ledger-store",ledgerPath,"--delivery-store",deliveryPath,"--private-key",keyPath,"--epiphany-trust-anchor",anchorPath,"--epiphany-rudp",`rudp://127.0.0.1:${port}`,"--owner-discord-id","owner"],{cwd:resolve("."),stdio:["ignore","pipe","pipe"]}),output=await collect(run);assert.equal(output.code,0,output.stderr);const pulse=JSON.parse(output.stdout.trim());assert.equal(pulse.status,"completed");const ledger=await DurableOperatorLedger.open(ledgerPath,{cultcacheRuntime:cache}),outbox=await EpiphanyOperatorDiscordOutbox.open(deliveryPath,{cultcacheRuntime:cache,encode:msgpack.encode});assert.equal(ledger.getCompletion(request.commandId).providerIdentityId,executor.identityId);const delivery=outbox.get(request.requestId);assert.equal(delivery.status,"completed");assert.equal(delivery.discordInteractionId,request.sourceEventId);assert.match(delivery.requestPayloadSha256,/^sha256-[0-9a-f]{64}$/);assert.equal(delivery.privateStateExposed,false);}finally{server.close();}}); +test("supervised worker consumes the typed inbox and durably records the sealed result",async()=>{const root=await mkdtemp(resolve(tmpdir(),"operator-worker-")),requestPath=resolve(root,"requests.cc"),privatePath=resolve(root,"private.cc"),ledgerPath=resolve(root,"ledger.cc"),deliveryPath=resolve(root,"deliveries.cc"),keyPath=resolve(root,"bifrost.seed"),anchorPath=resolve(root,"executor.msgpack"),defs=buildOperatorDefinitions(cache.defineDocumentType),feedback=buildFeedbackDefinitions(cache.defineDocumentType),requests=cache.CultCache.builder().withDocumentType(defs.request).withGenericStore(new cache.SingleFileMessagePackBackingStore(requestPath)).build(),privateCache=cache.CultCache.builder().withDocumentType(feedback.actorLink).withGenericStore(new cache.SingleFileMessagePackBackingStore(privatePath)).build(),issued=new Date(),expires=new Date(issued.getTime()+60000),request={schemaName:"voidbot.discord.epiphany_operator_request",schemaVersion:"voidbot.discord.epiphany_operator_request.v0",requestId:"interaction-1",commandId:"interaction-1",nonce:"interaction-1",sourceEventId:"interaction-1",sourceActorDiscordId:"owner",discordGuildId:"guild",discordChannelId:"ops",discordMessageId:"interaction-1",targetRuntimeId:"epiphany-yggdrasil",issuedAt:issued.toISOString(),expiresAt:expires.toISOString(),producerId:"voidbot",producerRuntimeId:"voidbot-yggdrasil",authorityClass:"operator_request_only",status:"pending",command:{kind:"status"}};await requests.put(defs.request,request.requestId,request);await privateCache.put(feedback.actorLink,"guild:owner",{schemaName:"bifrost.discord.actor_link",schemaVersion:"bifrost.discord.actor_link.v0",linkId:"guild:owner",guildId:"guild",discordAuthorId:"owner",bifrostActorId:"actor",heimdallActorRef:"heimdall:actor:1",linkedAt:issued.toISOString()});await enrollOperatorIdentity(keyPath);const executor=await enrollOperatorIdentity(resolve(root,"executor.seed")),anchor=trustAnchor(executor,issued.toISOString());await writeFile(anchorPath,msgpack.encode([anchor.schemaVersion,anchor.identityId,anchor.publicKey,anchor.assurance,anchor.identityCreatedAt,anchor.sourceIdentityRecordSha256]));const socket=createSocket("udp4");await new Promise((ok,fail)=>{socket.once("error",fail);socket.bind(0,"127.0.0.1",ok)});const server=new cultnet.CultNetRudpSocketTransportConnection({runtimeId:request.targetRuntimeId,socket,mode:"server",connectionId:0xe91f0001,resendDelayMs:25,resendPollMs:5});server.on("frame",frame=>{const wire=msgpack.decode(frame.payload),admission=msgpack.decode(wire.document.payload),completedAt=new Date(issued.getTime()+1000).toISOString(),result={schemaVersion:"epiphany.operator_command.result.v1",resultId:"result-1",commandId:admission.packet.commandId,packetSha256:admission.packetSha256,targetRuntimeId:admission.packet.targetRuntimeId,disposition:"observed",consequenceKind:"operator-snapshot",consequenceRef:"snapshot-1",completedAt,privateStateExposed:false,operatorStatus:"sleeping",stateStatus:"ready",coordinatorAction:"",brakeStatus:"engaged",detail:"bounded",reviews:[],reviewCandidateId:"",reviewDecision:""},receipt={schemaVersion:"epiphany.operator_command.sealed_result.v1",result,resultPayloadSha256:`sha256-${createHash("sha256").update(msgpack.encode(result)).digest("hex")}`,commandId:result.commandId,packetSha256:result.packetSha256,targetRuntimeId:result.targetRuntimeId,completedAt,providerIdentityId:executor.identityId,privateStateExposed:false,executorSignature:new Uint8Array()};receipt.executorSignature=new Uint8Array(signPurpose(executor,EPIPHANY_RESULT_SIGNING_PURPOSE,msgpack.encode(resultReceiptTuple(receipt))));server.send("schema",msgpack.encode({schemaVersion:"cultnet.document_put_raw.v0",messageId:"result-1",document:{schemaId:"epiphany.operator_command.sealed_result",recordKey:receipt.commandId,storedAt:receipt.completedAt,payloadEncoding:"messagepack",payload:msgpack.encode(receipt),sourceRuntimeId:receipt.targetRuntimeId,sourceAgentId:receipt.providerIdentityId,sourceRole:"epiphany-operator-command-executor",tags:["cultnet.transport.rudp.v0"]}}));});try{const port=socket.address().port,run=spawn(process.execPath,[resolve("tools/epiphany-operator-worker.mjs"),"once","--request-store",requestPath,"--private-store",privatePath,"--ledger-store",ledgerPath,"--delivery-store",deliveryPath,"--private-key",keyPath,"--epiphany-trust-anchor",anchorPath,"--epiphany-rudp",`rudp://127.0.0.1:${port}`,"--owner-discord-id","owner"],{cwd:resolve("."),stdio:["ignore","pipe","pipe"]}),output=await collect(run);assert.equal(output.code,0,output.stderr);const pulse=JSON.parse(output.stdout.trim());assert.equal(pulse.status,"completed");const ledger=await DurableOperatorLedger.open(ledgerPath,{cultcacheRuntime:cache}),outbox=await EpiphanyOperatorDiscordOutbox.open(deliveryPath,{cultcacheRuntime:cache,encode:msgpack.encode});assert.equal(ledger.getCompletion(request.commandId).providerIdentityId,executor.identityId);const delivery=outbox.get(request.requestId);assert.equal(delivery.status,"completed");assert.equal(delivery.discordInteractionId,request.sourceEventId);assert.match(delivery.requestPayloadSha256,/^sha256-[0-9a-f]{64}$/);assert.equal(delivery.privateStateExposed,false);}finally{server.close();}}); function collect(child){return new Promise(resolveExit=>{let stdout="",stderr="";child.stdout.on("data",x=>stdout+=x);child.stderr.on("data",x=>stderr+=x);child.on("exit",code=>resolveExit({code,stdout,stderr}));});} diff --git a/tools/epiphany-operator-command-documents.mjs b/tools/epiphany-operator-command-documents.mjs index ec5bb8b..8d80e88 100644 --- a/tools/epiphany-operator-command-documents.mjs +++ b/tools/epiphany-operator-command-documents.mjs @@ -1,36 +1,43 @@ export const operatorRequestType="voidbot.discord.epiphany_operator_request"; export const operatorRequestSchema="voidbot.discord.epiphany_operator_request.v0"; export const operatorAdmissionType="bifrost.operator_command.delivery"; -export const operatorAdmissionSchema="bifrost.operator_command.delivery.v0"; +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.v0"; +export const operatorResultSchema="epiphany.operator_command.result.v1"; +export const legacyOperatorResultSchema="epiphany.operator_command.result.v0"; export const operatorResultReceiptType="epiphany.operator_command.sealed_result"; -export const operatorResultReceiptSchema="epiphany.operator_command.sealed_result.v0"; +export const operatorResultReceiptSchema="epiphany.operator_command.sealed_result.v1"; +export const legacyOperatorResultReceiptSchema="epiphany.operator_command.sealed_result.v0"; export const operatorTerminalType="bifrost.operator_command.terminal"; export const operatorTerminalSchema="bifrost.operator_command.terminal.v0"; export const operatorDiscordDeliveryType="bifrost.discord.epiphany_operator_delivery"; -export const operatorDiscordDeliverySchema="bifrost.discord.epiphany_operator_delivery.v0"; +export const operatorDiscordDeliverySchema="bifrost.discord.epiphany_operator_delivery.v1"; -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",parseOperatorAdmission),result:make(operatorResultType,operatorResultSchema,"resultId",parseOperatorResult),sealedResult:make(operatorResultReceiptType,operatorResultReceiptSchema,"commandId",parseOperatorResultReceipt),terminal:make(operatorTerminalType,operatorTerminalSchema,"requestId",parseOperatorTerminal),discordDelivery:make(operatorDiscordDeliveryType,operatorDiscordDeliverySchema,"deliveryId",parseOperatorDiscordDelivery)};} +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){v=exactRecord(v,["schemaName","schemaVersion","admissionId","packet","packetSha256","sourceObserverId","sourceObserverRuntimeId","provider","bifrostAdmissionReceiptId","authority","providerIdentityId","providerSignature"],"operator admission");exact(v.schemaName,operatorAdmissionType,"schemaName");exact(v.schemaVersion,operatorAdmissionSchema,"schemaVersion");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);return v;} -export function parseOperatorResult(v){v=exactRecord(v,["schemaVersion","resultId","commandId","packetSha256","targetRuntimeId","disposition","consequenceKind","consequenceRef","completedAt","privateStateExposed","operatorStatus","stateStatus","coordinatorAction","brakeStatus","detail"],"operator result");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.`);date(v.completedAt,"completedAt");return v;} -export function parseOperatorResultReceipt(v){v=exactRecord(v,["schemaVersion","result","resultPayloadSha256","commandId","packetSha256","targetRuntimeId","completedAt","providerIdentityId","privateStateExposed","executorSignature"],"operator sealed result receipt");exact(v.schemaVersion,operatorResultReceiptSchema,"schemaVersion");parseOperatorResult(v.result);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 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 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","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","sealedResultPayloadSha256","executorSignatureSha256","resultProviderIdentityId"])if(typeof v[k]!=="string")throw new Error(`${k} must be a string.`);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","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 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){return [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];} +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 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(v.kind==="status"||v.kind==="wake")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;}throw new Error("command kind is invalid.");} +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.");} // rmp-serde's compact enum form is the snake-case unit variant string, or a // two-element [variant, sole-field] tuple for these one-field struct variants. -function commandTuple(v){if(v.kind==="sleep")return ["sleep",v.reason];if(v.kind==="directive")return ["directive",v.objective];return v.kind;} +function commandTuple(v){if(v.kind==="sleep")return ["sleep",v.reason];if(v.kind==="directive")return ["directive",v.objective];if(v.kind==="review")return ["review",[v.mindRequestId,v.candidateId,v.candidateSha256,v.expectedModelRevision,v.expectedModelHash,v.decision]];return 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];} +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;} function exact(v,e,label){if(v!==e)throw new Error(`${label} must be ${e}.`);} diff --git a/tools/epiphany-operator-commands.mjs b/tools/epiphany-operator-commands.mjs index db934c9..6220e8e 100644 --- a/tools/epiphany-operator-commands.mjs +++ b/tools/epiphany-operator-commands.mjs @@ -3,7 +3,7 @@ import { resolve } from "node:path"; import { admissionTuple,operatorAdmissionSchema,parseOperatorAdmission,parseOperatorRequest,parseOperatorResult,parseOperatorResultReceipt,packetTuple } from "./epiphany-operator-command-documents.mjs"; import { enrollSignedIdentity,loadSignedIdentity,signPurpose,trustAnchor } from "./bifrost-signed-identity.mjs"; -export const OPERATOR_SIGNING_PURPOSE="bifrost.operator-command.delivery.v0"; +export const OPERATOR_SIGNING_PURPOSE="bifrost.operator-command.delivery.v1"; export const defaultOperatorIdentityPath=()=>process.env.BIFROST_EPIPHANY_OPERATOR_PRIVATE_KEY??(process.platform==="win32"?resolve(".bifrost","private","epiphany-operator-ed25519.seed"):"/var/lib/gamecult/bifrost/epiphany-operator/operator-ed25519.seed"); export class OperatorCommandPermanentRefusal extends Error { constructor(code,message){super(message);this.name="OperatorCommandPermanentRefusal";this.code=code;this.retryable=false;} } @@ -17,9 +17,9 @@ export async function admitOperatorRequest({request,ownerDiscordId,actorLink,ide if(existingAdmission){ await compareAdmission(ledger,request,existingAdmission); if(existingCompletion){ - parseOperatorResultReceipt(existingCompletion); + parseOperatorResultReceipt(existingCompletion,{allowLegacy:true}); if(existingCompletion.commandId!==existingAdmission.packet.commandId||existingCompletion.packetSha256!==existingAdmission.packetSha256||existingCompletion.targetRuntimeId!==existingAdmission.packet.targetRuntimeId)refuse("command-id-collision","operator completion is not bound to this exact durable admission"); - transport.verifyReceipt(existingAdmission,existingCompletion); + parseOperatorAdmission(existingAdmission,{allowLegacy:true});transport.verifyReceipt(existingAdmission,existingCompletion); return {admission:existingAdmission,result:existingCompletion.result,receipt:existingCompletion,resultProviderIdentityId:existingCompletion.providerIdentityId,recovered:true}; } return executeAdmission({request,admission:existingAdmission,transport,ledger,recovered:true}); diff --git a/tools/epiphany-operator-cultnet-rudp.mjs b/tools/epiphany-operator-cultnet-rudp.mjs index 095316a..dfb9445 100644 --- a/tools/epiphany-operator-cultnet-rudp.mjs +++ b/tools/epiphany-operator-cultnet-rudp.mjs @@ -6,6 +6,7 @@ import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; import { operatorAdmissionType, + legacyOperatorResultReceiptSchema, operatorResultReceiptType, parseOperatorAdmission, parseOperatorResultReceipt, @@ -13,7 +14,8 @@ import { } from "./epiphany-operator-command-documents.mjs"; import { verifyPurpose } from "./bifrost-signed-identity.mjs"; -export const EPIPHANY_RESULT_SIGNING_PURPOSE="epiphany.operator-command.sealed-result.v0"; +export const EPIPHANY_RESULT_SIGNING_PURPOSE="epiphany.operator-command.sealed-result.v1"; +export const LEGACY_EPIPHANY_RESULT_SIGNING_PURPOSE="epiphany.operator-command.sealed-result.v0"; const RUDP_CONNECTION_ID=0xe91f0001; export class EpiphanyOperatorCultNetRudpTransport { @@ -41,7 +43,7 @@ export class EpiphanyOperatorCultNetRudpTransport { } async #executeOne(admission){ - parseOperatorAdmission(admission); + parseOperatorAdmission(admission,{allowLegacy:true}); const socket=this.socketFactory("udp4"); await bindSocket(socket); const connection=this.connectionFactory({runtimeId:this.runtimeId,socket,mode:"client",remoteHost:this.endpoint.host,remotePort:this.endpoint.port,connectionId:RUDP_CONNECTION_ID,resendDelayMs:50,resendPollMs:10}); @@ -61,7 +63,7 @@ export class EpiphanyOperatorCultNetRudpTransport { export async function loadEpiphanyOperatorTrustAnchor(path,{decode}={}){const runtime=decode?{decode}:loadCultNetRuntime().msgpack,value=runtime.decode(await readFile(path));if(!Array.isArray(value)||value.length!==6)throw new Error("Epiphany operator trust anchor must be the raw compact 6-field MessagePack crossing artifact.");return parseTrustAnchor({schemaVersion:value[0],identityId:value[1],publicKey:value[2],assurance:value[3],identityCreatedAt:value[4],sourceIdentityRecordSha256:value[5]});} export function verifySealedReceipt({receipt,admission,trustedIdentity,encode}){ - parseOperatorAdmission(admission);parseOperatorResultReceipt(receipt);const anchor=parseTrustAnchor(trustedIdentity); + parseOperatorAdmission(admission,{allowLegacy:true});parseOperatorResultReceipt(receipt,{allowLegacy:true});const anchor=parseTrustAnchor(trustedIdentity); 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."); @@ -73,7 +75,8 @@ export function verifySealedReceipt({receipt,admission,trustedIdentity,encode}){ if(completedx instanceof Uint8Array?Buffer.from(x).toString("hex"):x);} diff --git a/tools/provider-advertisement.mjs b/tools/provider-advertisement.mjs index 63b54c4..9ef52f6 100644 --- a/tools/provider-advertisement.mjs +++ b/tools/provider-advertisement.mjs @@ -158,19 +158,18 @@ const surfaceSchemaId = "gamecult.eve.surface_state.v1"; const interfaceBindingDocumentType = "gamecult.eve.interface_binding"; const interfaceBindingSchemaId = "gamecult.eve.interface_binding.v1"; const verseId = "bifrost.local"; -const rootVerse = "asgard"; -const currentMachine = "starfire"; +const rootVerse = requiredDeploymentIdentity("BIFROST_ROOT_VERSE"); +const currentMachine = requiredDeploymentIdentity("BIFROST_MACHINE_ID"); const canonicalService = `${rootVerse}.bifrost`; const locatedService = `${rootVerse}.${currentMachine}.bifrost`; -const plannedLocatedService = `${rootVerse}.yggdrasil.bifrost`; const motionSurfaceCultMeshUri = `cultmesh://${locatedService}/eve/governance/surface`; const motionCommandCultMeshUri = `cultmesh://${locatedService}/commands/motion`; const epiphanyOperatorRequestSchemaId = "voidbot.discord.epiphany_operator_request.v0"; -const epiphanyOperatorAdmissionSchemaId = "bifrost.operator_command.delivery.v0"; -const epiphanyOperatorResultSchemaId = "epiphany.operator_command.result.v0"; -const epiphanyOperatorSealedResultSchemaId = "epiphany.operator_command.sealed_result.v0"; +const epiphanyOperatorAdmissionSchemaId = "bifrost.operator_command.delivery.v1"; +const epiphanyOperatorResultSchemaId = "epiphany.operator_command.result.v1"; +const epiphanyOperatorSealedResultSchemaId = "epiphany.operator_command.sealed_result.v1"; const epiphanyOperatorTerminalSchemaId = "bifrost.operator_command.terminal.v0"; -const epiphanyOperatorDiscordDeliverySchemaId = "bifrost.discord.epiphany_operator_delivery.v0"; +const epiphanyOperatorDiscordDeliverySchemaId = "bifrost.discord.epiphany_operator_delivery.v1"; const advertisementDefinition = defineDocumentType({ type: documentType, @@ -323,7 +322,6 @@ function buildAdvertisement(options) { rootVerse, canonicalService, locatedService, - plannedLocatedService, cultMeshAddress: locatedService, endpoints: [ endpoint("operator-tui", `${locatedService}/eve/tui`, "gamecult.eve.surface.v1", ["tui", "nightwing-tui"]), @@ -349,7 +347,7 @@ function buildAdvertisement(options) { presentationOwner: "Eve/CultUI", discoveryOwner: "Odin through CultMesh", stateOwner: "Bifrost typed state with CultCache .cc witnesses or export paths", - runtimeMigration: `currently ${locatedService}; planned move target ${plannedLocatedService}`, + runtimeMigration: `deployment identity ${locatedService}`, }, namespaces: [ namespace("gamecult.bifrost.service", "service registration, build/version, schema catalog, and command discovery"), @@ -971,7 +969,6 @@ function buildInterfaceBinding(surface, stats) { cultMeshAddress: `${locatedService}/eve/tui`, canonicalService, locatedService, - plannedLocatedService, endpoints: [ endpoint("operator-tui", `${locatedService}/eve/tui`, "gamecult.eve.surface.v1", ["tui", "nightwing-tui"]), endpoint("operator-gui", `${locatedService}/eve/gui`, "gamecult.eve.surface.v1", ["gui", "browser", "eve-native"]), @@ -1186,7 +1183,6 @@ function parseAdvertisement(input, { requireCurrentContract = true } = {}) { rootVerse: optionalString(input.rootVerse, rootVerse), canonicalService: optionalString(input.canonicalService, canonicalService), locatedService: optionalString(input.locatedService, locatedService), - plannedLocatedService: typeof input.plannedLocatedService === "string" ? input.plannedLocatedService.trim() : "", cultMeshAddress: optionalString(input.cultMeshAddress, locatedService), endpoints: Array.isArray(input.endpoints) ? requireObjectArray(input.endpoints, "endpoints") : [], routes: Array.isArray(input.routes) ? requireObjectArray(input.routes, "routes") : [], @@ -1305,6 +1301,14 @@ function resolveOptionPath(path) { return resolve(process.cwd(), path); } +function requiredDeploymentIdentity(name) { + const value = process.env[name]; + if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(value)) { + throw new Error(`${name} must explicitly name this deployment using lowercase letters, digits, or hyphens.`); + } + return value; +} + function printJson(value) { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); }