Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions build/story-workflow-buzz-bridge-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type StoryBridgeRequest,
} from "../core/story-workflow/buzz-story-bridge-core.mjs";
import { agentProfileById } from "../lib/agents/agent-profiles";
import { prepareStoryBridgeRequest } from "../modules/story-workflow/buzz-story-bridge";
import { ensurePrivateBuzzAgentMembership } from "./story-workflow/buzz-private-room-membership";

const API = "/api/story-workflow/buzz-bridge";
Expand Down Expand Up @@ -38,6 +39,7 @@ type ResponsibilityRunSnapshot = {
};
};
type LocalResponse<T> = T & { ok?: boolean; message?: string };
type StoryBridgePreparationInput = Parameters<typeof prepareStoryBridgeRequest>[0];

function requestMatchesLocalBridge(request: IncomingMessage, expectedPath: string) {
if (!["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(request.socket.remoteAddress || "")) return false;
Expand Down Expand Up @@ -111,6 +113,15 @@ async function localJson<T>(request: IncomingMessage, route: string, init?: Requ
return value as T;
}

function prepareRequest(body: Record<string, unknown>) {
return prepareStoryBridgeRequest({
project: body.project as StoryBridgePreparationInput["project"],
workItem: body.workItem as StoryBridgePreparationInput["workItem"],
run: body.run as StoryBridgePreparationInput["run"],
contextPacket: body.contextPacket as StoryBridgePreparationInput["contextPacket"],
});
}

function normalizeRequest(value: unknown): StoryBridgeRequest {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Story Bridge requires one bounded request envelope.");
const input = value as StoryBridgeRequest;
Expand Down Expand Up @@ -383,6 +394,11 @@ export function storyWorkflowBuzzBridgeGateway(): Plugin {
}
const body = await readBridgeAction(request);
const action = typeof body.action === "string" ? body.action : "";
if (action === "prepare") {
const bridge = prepareRequest(body);
writeBridgeResponse(response, 200, { ok: true, request: bridge }, bridge.requestId);
return;
}
const bridge = normalizeRequest(body.request);
if (action === "dispatch") {
writeBridgeResponse(response, 200, await dispatch(request, bridge), bridge.requestId);
Expand Down
19 changes: 14 additions & 5 deletions modules/story-workflow/ui/foundations-buzz-story-live-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { CurriculumLesson } from "../../../core/contracts/curriculum";
import type { PPFProject } from "../../../core/project/project";
import type { StoryWorkItem } from "../../../core/story-workflow/story-workflow-core.mjs";
import { loadFoundationProject } from "../../../core/storage/foundation-project-browser";
import { prepareStoryBridgeRequest } from "../buzz-story-bridge";
import { createFoundationsStoryResponsibilityRun } from "../foundations-story-workflow";

type RunResponse = {
Expand All @@ -21,12 +20,20 @@ type BridgeContribution = {
readonly provenance?: { readonly eventId?: string; readonly signatureVerified?: boolean };
};

type PreparedBridgeRequest = Record<string, unknown> & {
readonly requestId?: string;
readonly state?: string;
readonly stateReason?: string;
readonly expectedAgentPubkey?: string;
};

type BridgeResponse = {
readonly ok?: boolean;
readonly message?: string;
readonly state?: string;
readonly executionPath?: string;
readonly requestId?: string;
readonly request?: PreparedBridgeRequest;
readonly idempotent?: boolean;
readonly contributions?: readonly BridgeContribution[];
readonly accepted?: readonly BridgeContribution[];
Expand Down Expand Up @@ -104,14 +111,16 @@ export default function FoundationsBuzzStoryLiveTest({
contextCharacters: task.contextPacket.receipt.usedCharacters,
});

const bridge = prepareStoryBridgeRequest({
const prepared = await bridgeRequest({
action: "prepare",
project,
workItem,
run: task.run,
contextPacket: task.contextPacket,
});
if (bridge.state !== "ready" || !bridge.expectedAgentPubkey) {
throw new Error(bridge.stateReason || "Tamsin does not have a usable local BUZZ signer binding.");
const bridge = prepared.request;
if (!bridge || bridge.state !== "ready" || !bridge.expectedAgentPubkey) {
throw new Error(bridge?.stateReason || "Tamsin does not have a usable local BUZZ signer binding.");
}

onStatus("BUZZ Story Test · adding Tamsin to the private Story Room and dispatching the bounded task…");
Expand Down Expand Up @@ -163,7 +172,7 @@ export default function FoundationsBuzzStoryLiveTest({
action: "proposal-ready",
runId: task.run.runId,
resultId: contribution.result.resultId,
ref: contribution.provenance?.eventId ? `buzz-event:${contribution.provenance.eventId}` : `buzz-story-bridge:${bridge.requestId}`,
ref: contribution.provenance?.eventId ? `buzz-event:${contribution.provenance.eventId}` : `buzz-story-bridge:${bridge.requestId || "unknown"}`,
producedAt: new Date().toISOString(),
});
setState("pass");
Expand Down
8 changes: 7 additions & 1 deletion tests/issue-1422-buzz-story-bridge-hardening.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,15 @@ test("#1422 Story Bridge puts the exact approved BUZZ Agent in the private room
});

test("#1422 exposes a one-click Afterglow/Tamsin live proof without writing the PPF", async () => {
const [panel, liveTest] = await Promise.all([
const [panel, liveTest, gateway] = await Promise.all([
read("modules/story-workflow/ui/foundations-story-workflow-panel.tsx"),
read("modules/story-workflow/ui/foundations-buzz-story-live-test.tsx"),
read("build/story-workflow-buzz-bridge-gateway.ts"),
]);
assert.ok(panel.includes("FoundationsBuzzStoryLiveTest"));
for (const contract of [
"Run BUZZ Story Test",
"action: \"prepare\"",
"action: \"dispatch\"",
"retry.idempotent !== true",
"Waiting for Tamsin Hearthquill’s signed response",
Expand All @@ -176,5 +178,9 @@ test("#1422 exposes a one-click Afterglow/Tamsin live proof without writing the
"BUZZ Story Test PASS",
]) assert.ok(liveTest.includes(contract), `Live Story Bridge UI is missing exit-proof behavior: ${contract}`);

assert.ok(gateway.includes('if (action === "prepare")') && gateway.includes("prepareStoryBridgeRequest"),
"Story Bridge request preparation must execute inside the localhost server gateway.");
assert.doesNotMatch(liveTest, /prepareStoryBridgeRequest|nostr-event-verification|node:crypto/,
"The client live-test component must not import server-only Story Bridge crypto or request preparation.");
assert.doesNotMatch(liveTest, /saveFoundationProject|applyStoryCommand|foundations\.proposal\.store|writing-assistant\/chat/);
});
Loading