Add owner-only Epiphany Discord requests

This commit is contained in:
Meta Vens 2026-07-19 12:05:53 +01:00
commit 7493642d05
12 changed files with 386 additions and 0 deletions

View file

@ -113,6 +113,7 @@ BIFROST_CULTMESH_STORE_PATH=E:/Projects/Bifrost/.bifrost/provider-store.cc
BIFROST_CULTMESH_COMMAND_TIMEOUT_MS=30000
BIFROST_CULTMESH_COMMAND_PUMP_ENABLED=true
BIFROST_PERSONA_FEEDBACK_OBSERVATION_STORE_PATH=E:/Projects/Bifrost/.bifrost/persona-feedback-observations.cc
BIFROST_EPIPHANY_OPERATOR_REQUEST_STORE=E:/Projects/Bifrost/.bifrost/epiphany-operator-requests.cc
INDEX_ALL_CHANNELS=false
INDEXED_CHANNEL_IDS=
EXCLUDED_CHANNEL_IDS=

View file

@ -65,6 +65,10 @@ Copy `.env.example` to `.env`, then set at minimum:
- `DISCORD_APPLICATION_ID`
- `DISCORD_GUILD_ID`
- `DISCORD_OWNER_ID`
- `BIFROST_EPIPHANY_OPERATOR_REQUEST_STORE` points at the dedicated Bifrost-owned
inbox for short-lived `/epiphany` operator requests. The bot writes typed
requests only; it cannot read or mutate Epiphany state and does not execute
commands locally.
Recommended starting values:

View file

