diff --git a/.changeset/keeper-door-guest-room-model.md b/.changeset/keeper-door-guest-room-model.md new file mode 100644 index 00000000..6e6132d2 --- /dev/null +++ b/.changeset/keeper-door-guest-room-model.md @@ -0,0 +1,10 @@ +--- +--- + +A1 of the prx→guest-room convergence: add `@bounded-systems/guest-room` as a prx +dependency (github dep until it publishes to JSR) and describe the keeper door +through guest-room's capability model — a `DoorCatalog` preset + a rulebook +rendered by guest-room's `resolveDoor`/`grantedDoorLines`/`deniedDoorSection`. +First real dependency edge from prx onto the flagship runtime. Additive: the +keeperd transport is unchanged; the keeper endpoint env stays sourced from +`keeperd/endpoint.ts`. No public API or behavior change, no release. diff --git a/bun.lock b/bun.lock index a9bccf02..a4fd7800 100644 --- a/bun.lock +++ b/bun.lock @@ -45,6 +45,7 @@ "@bounded-systems/gh": "npm:@jsr/bounded-systems__gh@^0.1.0", "@bounded-systems/git": "npm:@jsr/bounded-systems__git@^0.1.0", "@bounded-systems/github-budget": "npm:@jsr/bounded-systems__github-budget@^0.1.0", + "@bounded-systems/guest-room": "npm:@jsr/bounded-systems__guest-room@^0.1.0", "@bounded-systems/host": "npm:@jsr/bounded-systems__host@^0.2.0", "@bounded-systems/machine-schema": "npm:@jsr/bounded-systems__machine-schema@^0.2.0", "@bounded-systems/policy": "npm:@jsr/bounded-systems__policy@^0.2.0", @@ -129,6 +130,8 @@ "@bounded-systems/github-budget": ["@jsr/bounded-systems__github-budget@0.1.0", "https://npm.jsr.io/~/11/@jsr/bounded-systems__github-budget/0.1.0.tgz", { "dependencies": { "@jsr/bounded-systems__audit-context": "^0.2.0", "@jsr/bounded-systems__env": "^0.2.0", "@jsr/bounded-systems__proc": "^0.2.0", "zod": "^4.4.3" } }, "sha512-7lBN1nN6V3AGuBwF0TjepWklCvPX4FD7w1XJezhZaP7k1h6nGKB1xheu8RWPCWPGfV8DmePWpN2L8Znaugcm2w=="], + "@bounded-systems/guest-room": ["@jsr/bounded-systems__guest-room@0.1.0", "https://npm.jsr.io/~/11/@jsr/bounded-systems__guest-room/0.1.0.tgz", {}, "sha512-B2x+wyCyJT9ZNLmN64OS9SL81eqsFPmnWmL0A54Ic++X6GYqbwVdvWdHiE4lI8Bm3kP0mirLpIiP67ICkgaGOg=="], + "@bounded-systems/host": ["@jsr/bounded-systems__host@0.2.0", "https://npm.jsr.io/~/11/@jsr/bounded-systems__host/0.2.0.tgz", { "dependencies": { "@jsr/bounded-systems__env": "^0.2.0" } }, "sha512-N5uc3Du3jzmlgX3tP0gnIZwOYXGOYYiU+wTBpiG6iYpfPG9WVoFWWoZkcb79QH93AaERnJUhrw2yKSq13VJEAg=="], "@bounded-systems/machine-schema": ["@jsr/bounded-systems__machine-schema@0.2.0", "https://npm.jsr.io/~/11/@jsr/bounded-systems__machine-schema/0.2.0.tgz", { "dependencies": { "zod": "^4.4.3" } }, "sha512-Ttvrh+Gj+IWkdq0fO6Db1OVuvMB5nRUOgKuvEUX7GfJK8AAU/dvsu9bq8sZCk67XGZl1diQ6BrflIgA5NxzqJg=="], diff --git a/packages/prx/package.json b/packages/prx/package.json index 5a7d1d6d..bf267b5e 100644 --- a/packages/prx/package.json +++ b/packages/prx/package.json @@ -25,6 +25,7 @@ "@bounded-systems/gh": "npm:@jsr/bounded-systems__gh@^0.1.0", "@bounded-systems/git": "npm:@jsr/bounded-systems__git@^0.1.0", "@bounded-systems/github-budget": "npm:@jsr/bounded-systems__github-budget@^0.1.0", + "@bounded-systems/guest-room": "npm:@jsr/bounded-systems__guest-room@^0.1.0", "@bounded-systems/host": "npm:@jsr/bounded-systems__host@^0.2.0", "@bounded-systems/machine-schema": "npm:@jsr/bounded-systems__machine-schema@^0.2.0", "@bounded-systems/policy": "npm:@jsr/bounded-systems__policy@^0.2.0", diff --git a/packages/prx/src/door/guest-room-catalog.ts b/packages/prx/src/door/guest-room-catalog.ts new file mode 100644 index 00000000..1856eb32 --- /dev/null +++ b/packages/prx/src/door/guest-room-catalog.ts @@ -0,0 +1,66 @@ +// A1 of the prx→guest-room convergence (see .github-private +// docs/convergence-prx-claude-box.md): describe prx's keeper door using the +// published @bounded-systems/guest-room capability model instead of an ad-hoc +// local description. This is the first real dependency edge from prx onto +// guest-room — the same model that powers claude-box now describes prx's door. +// +// Scope: ADDITIVE. It does not change the keeperd transport (still the bespoke +// client in ../keeperd/); it makes the door's *definition + rulebook* come from +// the guest-room contract. The endpoint env stays sourced from +// ../keeperd/endpoint.ts, so there is still one source of truth for the socket. + +import { + capabilityPreamble, + deniedDoors, + type DoorCatalog, + type DoorGrant, + deniedDoorSection, + type Env, + grantedDoorLines, + resolveDoor, +} from "@bounded-systems/guest-room"; +import { processEnv } from "@bounded-systems/env"; + +import { DEFAULT_LOCAL_KEEPER_SOCKET } from "../keeperd/endpoint.ts"; + +/** + * prx's door catalog, expressed in the guest-room model. Today it is the keeper + * door (signed git-writes via keeperd); the other prx doors (beads, …) join here + * as they move onto the runtime. + */ +export const prxDoorCatalog: DoorCatalog = { + keeper: { + flag: "--keeper", + inBox: "/run/prx/doors/keeperd.sock", + env: "PRX_KEEPER_SOCKET", + hostDefault: DEFAULT_LOCAL_KEEPER_SOCKET, + grants: "signed git writes via keeperd", + use: "Route every git write through the keeper door; keeperd imports the commit and performs the signed push.", + deny: "No git-write authority in this room — relaunch with the keeper door.", + }, +}; + +/** Resolve prx's keeper door to a concrete grant via the guest-room engine. */ +export function keeperDoorGrant(env: Env = processEnv()): DoorGrant { + return resolveDoor(prxDoorCatalog, "keeper", undefined, env); +} + +/** + * Render the honest rulebook for a prx room over the given granted doors, using + * guest-room's renderer: the capability preamble, a card per granted door, and + * the denied section — so the surface is honest about what is absent, not only + * what is present. + */ +export function renderPrxRulebook( + workcell: string, + grantedDoorNames: readonly string[], + env: Env = processEnv(), +): string { + const granted = grantedDoorNames.map((n) => resolveDoor(prxDoorCatalog, n, undefined, env)); + const denied = deniedDoors(prxDoorCatalog, new Set(grantedDoorNames)); + return [ + ...capabilityPreamble(workcell), + ...grantedDoorLines(granted), + ...deniedDoorSection(denied), + ].join("\n"); +} diff --git a/packages/prx/test/door/guest-room-catalog.test.ts b/packages/prx/test/door/guest-room-catalog.test.ts new file mode 100644 index 00000000..bdccb3a1 --- /dev/null +++ b/packages/prx/test/door/guest-room-catalog.test.ts @@ -0,0 +1,28 @@ +// A1: prx describes its keeper door through the guest-room capability model. +import { describe, expect, test } from "bun:test"; + +import { + keeperDoorGrant, + prxDoorCatalog, + renderPrxRulebook, +} from "../../src/door/guest-room-catalog.ts"; + +describe("prx keeper door via the guest-room model", () => { + test("the catalog defines the keeper door against the keeperd endpoint env", () => { + expect(prxDoorCatalog.keeper).toBeDefined(); + expect(prxDoorCatalog.keeper!.env).toBe("PRX_KEEPER_SOCKET"); + }); + + test("resolveDoor maps the preset into a concrete grant", () => { + const grant = keeperDoorGrant({ PRX_KEEPER_SOCKET: "/run/prx/doors/keeperd.sock" }); + expect(grant.name).toBe("keeper"); + expect(grant.env).toBe("PRX_KEEPER_SOCKET"); + expect(grant.grants).toContain("signed git writes"); + }); + + test("the rulebook is honest — a card for keeper, and a DENIED section", () => { + const book = renderPrxRulebook("prx", ["keeper"], { PRX_KEEPER_SOCKET: "/x.sock" }); + expect(book).toContain("keeper: signed git writes"); + expect(book).toContain("DENIED"); + }); +});