diff --git a/.changeset/host-beads-door-priming.md b/.changeset/host-beads-door-priming.md new file mode 100644 index 00000000..15348383 --- /dev/null +++ b/.changeset/host-beads-door-priming.md @@ -0,0 +1,11 @@ +--- +"@bounded-systems/prx": minor +--- + +Host-shell read routing (prx-82b Slice 2e.1): `primeHostBeadsDoor` — on a host +shell in a repo whose pod is up, prx now points `PRX_BEADS_DOOR`/`PRX_BEADS_SOCKET` +at that pod's beadsd socket at startup, so the door-gated bd READ sites +(`bdDoorGate` / `bdCommandRunner`) route to the pod instead of spawning host bd. +No-op in a pod/room profile (the pod already projects these) or when no pod is up +(the host-native daemon stays the fallback until 2e.4). Reuses `podFor` (2a) + +the pod-socket probe (2b); env writes go through `@bounded-systems/env` `setEnv`. diff --git a/packages/prx/src/beadsd/client-factory.ts b/packages/prx/src/beadsd/client-factory.ts index 5b38b06a..0934ec84 100644 --- a/packages/prx/src/beadsd/client-factory.ts +++ b/packages/prx/src/beadsd/client-factory.ts @@ -16,7 +16,7 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; -import { getEnv } from "@bounded-systems/env"; +import { getEnv, setEnv } from "@bounded-systems/env"; import { spawnDetached } from "@bounded-systems/proc"; import { IsolatedBeadsClient } from "./client.ts"; @@ -69,6 +69,33 @@ export function isHostNativeSocket(socket: string): boolean { return socket === DEFAULT_LOCAL_BEADS_SOCKET; } +/** Deps for {@link primeHostBeadsDoor} (injectable for tests). */ +export interface PrimeHostBeadsDoorDeps { + env?: typeof getEnv; + setEnvVar?: typeof setEnv; + /** The cwd's pod beadsd socket if its pod is up, else null (default: probe `podFor`). */ + podSocket?: (() => string | null) | undefined; +} + +/** + * Host-shell read routing (prx-82b Slice 2e.1): when host `prx` runs in a repo + * whose pod is up, point `PRX_BEADS_DOOR`/`PRX_BEADS_SOCKET` at that pod so the + * door-gated bd READ sites route to the pod instead of spawning host bd. No-op + * when already in a profile (the pod projects these into rooms) or when no pod is + * up (the host-native daemon stays the fallback until 2e.4). Returns true iff it + * primed the door. Call once at startup before the door dialer is consulted. + */ +export function primeHostBeadsDoor(deps: PrimeHostBeadsDoorDeps = {}): boolean { + const env = deps.env ?? getEnv; + const set = deps.setEnvVar ?? setEnv; + if (env("PRX_BEADS_DOOR")) return false; // already in a pod/room profile + const socket = (deps.podSocket ?? defaultPodBeadsSocket)(); + if (!socket) return false; // no live pod → host-native fallback + set("PRX_BEADS_DOOR", "beadsd"); + set("PRX_BEADS_SOCKET", socket); + return true; +} + /** Deps for {@link resolveBeadsEndpoint} (injectable for tests). */ export interface ResolveBeadsEndpointDeps { /** The cwd's pod beadsd socket if its pod is up, else null (default: probe `podFor`). */ diff --git a/packages/prx/src/pr-state/cli.ts b/packages/prx/src/pr-state/cli.ts index f3158492..619644ef 100644 --- a/packages/prx/src/pr-state/cli.ts +++ b/packages/prx/src/pr-state/cli.ts @@ -317,7 +317,11 @@ import { runBeadsServe, type BeadsDaemonDeps } from "../beadsd/daemon.ts"; // GH-228: beads workspace self-heal (`prx beads doctor [--fix]`). import { diagnoseBeads, healBeads } from "../beads/doctor.ts"; // GH-296: the host read-door — route `prx beads ready|list|show` through beadsd. -import { withBeadsClient, defaultCanonicalBeadsCwd } from "../beadsd/client-factory.ts"; +import { + withBeadsClient, + defaultCanonicalBeadsCwd, + primeHostBeadsDoor, +} from "../beadsd/client-factory.ts"; import { resolveWorkspaceAffinity, WorkspaceAffinityError, @@ -14772,6 +14776,10 @@ export function runCli( // instead of spawning a local `bd`; off-profile it is never consulted // (prx-asr / prx-634). registerBdDoorDialer(prxBeadsDoorDialer); + // prx-82b Slice 2e.1: on a host shell in a repo whose pod is up, point the bd + // door at that pod so the door-gated read sites route to it instead of host + // bd. No-op in a pod/room profile or when no pod is up (host-native fallback). + primeHostBeadsDoor(); try { // Spec-driven verbs (pilot/fleet/health) are handled by the canonical // VerbSpec dispatch, ahead of the legacy typed-command union/executor diff --git a/packages/prx/test/beadsd/client-factory.test.ts b/packages/prx/test/beadsd/client-factory.test.ts index a20ad157..70ab6d4e 100644 --- a/packages/prx/test/beadsd/client-factory.test.ts +++ b/packages/prx/test/beadsd/client-factory.test.ts @@ -9,6 +9,7 @@ import { resolveLocalBeadsCwd, withBeadsClient, isHostNativeSocket, + primeHostBeadsDoor, DEFAULT_LOCAL_BEADS_SOCKET, } from "../../src/beadsd/client-factory.ts"; @@ -201,3 +202,39 @@ describe("ensureLocalBeadsd", () => { ).rejects.toThrow(BeadsUnavailableError); }); }); + +describe("primeHostBeadsDoor — host-shell read routing (prx-82b 2e.1)", () => { + function harness(overrides: { door?: string; podSocket?: string | null }) { + const set: Record = {}; + const env = (k: string) => (k === "PRX_BEADS_DOOR" ? overrides.door : undefined); + const result = primeHostBeadsDoor({ + env: env as never, + setEnvVar: ((k: string, v: string) => { + set[k] = v; + }) as never, + podSocket: () => overrides.podSocket ?? null, + }); + return { result, set }; + } + + test("primes the door to the cwd's pod socket when a pod is up", () => { + const { result, set } = harness({ podSocket: "/run/prx/doors/slug/beadsd.sock" }); + expect(result).toBe(true); + expect(set).toEqual({ + PRX_BEADS_DOOR: "beadsd", + PRX_BEADS_SOCKET: "/run/prx/doors/slug/beadsd.sock", + }); + }); + + test("no-op when no pod is up (host-native fallback stays)", () => { + const { result, set } = harness({ podSocket: null }); + expect(result).toBe(false); + expect(set).toEqual({}); + }); + + test("no-op when already in a pod/room profile (PRX_BEADS_DOOR set)", () => { + const { result, set } = harness({ door: "beadsd", podSocket: "/run/prx/doors/x/beadsd.sock" }); + expect(result).toBe(false); + expect(set).toEqual({}); + }); +});