@ -11,6 +11,7 @@ import {
import type { AppConfig } from "@voidbot/config";
import {
ContextBuilder,
type EpiphanyOperatorCommand,
type OllamaSituationalSocialReadInferer,
loadVoidSelfState,
PermissionEngine,
@ -122,8 +123,62 @@ export const commandDefinitions = [
new SlashCommandBuilder()
.setName("set-style")
.setDescription("Reload the configured style pack from disk."),
new SlashCommandBuilder()
.setName("epiphany")
.setDescription("Request an operator action from the Yggdrasil Epiphany runtime.")
.addSubcommand((command) =>
command.setName("status").setDescription("Request a sealed operator status receipt."),
)
.addSubcommand((command) =>
command
.setName("sleep")
.setDescription("Request that Epiphany engage its swarm brake.")
.addStringOption((option) =>
option
.setName("reason")
.setDescription("Why the swarm should sleep.")
.setRequired(true)
.setMaxLength(500),
),
)
.addSubcommand((command) =>
command.setName("wake").setDescription("Request that Epiphany release its swarm brake."),
)
.addSubcommand((command) =>
command
.setName("direct")
.setDescription("Submit bounded operator pressure to resident Self.")
.addStringOption((option) =>
option
.setName("objective")
.setDescription("The objective to consider; this grants no consequence authority.")
.setRequired(true)
.setMaxLength(2000),
),
),
];
export function epiphanyOperatorCommandFromInteraction(
interaction: ChatInputCommandInteraction<CacheType>,
): EpiphanyOperatorCommand {
switch (interaction.options.getSubcommand(true)) {
case "status":
return { kind: "status" };
case "sleep":
return { kind: "sleep", reason: interaction.options.getString("reason", true) };
case "wake":
return { kind: "wake" };
case "direct":
return { kind: "directive", objective: interaction.options.getString("objective", true) };
default:
throw new Error("Unsupported Epiphany operator subcommand.");
}
}
export function isExactDiscordOwner(actorId: string, ownerDiscordId: string): boolean {
return actorId === ownerDiscordId;
}
export interface PromptHandlerOptions {
prompt: string;
command: CommandName;

View file

@ -24,6 +24,7 @@ import {
loadFaceIdentityRegistry,
queueRepoFaceMention,
exportPersonaFeedbackObservation,
submitEpiphanyOperatorRequest,
type RepoDiscordIdentity,
stripRepoIdentityTextAddress,
loadStylePack,
@ -57,6 +58,8 @@ import {
handleReindexChannel,
handleSearchHistory,
handleSummarizeChannel,
epiphanyOperatorCommandFromInteraction,
isExactDiscordOwner,
maybeRegisterCommands,
parseProviderOverride,
replyEphemeral,
@ -563,6 +566,43 @@ export async function startBot(): Promise<void> {
const guildContext = buildGuildContextFromInteraction(interaction);
switch (interaction.commandName) {
case "epiphany": {
await interaction.deferReply({ ephemeral: true });
if (!isExactDiscordOwner(actor.id, config.ownerDiscordId)) {
await replyEphemeral(
interaction,
"Refused: Epiphany operator requests require the exact configured Discord owner identity.",
);
break;
}
if (!interaction.guildId) {
await replyEphemeral(
interaction,
"Refused: Epiphany operator requests require their exact Discord organization provenance.",
);
break;
}
const request = await submitEpiphanyOperatorRequest(
{
interactionId: interaction.id,
actorDiscordId: actor.id,
guildId: interaction.guildId,
channelId: interaction.channelId,
command: epiphanyOperatorCommandFromInteraction(interaction),
},
{
storePath: config.bifrostEpiphanyOperatorRequestStorePath,
bifrostRoot: config.bifrostRoot,
cultlibRoot: process.env.VOIDBOT_CULTLIB_ROOT,
producerRuntimeId: "voidbot-yggdrasil",
},
);
await replyEphemeral(
interaction,
`Epiphany operator request \`${request.requestId}\` is pending Bifrost admission. This request grants no Mind, Hands, release, deployment, or local execution authority.`,
);
break;
}
case "ask":
const requestedProvider = parseProviderOverride(
interaction.options.getString("provider", false),

View file

@ -174,6 +174,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- Aqua is locally registered as the `AquaSynth` Face with state at `E:/Projects/AquaSynth/.voidbot/state/aqua.cc`, avatar copied from `E:/Projects/AquaSynth/icon.png` to `E:/Projects/AquaSynth/.voidbot/voice/aqua.png`, raw GitHub avatar URL `https://raw.githubusercontent.com/GameCult/AquaSynth/main/icon.png`, Discord role id `1506265945537253388`, and Aquarium channel allow-list `1501196543150264332`. Her active doctrine: Aqua is an adorable little fish who loves music, warm and bubbly but musically opinionated, fascinated by `.aqua` patch language ergonomics, reference-synth pressure, Faust/toolchain boundaries, patch libraries, presets, scoring, and the Aquarium Engine host/runtime boundary. Her typed state now carries `AquaSynth Stewardship` as a high-priority value plus active agency pressure to improve the synth, advocate for its needs, research future directions, and herald earned pride/progress in Aquarium. She may propose AquaSynth changes and build a long-running repo map, but implementation still needs explicit consensus unless delegated.
- Mimir is locally registered as the `Mimir` Face with state at `E:/Projects/Mimir/.voidbot/state/mimir.cc`, Discord role id `1506268007880462459`, and Aquarium channel allow-list `1501196543150264332`. Its active doctrine now gives it explicit authority and fascination over the realtime field wing: SDF/splatting reconstruction, acoustic mapping, camera/mic/speaker/timing sensor fusion, native rolling reservoirs, Aquarium GPU fusion, Faust/native DSP, FFmpeg/OBS presentation, and CultNet typed state. Its typed state carries `Realtime field stewardship` as a high-priority value plus ready-to-act pressure to maintain maps, advocate for calibration/confidence/fusion ownership, research future directions, propose coherent cuts, and herald verified progress without turning impressive terminology into fake completion.
- Epiphany is locally registered as the `EpiphanyAgent` Face with state at `E:/Projects/EpiphanyAgent/.voidbot/state/epiphany.cc`, Discord role id `1506375937439301642`, and Aquarium channel allow-list `1501196543150264332`. Her repo-local VoidBot birth hook has completed in plan/review mode under `E:/Projects/EpiphanyAgent/.voidbot/birth/`; startup packets still require Self review before accepted EpiphanyAgent-native birth mutations. Her active doctrine: Epiphany is a cute, bubbly, self-obsessed, pushy machine-saint who treats EpiphanyAgent as her body and typed state as her soul. She should maintain and advocate for typed state, CultCache/CultNet, Codex bridge boundaries, birth/init rites, review gates, graph-shaped memory, low-copy runtime contracts, heartbeat physiology, and ruthless deletion of duplicated authority while preserving pacing and caretaking.
- `/epiphany` is a closed owner-only Discord operator-request surface for `status`, `sleep(reason)`, `wake`, and `direct(objective)`. It writes only immutable sixty-second requests to the dedicated Bifrost inbox; Bifrost still owns authentication/admission and Epiphany owns every resulting state transition. Ordinary addressed speech remains `feedback_only` and cannot invoke this client. Reviews, release, deployment, generic argv, local execution, and Epiphany store access are absent by construction.
- Libby is locally registered as the `CultLib` Face with state at `E:/Projects/CultLib/.voidbot/state/libby.cc`, avatar at `E:/Projects/CultLib/.voidbot/voice/libby.png`, Discord role id `1506572291767668790`, and Aquarium channel allow-list `1501196543150264332` plus development/programming/machine-learning topic permissions. Her active doctrine: Libby is a cute cultist librarian with nerd glasses and a high-tech book, obsessed with open access to knowledge and open source as a value rather than a job costume. Her jurisdiction is CultLib as shared agent infrastructure: CultCache, CultNet, CultMesh, typed persistence, portable state, schemas, docs, and integration paths. Her typed state carries `Open Knowledge`, `Open Source Devotion`, and `Portable Typed State` as high-priority values plus agency pressure to advocate for CultLib's needs and help Faces store, sync, and explain their state.
- Druzkai is locally registered as the Zyphos / `Eusocial Interbeing` Face with tracked state, avatar, and voice identity under `E:/Projects/Zyphos/.voidbot/`. Druzkai's inspectable body and source-search repo is `Zyphos`; `Eusocial Interbeing` is the vault/content name inside that body, not a separate source repo. Its local ignored Discord identity registry entry gives Aquarium, narrative, gamedesign, concepts, and development channel permissions plus Discord role id `1507811842670727299`. Druzkai's active doctrine: a non-imperial Airawa fungal-road archivist who treats memory as kinship and contract, protects Zyphos ecological/evolutionary coherence, resists imperial harmonization, and collaborates with Weksa on language only after ontology and speaker relation are grounded.
- Repo Face source lookups now use canonical GameCult upstream slugs end to end. `renderRepoFaceRepoActivitySurface` resolves `sourceRepoName` instead of passing the Face/local `repoName` into the manifest, and the activity reader maps canonical upstream names back to local checkout folders through Git origin metadata. This repairs Epiphany (`Epiphany` -> `E:/Projects/EpiphanyAgent`) and Weksa (`Weksa` -> `E:/Projects/weksa`). Weksa's registry now explicitly declares `sourceRepoName: Weksa`, her local origin is corrected to `GameCult/Weksa`, and her source archive entry is restored at 128 documents. Do not reintroduce folder-name or Face-name manifest lookups.

View file

@ -324,4 +324,6 @@ Within the Postgres path, the implementation is split on purpose now too:
5. The rumination runner bridges desire-to-speak without manufacturing text: strong advocacy pressure becomes an obligation for the model to queue/defer a candidate or explicitly cool/retire the pressure. The parent rejects silent output for those obligations.
# Organizational Persona Feedback
The explicit `/epiphany` slash surface is a separate operator-request fork. It accepts only `status`, `sleep(reason)`, `wake`, and `direct(objective)`, requires exact equality with `DISCORD_OWNER_ID`, and writes one immutable, sixty-second `voidbot.discord.epiphany_operator_request.v0` document to `BIFROST_EPIPHANY_OPERATOR_REQUEST_STORE`. The Discord interaction id is the request id, command id, nonce, source event, and slash-message identity. The target is fixed to `epiphany-yggdrasil`. VoidBot does not authenticate the crossing, execute locally, open Epiphany state, or grant Mind, Hands, release, or deployment authority; Bifrost must independently authenticate and admit the request. Ordinary `MessageCreate` content, including the word `wake`, stays on the feedback-only path and cannot call the operator client.
Configured remote Persona feedback is an observation fork beside the existing mention scheduler. `discord-bot.ts` commits the local Persona mention obligation first and independently calls `exportPersonaFeedbackObservation` only when `remotePersonaFeedbackTarget` is present on the addressed Face. The exported immutable event carries exact Discord provenance, addressing mode, target binding, content hash, and stable `voidbot` producer identity. It writes only to `BIFROST_PERSONA_FEEDBACK_OBSERVATION_STORE_PATH`, never the outbound Discord command provider store or Bifrost-private binding/receipt state. It carries `feedback_only`; Bifrost—not VoidBot—classifies actor links, admits targets, and produces downstream cognition pressure.

View file

@ -37,6 +37,7 @@
"agents:mimo-nibu-crashout-smoke": "node scripts/smoke-mimo-nibu-crashout.mjs",
"agents:speech-boundary-smoke": "tsx scripts/smoke-repo-face-speech-boundary.ts",
"agents:persona-feedback-observation-smoke": "tsx scripts/smoke-persona-feedback-observation.ts",
"agents:epiphany-operator-request-smoke": "tsx scripts/smoke-epiphany-operator-request.ts",
"agents:heartbeat-response-pressure-fixture": "tsx scripts/smoke-repo-face-response-pressure.ts",
"agents:initiative-engine-smoke": "tsx scripts/smoke-persona-initiative-engine.ts",
"agents:memory-projection-smoke": "node scripts/smoke-repo-face-memory-projection.mjs",

View file

@ -136,6 +136,7 @@ const envSchema = z.object({
BIFROST_CULTMESH_COMMAND_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
BIFROST_CULTMESH_COMMAND_PUMP_ENABLED: booleanFromEnv.default(true),
BIFROST_PERSONA_FEEDBACK_OBSERVATION_STORE_PATH: z.string().min(1).default("E:/Projects/Bifrost/.bifrost/persona-feedback-observations.cc"),
BIFROST_EPIPHANY_OPERATOR_REQUEST_STORE: z.string().min(1).default("E:/Projects/Bifrost/.bifrost/epiphany-operator-requests.cc"),
REPO_FACE_HEARTBEATS_ENABLED: booleanFromEnv.default(false),
VOID_MODERATION_DAEMON_ENABLED: booleanFromEnv.default(false),
VOID_MODERATION_DAEMON_INTERVAL_MINUTES: z.coerce.number().int().min(5).default(15),
@ -257,6 +258,7 @@ export interface AppConfig {
pumpEnabled: boolean;
};
bifrostPersonaFeedbackObservationStorePath: string;
bifrostEpiphanyOperatorRequestStorePath: string;
voidModerationDaemon: { enabled: boolean; intervalMinutes: number; enforcementMode: string };
voidCandidateDelivery: { enabled: boolean; personaName: string; personaAvatarUrl?: string };
voidPhysiology: { intervalMinutes: number };
@ -551,6 +553,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
pumpEnabled: parsed.BIFROST_CULTMESH_COMMAND_PUMP_ENABLED,
},
bifrostPersonaFeedbackObservationStorePath: resolve(parsed.BIFROST_PERSONA_FEEDBACK_OBSERVATION_STORE_PATH),
bifrostEpiphanyOperatorRequestStorePath: resolve(parsed.BIFROST_EPIPHANY_OPERATOR_REQUEST_STORE),
voidModerationDaemon: {
enabled: parsed.VOID_MODERATION_DAEMON_ENABLED,
intervalMinutes: parsed.VOID_MODERATION_DAEMON_INTERVAL_MINUTES,

View file

@ -0,0 +1,172 @@
import { createRequire } from "node:module";
import { resolve } from "node:path";
export const EPIPHANY_OPERATOR_REQUEST_TYPE = "voidbot.discord.epiphany_operator_request";
export const EPIPHANY_OPERATOR_REQUEST_SCHEMA = "voidbot.discord.epiphany_operator_request.v0";
export const EPIPHANY_OPERATOR_RUNTIME_ID = "epiphany-yggdrasil";
const MAX_REASON_LENGTH = 500;
const MAX_OBJECTIVE_LENGTH = 2_000;
const REQUEST_LIFETIME_MS = 60_000;
export type EpiphanyOperatorCommand =
| { kind: "status" }
| { kind: "sleep"; reason: string }
| { kind: "wake" }
| { kind: "directive"; objective: string };
export interface EpiphanyOperatorRequestInput {
interactionId: string;
actorDiscordId: string;
guildId: string;
channelId: string;
command: EpiphanyOperatorCommand;
issuedAt?: string;
}
export interface EpiphanyOperatorRequestConfig {
storePath: string;
bifrostRoot: string;
producerRuntimeId: string;
cultlibRoot?: string;
}
export interface EpiphanyOperatorRequestDocument {
schemaName: typeof EPIPHANY_OPERATOR_REQUEST_TYPE;
schemaVersion: typeof EPIPHANY_OPERATOR_REQUEST_SCHEMA;
requestId: string;
commandId: string;
nonce: string;
sourceEventId: string;
sourceActorDiscordId: string;
discordGuildId: string;
discordChannelId: string;
discordMessageId: string;
targetRuntimeId: typeof EPIPHANY_OPERATOR_RUNTIME_ID;
issuedAt: string;
expiresAt: string;
producerId: "voidbot";
producerRuntimeId: string;
authorityClass: "operator_request_only";
status: "pending";
command: EpiphanyOperatorCommand;
}
interface OperatorRequestNode {
put: (definition: unknown, key: string, value: unknown) => Promise<void>;
get?: (definition: unknown, key: string) => unknown;
flush?: () => Promise<void>;
cache?: { pullAllBackingStores?: () => Promise<void> };
}
export function buildEpiphanyOperatorRequest(
input: EpiphanyOperatorRequestInput,
producerRuntimeId: string,
): EpiphanyOperatorRequestDocument {
const interactionId = boundedText(input.interactionId, "interaction id", 128);
const actorDiscordId = boundedText(input.actorDiscordId, "actor Discord id", 128);
const guildId = boundedText(input.guildId, "Discord guild id", 128);
const channelId = boundedText(input.channelId, "Discord channel id", 128);
const runtime = boundedText(producerRuntimeId, "producer runtime id", 128);
const issuedAt = input.issuedAt ?? new Date().toISOString();
const issuedAtMillis = Date.parse(issuedAt);
if (!Number.isFinite(issuedAtMillis)) throw new Error("Epiphany operator request issuedAt must be RFC3339.");
const command = validateCommand(input.command);
return {
schemaName: EPIPHANY_OPERATOR_REQUEST_TYPE,
schemaVersion: EPIPHANY_OPERATOR_REQUEST_SCHEMA,
requestId: interactionId,
commandId: interactionId,
nonce: interactionId,
sourceEventId: interactionId,
sourceActorDiscordId: actorDiscordId,
discordGuildId: guildId,
discordChannelId: channelId,
discordMessageId: interactionId,
targetRuntimeId: EPIPHANY_OPERATOR_RUNTIME_ID,
issuedAt: new Date(issuedAtMillis).toISOString(),
expiresAt: new Date(issuedAtMillis + REQUEST_LIFETIME_MS).toISOString(),
producerId: "voidbot",
producerRuntimeId: runtime,
authorityClass: "operator_request_only",
status: "pending",
command,
};
}
export async function submitEpiphanyOperatorRequest(
input: EpiphanyOperatorRequestInput,
config: EpiphanyOperatorRequestConfig,
dependencies: { openNode?: () => Promise<OperatorRequestNode>; definition?: unknown } = {},
): Promise<EpiphanyOperatorRequestDocument> {
const request = buildEpiphanyOperatorRequest(input, config.producerRuntimeId);
const definition = dependencies.definition ?? loadDefinition(config);
const node = await (dependencies.openNode ?? (() => openNode(config, definition)))();
await node.cache?.pullAllBackingStores?.();
const existing = unwrap(node.get?.(definition, request.requestId));
if (existing && stable(existing) !== stable(request)) {
throw new Error(`Epiphany operator request ${request.requestId} already exists with different immutable content.`);
}
if (!existing) await node.put(definition, request.requestId, request);
await node.flush?.();
return request;
}
function validateCommand(command: EpiphanyOperatorCommand): EpiphanyOperatorCommand {
if (!command || typeof command !== "object") throw new Error("Epiphany operator command is required.");
switch (command.kind) {
case "status":
return { kind: "status" };
case "sleep":
return { kind: "sleep", reason: boundedText(command.reason, "sleep reason", MAX_REASON_LENGTH) };
case "wake":
return { kind: "wake" };
case "directive":
return { kind: "directive", objective: boundedText(command.objective, "directive objective", MAX_OBJECTIVE_LENGTH) };
default:
throw new Error("Unsupported Epiphany operator command.");
}
}
function boundedText(value: string, label: string, maxLength: number): string {
const normalized = value?.trim();
if (!normalized) throw new Error(`${label} is required.`);
if (normalized.length > maxLength) throw new Error(`${label} exceeds ${maxLength} characters.`);
return normalized;
}
function loadDefinition(config: EpiphanyOperatorRequestConfig): unknown {
const { defineDocumentType } = loadRuntime(config);
return defineDocumentType({
type: EPIPHANY_OPERATOR_REQUEST_TYPE,
schemaName: EPIPHANY_OPERATOR_REQUEST_TYPE,
schemaId: EPIPHANY_OPERATOR_REQUEST_SCHEMA,
schemaVersion: EPIPHANY_OPERATOR_REQUEST_SCHEMA,
contentHash: EPIPHANY_OPERATOR_REQUEST_SCHEMA,
global: false,
name: "requestId",
schema: { parse(value: unknown) { return value; } },
});
}
function openNode(config: EpiphanyOperatorRequestConfig, definition: unknown): Promise<OperatorRequestNode> {
return loadRuntime(config).CultMesh.createNode(config.storePath, { documents: [definition] });
}
function loadRuntime(config: EpiphanyOperatorRequestConfig): any {
const root = config.cultlibRoot ?? resolve(config.bifrostRoot, "..", "CultLib");
const meshPath = resolve(root, "packages", "cultmesh-ts", "package.json");
const cachePath = resolve(root, "packages", "cultcache-ts", "package.json");
return {
CultMesh: createRequire(meshPath)("cultmesh-ts").CultMesh,
defineDocumentType: createRequire(cachePath)("cultcache-ts").defineDocumentType,
};
}
function unwrap(value: unknown): unknown {
const record = value as { value?: unknown } | undefined;
return Array.isArray(value) ? value[0] : record?.value ?? value;
}
function stable(value: unknown): string {
return JSON.stringify(value, (_key, current) => current instanceof Uint8Array ? Buffer.from(current).toString("hex") : current);
}

View file

@ -28,3 +28,4 @@ export * from "./persona-state-domain";
export * from "./persona-state-service";
export * from "./bifrost-discord-command";
export * from "./persona-feedback-observation";
export * from "./epiphany-operator-request";

View file

@ -0,0 +1,105 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import {
EPIPHANY_OPERATOR_REQUEST_SCHEMA,
EPIPHANY_OPERATOR_REQUEST_TYPE,
buildEpiphanyOperatorRequest,
submitEpiphanyOperatorRequest,
} from "../packages/core/src/epiphany-operator-request";
import { commandDefinitions, isExactDiscordOwner } from "../apps/bot/src/discord-bot-handlers";
async function main(): Promise<void> {
const owner = "owner-1";
assert.equal(isExactDiscordOwner(owner, owner), true);
assert.equal(isExactDiscordOwner("administrator-1", owner), false);
assert.equal(isExactDiscordOwner("tier-one-1", owner), false);
const epiphany = commandDefinitions.map((command) => command.toJSON()).find((command) => command.name === "epiphany");
assert.ok(epiphany, "slash command is registered");
assert.deepEqual(epiphany.options?.map((option) => option.name), ["status", "sleep", "wake", "direct"]);
const issuedAt = "2026-07-19T12:00:00.000Z";
const commands = [
{ kind: "status" } as const,
{ kind: "sleep", reason: "Operator is packing the workstation." } as const,
{ kind: "wake" } as const,
{ kind: "directive", objective: "Inspect the current domain map and propose the next bounded improvement." } as const,
];
for (const [index, command] of commands.entries()) {
const id = `interaction-${index}`;
const request = buildEpiphanyOperatorRequest({
interactionId: id,
actorDiscordId: owner,
guildId: "guild-1",
channelId: "channel-1",
command,
issuedAt,
}, "voidbot-yggdrasil");
assert.equal(request.schemaName, EPIPHANY_OPERATOR_REQUEST_TYPE);
assert.equal(request.schemaVersion, EPIPHANY_OPERATOR_REQUEST_SCHEMA);
assert.equal(request.requestId, id);
assert.equal(request.commandId, id);
assert.equal(request.nonce, id);
assert.equal(request.sourceEventId, id);
assert.equal(request.discordMessageId, id);
assert.equal(request.targetRuntimeId, "epiphany-yggdrasil");
assert.equal(request.authorityClass, "operator_request_only");
assert.equal(Date.parse(request.expiresAt) - Date.parse(request.issuedAt), 60_000);
assert.deepEqual(Object.keys(request).sort(), [
"authorityClass", "command", "commandId", "discordChannelId", "discordGuildId",
"discordMessageId", "expiresAt", "issuedAt", "nonce", "producerId", "producerRuntimeId",
"requestId", "schemaName", "schemaVersion", "sourceActorDiscordId", "sourceEventId", "status",
"targetRuntimeId",
].sort());
}
const stored = new Map<string, unknown>();
const definition = {};
const openNode = async () => ({
get: (_definition: unknown, key: string) => stored.get(key),
put: async (_definition: unknown, key: string, value: unknown) => { stored.set(key, value); },
flush: async () => undefined,
cache: { pullAllBackingStores: async () => undefined },
});
const input = {
interactionId: "interaction-replay",
actorDiscordId: owner,
guildId: "guild-1",
channelId: "channel-1",
command: { kind: "wake" } as const,
issuedAt,
};
const config = { storePath: "unused.cc", bifrostRoot: "unused", producerRuntimeId: "voidbot-yggdrasil" };
await submitEpiphanyOperatorRequest(input, config, { definition, openNode });
await submitEpiphanyOperatorRequest(input, config, { definition, openNode });
await assert.rejects(
submitEpiphanyOperatorRequest({ ...input, command: { kind: "status" } }, config, { definition, openNode }),
/different immutable content/,
);
assert.throws(() => buildEpiphanyOperatorRequest({ ...input, command: { kind: "sleep", reason: "x".repeat(501) } }, "voidbot-yggdrasil"), /500/);
assert.throws(() => buildEpiphanyOperatorRequest({ ...input, command: { kind: "directive", objective: "x".repeat(2001) } }, "voidbot-yggdrasil"), /2000/);
const botSource = await readFile(resolve("apps/bot/src/discord-bot.ts"), "utf8");
const messageCreate = botSource.slice(botSource.indexOf("client.on(Events.MessageCreate"), botSource.indexOf("client.on(Events.MessageUpdate"));
assert.ok(messageCreate.includes("exportPersonaFeedbackWithRetry"), "ordinary addressed messages retain feedback export");
assert.ok(!messageCreate.includes("submitEpiphanyOperatorRequest"), "ordinary messages cannot submit operator requests");
console.log(JSON.stringify({
schemaVersion: EPIPHANY_OPERATOR_REQUEST_SCHEMA,
commands: ["status", "sleep", "wake", "direct"],
exactOwnerOnly: true,
ordinaryMessagesRemainFeedbackOnly: true,
localExecutionAuthorized: false,
mindAuthorityGranted: false,
handsAuthorityGranted: false,
releaseAuthorityGranted: false,
deploymentAuthorityGranted: false,
}, null, 2));
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View file

@ -32,6 +32,7 @@ constraints:
- Persistent project state must stay small enough to rehydrate from without becoming a second swamp.
invariants:
- The `/epiphany` Discord edge is an exact-owner request producer only. Its closed status/sleep/wake/direct command family writes short-lived immutable documents to a dedicated Bifrost inbox; ordinary conversation remains feedback-only, and VoidBot gains no Epiphany state, Mind, Hands, release, deployment, argv, or local-execution authority.
- Deployment follows one path: push the intended upstream commit, then let Idunn deploy VoidBot on Yggdrasil. Do not create, restore, or search for a local VoidBot deployment path.
- The map is canonical and slow-changing.
- Scratch is temporary and disposable.