Skip to content

Commit 1f86048

Browse files
feat(glass): add plain-language policy room as the ninth basement room
The stable `policy` room completes the design-required Basement. It presents owner-language autonomy truth - exact owner directions are never policed, dangerous work gets exactly one consequence confirmation, and policy governs derived/unattended work - with quotas labeled as technical execution limits, never money. - policyActions.ts reproduces the gateway's serde body digest and strict canonical safe-snapshot digest byte for byte for the owner-proof binding. - The versioned client gains an additive requestId option so the policy PUT sends the bound correlation id as x-request-id. - The one Office stream authority exposes policyInvalidationGeneration; no second websocket, cursor loop, or event store. - useExecassPolicyController: identity-bound generation-safe reads, one synchronous apply lock, complete-ruleset PUT at the authoritative revision, success only after the authoritative response, retained drafts on failure and conflict, conflict refetch, and the update's own policy.changed echo is never mistaken for a stale identity. - PolicyRoomPage: profile cards, advanced rules, one review step with the concrete consequence then one confirmation, honest loading/error/ unconfigured states, and the policy pin only on the ready room. - Policy shares the connectors route like Setup: always available, rendered before every connector gate, never redirected to Setup. - Hidden fourteenth `policy` shortcut with a 24/24 capacity fixture (thirteen earlier placements, one explicitly hidden, byte-identical refusal, admit-after-free) and config-only pinning. - Stateful mock policy GET/PUT enforces the production binding identity, records exact requests, injects 503/409, and emits policy.changed; the mock resume handler now honestly refetches compacted-history cursors so live events apply instead of gapping forever. - Desktop + 390px Playwright proof with exact error accounting. Validation: typecheck, lint, unit 755/755, build, p5-policy e2e, all P5 anchors, composed core 45/45, contract validator, git diff --check PASS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f6b874f commit 1f86048

21 files changed

Lines changed: 3615 additions & 9 deletions

apps/mission-control/e2e/mockGateway.mjs

Lines changed: 366 additions & 2 deletions
Large diffs are not rendered by default.

apps/mission-control/e2e/p5-policy-slice.spec.ts

Lines changed: 774 additions & 0 deletions
Large diffs are not rendered by default.

apps/mission-control/src/App.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { SimpleIntegrationWizard } from "./features/connectors/SimpleIntegration
3232
import type { SimpleIntegrationId } from "./features/connectors/simpleIntegrations";
3333
import { useConnectorsController } from "./features/connectors/useConnectorsController";
3434
import { useExecassOfficeController } from "./features/execassOffice/useExecassOfficeController";
35+
import { useExecassPolicyController } from "./features/execassPolicy/useExecassPolicyController";
3536
import type { SetupSurfaceProps } from "./features/setup/SetupControls";
3637
import { useGlassWindowController } from "./features/glassWindow/useGlassWindowController";
3738
import { findRoom, roomForTab } from "./glass/floors";
@@ -440,10 +441,11 @@ export default function App() {
440441
],
441442
[connectorsHubEnabled, memoryHubEnabled, runbookHubEnabled]
442443
);
443-
// Setup shares the Connectors render route, but it is the authority used to
444-
// turn Connectors back on. Keep that stable room independently available
445-
// when the optional Connectors product surface is disabled.
446-
const alwaysAvailableElevatorRooms = useMemo(() => ["setup"], []);
444+
// Setup and Policy share the Connectors render route, but Setup is the
445+
// authority used to turn Connectors back on and Policy is the owner's
446+
// autonomy surface. Keep both stable rooms independently available when
447+
// the optional Connectors product surface is disabled.
448+
const alwaysAvailableElevatorRooms = useMemo(() => ["setup", "policy"], []);
447449
const elevatorFloors = useResolvedElevator(
448450
availableTabs,
449451
alwaysAvailableElevatorRooms,
@@ -684,6 +686,15 @@ export default function App() {
684686
active: activeTab === "assistant",
685687
setNotice,
686688
});
689+
// The one App-owned policy controller. Its invalidation signal comes from
690+
// the Office controller's durable stream - never a second websocket.
691+
const policyController = useExecassPolicyController({
692+
settings,
693+
tokenConfigured,
694+
active: activeTab === "connectors" && resolvedActiveRoomId === "policy",
695+
policyInvalidationGeneration: officeController.policyInvalidationGeneration,
696+
setNotice,
697+
});
687698
const glassWindowController = useGlassWindowController({
688699
settings,
689700
tokenConfigured,
@@ -903,7 +914,8 @@ export default function App() {
903914
if (
904915
activeTab === "connectors" &&
905916
!connectorsHubEnabled &&
906-
activeRoomId !== "setup"
917+
activeRoomId !== "setup" &&
918+
activeRoomId !== "policy"
907919
) {
908920
selectRoom("setup", elevatorFloors);
909921
setNotice({
@@ -1343,6 +1355,7 @@ export default function App() {
13431355
setNotice={setNotice}
13441356
usageChartsEnabled={usageChartsEnabled}
13451357
setupSurface={setupSurface}
1358+
policyController={policyController}
13461359
onOpenSimpleIntegrationWizard={openSimpleIntegrationWizard}
13471360
quickGuidesCollapsed={quickGuideState.collapsed}
13481361
quickGuideOpenTab={quickGuideState.openTab}

apps/mission-control/src/app/AppContent.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { type CockpitWidgetLayoutV2 } from "../features/cockpit/cockpitLayout";
2525
import { CockpitWidgetRenderer } from "../features/cockpit/CockpitWidgetRenderer";
2626
import { ConnectorsPage } from "../features/connectors/ConnectorsPage";
2727
import type { SetupSurfaceProps } from "../features/setup/SetupControls";
28+
import type { ExecassPolicyController } from "../features/execassPolicy/useExecassPolicyController";
2829
import type { SimpleIntegrationId } from "../features/connectors/simpleIntegrations";
2930
import { useConnectorsController } from "../features/connectors/useConnectorsController";
3031
import { EventsPage } from "../features/events/EventsPage";
@@ -98,6 +99,8 @@ interface AppContentProps {
9899
usageChartsEnabled: boolean;
99100
/** The one shared Setup authority; the Basement Setup room renders it. */
100101
setupSurface: SetupSurfaceProps;
102+
/** The one App-owned policy controller; the Basement Policy room renders it. */
103+
policyController: ExecassPolicyController;
101104
onOpenSimpleIntegrationWizard: (integrationId?: SimpleIntegrationId) => void;
102105
quickGuidesCollapsed: boolean;
103106
quickGuideOpenTab: HelpTab | null;
@@ -1045,6 +1048,7 @@ export function AppContent(props: AppContentProps) {
10451048
onOpenSimpleIntegrationWizard={props.onOpenSimpleIntegrationWizard}
10461049
activeRoomId={props.activeRoomId}
10471050
setupSurface={props.setupSurface}
1051+
policyController={props.policyController}
10481052
/>
10491053
</TabBoundaryPane>
10501054

apps/mission-control/src/features/connectors/ConnectorsPage.test.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
66

77
import { ConnectorsPage } from "./ConnectorsPage";
88
import type { useConnectorsController } from "./useConnectorsController";
9+
import { fixturePolicyResponse } from "../../glass/execass/fixtures";
910
import { DEFAULT_OPSUX_RUNTIME_CONFIG } from "../../lib/opsUxConfig";
11+
import type { ExecassPolicyController } from "../execassPolicy/useExecassPolicyController";
1012
import type { SetupSurfaceProps } from "../setup/SetupControls";
1113

1214
vi.mock("../../lib/api", () => ({
@@ -124,10 +126,32 @@ function stubSetupSurface(): SetupSurfaceProps {
124126
};
125127
}
126128

129+
function stubPolicyController(
130+
overrides: Partial<ExecassPolicyController> = {},
131+
): ExecassPolicyController {
132+
return {
133+
phase: "loaded",
134+
policy: fixturePolicyResponse(),
135+
error: null,
136+
draft: null,
137+
conflict: false,
138+
updateBusy: false,
139+
refresh: vi.fn(async () => {}),
140+
beginDraft: vi.fn(),
141+
setDraftProfile: vi.fn(),
142+
setDraftRule: vi.fn(),
143+
setDraftChangeSummary: vi.fn(),
144+
discardDraft: vi.fn(),
145+
applyDraft: vi.fn(async () => ({ ok: true as const, message: "ok" })),
146+
...overrides,
147+
};
148+
}
149+
127150
async function render(
128151
controller: ConnectorsController,
129152
activeRoomId: string | null,
130153
setupSurface: SetupSurfaceProps = stubSetupSurface(),
154+
policyController: ExecassPolicyController = stubPolicyController(),
131155
) {
132156
await act(async () => {
133157
if (!root) {
@@ -139,6 +163,7 @@ async function render(
139163
onOpenSimpleIntegrationWizard={() => {}}
140164
activeRoomId={activeRoomId}
141165
setupSurface={setupSurface}
166+
policyController={policyController}
142167
/>,
143168
);
144169
});
@@ -204,6 +229,75 @@ describe("ConnectorsPage Basement room seam", () => {
204229
},
205230
);
206231

232+
it("lands the Policy room on its distinct owner-language surface", async () => {
233+
await render(stubController(), "policy");
234+
expect(
235+
container.querySelector('[data-testid="policy-room-page"]'),
236+
).toBeTruthy();
237+
expect(container.textContent).toContain("The deal");
238+
expect(container.textContent).toContain("Autonomy profile");
239+
// The connector tab strip belongs to the Connectors room only.
240+
expect(container.querySelector(".mc-connectors-tab-bar")).toBeNull();
241+
expect(container.textContent).not.toContain("Quick Setup");
242+
});
243+
244+
it.each([
245+
["disabled", { enabled: false, availability: "disabled" }],
246+
["unsupported", { enabled: true, availability: "unsupported" }],
247+
["error", { enabled: true, availability: "error" }],
248+
["cold loading", { enabled: true, availability: "loading" }],
249+
] as const)(
250+
"keeps Policy available through a %s Connectors controller",
251+
async (_label, override) => {
252+
const controller = stubController({
253+
...override,
254+
installedConnectors: [],
255+
});
256+
await render(controller, "policy");
257+
258+
expect(
259+
container.querySelector('[data-testid="policy-room-page"]'),
260+
).not.toBeNull();
261+
expect(container.textContent).toContain("Autonomy profile");
262+
expect(container.textContent).not.toContain("Connectors are disabled");
263+
expect(container.textContent).not.toContain("Connectors surface unavailable");
264+
expect(container.textContent).not.toContain("Connectors failed to load");
265+
expect(container.textContent).not.toContain("Loading Connectors");
266+
},
267+
);
268+
269+
it("keeps the user's connector tab across a Policy room visit and relands exactly", async () => {
270+
const controller = stubController();
271+
await render(controller, "connectors");
272+
await act(async () => tabButton("Catalog")!.click());
273+
expect(tabButton("Catalog")?.className).toContain("active");
274+
275+
await render(controller, "policy");
276+
expect(
277+
container.querySelector('[data-testid="policy-room-page"]'),
278+
).toBeTruthy();
279+
280+
await render(controller, "connectors");
281+
expect(tabButton("Registry")?.className).toContain("active");
282+
});
283+
284+
it("offers the Policy pin only under the Policy room identity", async () => {
285+
const controller = stubController();
286+
await render(controller, "policy");
287+
const policyPin = Array.from(container.querySelectorAll("button")).find(
288+
(button) => button.getAttribute("aria-label") === "Pin Policy to Office",
289+
);
290+
expect(policyPin).toBeTruthy();
291+
292+
await render(controller, "connectors");
293+
const stalePolicyPin = Array.from(
294+
container.querySelectorAll("button"),
295+
).find(
296+
(button) => button.getAttribute("aria-label") === "Pin Policy to Office",
297+
);
298+
expect(stalePolicyPin).toBeUndefined();
299+
});
300+
207301
it("keeps connector quick setup reachable inside the Connectors room", async () => {
208302
await render(stubController(), "connectors");
209303
await act(async () => tabButton("Setup")!.click());

apps/mission-control/src/features/connectors/ConnectorsPage.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import {
4848
} from "./connectorsConfig";
4949
import "./connectors.css";
5050
import { PinRoomToOffice } from "../execassOffice/PinRoomToOffice";
51+
import { PolicyRoomPage } from "../execassPolicy/PolicyRoomPage";
52+
import type { ExecassPolicyController } from "../execassPolicy/useExecassPolicyController";
5153
import { SetupRoomPage } from "../setup/SetupRoomPage";
5254
import type { SetupSurfaceProps } from "../setup/SetupControls";
5355
import { stringifyJson } from "./connectorsModel";
@@ -75,6 +77,12 @@ interface ConnectorsPageProps {
7577
* Setup room renders it directly; no second store or draft exists here.
7678
*/
7779
setupSurface: SetupSurfaceProps;
80+
/**
81+
* The one App-owned ExecAss policy controller. The Policy room renders
82+
* it directly; like Setup it shares this mounted route only for the pane
83+
* lifecycle and is never gated by connector availability.
84+
*/
85+
policyController: ExecassPolicyController;
7886
}
7987

8088
function humanize(value: string | null | undefined): string {
@@ -254,6 +262,7 @@ export function ConnectorsPage({
254262
onOpenSimpleIntegrationWizard,
255263
activeRoomId,
256264
setupSurface,
265+
policyController,
257266
}: ConnectorsPageProps) {
258267
const [activeTab, setActiveTab] = useState<ConnectorsTab>(
259268
() => (activeRoomId ? ROOM_LANDING_TAB[activeRoomId] : undefined) ?? "setup",
@@ -493,6 +502,12 @@ export function ConnectorsPage({
493502
return <SetupRoomPage surface={setupSurface} />;
494503
}
495504

505+
// Policy is the owner's autonomy surface. Like Setup it owns its own
506+
// availability and renders before every connector-specific gate.
507+
if (activeRoomId === "policy") {
508+
return <PolicyRoomPage controller={policyController} />;
509+
}
510+
496511
if (!controller.enabled || controller.availability === "disabled") {
497512
return (
498513
<ConnectorsStatePanel

apps/mission-control/src/features/execassOffice/officeBlocks.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,12 @@ export const OFFICE_BLOCK_REGISTRY: readonly OfficeBlockDef[] = [
158158
defaultSize: "s",
159159
defaultVisible: false,
160160
},
161+
{
162+
id: "policy",
163+
rendererKey: "room-shortcut",
164+
roomId: "policy",
165+
title: "Policy",
166+
defaultSize: "s",
167+
defaultVisible: false,
168+
},
161169
];

apps/mission-control/src/features/execassOffice/pinToOffice.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ describe("OFFICE_BLOCK_REGISTRY room shortcuts", () => {
127127
expect(setup?.defaultSize).toBe("s");
128128
expect(setup?.defaultVisible).toBe(false);
129129
});
130+
131+
test("registers the Policy room shortcut hidden by default", () => {
132+
const policy = OFFICE_BLOCK_REGISTRY.find((def) => def.id === "policy");
133+
expect(policy).toBeDefined();
134+
expect(policy?.rendererKey).toBe("room-shortcut");
135+
expect(policy?.roomId).toBe("policy");
136+
expect(policy?.defaultSize).toBe("s");
137+
expect(policy?.defaultVisible).toBe(false);
138+
});
130139
});
131140

132141
describe("pinRoomBlocksToOffice", () => {
@@ -885,6 +894,114 @@ describe("pinRoomBlocksToOffice", () => {
885894
).toHaveLength(1);
886895
});
887896

897+
test("the fourteenth shortcut refuses byte-for-byte against thirteen earlier placements with twelve visible", () => {
898+
// Thirteen earlier small shortcuts cannot all be visible beside a full
899+
// 6x4 canvas, so the honest capacity fixture holds every placement
900+
// through Setup while exactly one earlier shortcut is explicitly hidden:
901+
// twelve visible s blocks fill 24/24 cells before Policy asks for a slot.
902+
localStorage.setItem(
903+
GLASS_CONFIG_STORAGE_KEY,
904+
JSON.stringify({
905+
themeId: "auto",
906+
customThemes: [],
907+
layout: [
908+
{ id: "needs-you", size: "l", visible: false },
909+
{ id: "in-motion", size: "m", visible: false },
910+
{ id: "done", size: "m", visible: false },
911+
{ id: "next", size: "s", visible: false },
912+
],
913+
}),
914+
);
915+
const roomsThroughLocks = [
916+
"boards",
917+
"calendar",
918+
"plan",
919+
"staff",
920+
"history",
921+
"connectors",
922+
"models",
923+
"events",
924+
"breakers",
925+
"directory",
926+
"memory",
927+
"locks",
928+
];
929+
for (const roomId of roomsThroughLocks) {
930+
expect(pinRoomBlocksToOffice(roomId).ok).toBe(true);
931+
}
932+
// Explicitly hide one earlier placement so Setup - the thirteenth - can
933+
// genuinely join the persisted layout.
934+
const withTwelve = JSON.parse(
935+
localStorage.getItem(GLASS_CONFIG_STORAGE_KEY) ?? "{}",
936+
) as { layout?: { id: string; size: string; visible: boolean }[] };
937+
withTwelve.layout = (withTwelve.layout ?? []).map((entry) =>
938+
entry.id === "boards" ? { ...entry, visible: false } : entry,
939+
);
940+
localStorage.setItem(GLASS_CONFIG_STORAGE_KEY, JSON.stringify(withTwelve));
941+
expect(pinRoomBlocksToOffice("setup").ok).toBe(true);
942+
943+
// Prove the precondition: all thirteen earlier placements exist, exactly
944+
// one is hidden, and twelve visible s shortcuts occupy 24/24 cells.
945+
const layout = loadGlassConfig().layout ?? [];
946+
const earlierBlocks = [
947+
"boards",
948+
"calendar",
949+
"strategy",
950+
"staff",
951+
"history",
952+
"connectors",
953+
"models",
954+
"events",
955+
"breakers",
956+
"directory",
957+
"memory",
958+
"locks",
959+
"setup",
960+
];
961+
for (const id of earlierBlocks) {
962+
expect(layout.some((entry) => entry.id === id)).toBe(true);
963+
}
964+
expect(layout.find((entry) => entry.id === "boards")?.visible).toBe(false);
965+
expect(
966+
layout.filter((entry) => entry.visible && entry.size === "s"),
967+
).toHaveLength(12);
968+
969+
const before = localStorage.getItem(GLASS_CONFIG_STORAGE_KEY);
970+
const result = pinRoomBlocksToOffice("policy");
971+
expect(result.ok).toBe(false);
972+
expect(result.error).toMatch(/exceed/i);
973+
expect(localStorage.getItem(GLASS_CONFIG_STORAGE_KEY)).toBe(before);
974+
expect(
975+
(loadGlassConfig().layout ?? []).find((entry) => entry.id === "policy")
976+
?.visible,
977+
).not.toBe(true);
978+
979+
// Freeing one visible shortcut admits Policy, and repeating is honest.
980+
const stored = JSON.parse(before ?? "{}") as {
981+
layout?: { id: string; size: string; visible: boolean }[];
982+
};
983+
stored.layout = (stored.layout ?? []).map((entry) =>
984+
entry.id === "calendar" ? { ...entry, visible: false } : entry,
985+
);
986+
localStorage.setItem(GLASS_CONFIG_STORAGE_KEY, JSON.stringify(stored));
987+
const admitted = pinRoomBlocksToOffice("policy");
988+
expect(admitted.ok).toBe(true);
989+
expect(admitted.pinned).toEqual(["policy"]);
990+
expect(
991+
(loadGlassConfig().layout ?? []).find((entry) => entry.id === "policy")
992+
?.visible,
993+
).toBe(true);
994+
995+
const beforeRepeat = localStorage.getItem(GLASS_CONFIG_STORAGE_KEY);
996+
const again = pinRoomBlocksToOffice("policy");
997+
expect(again.ok).toBe(true);
998+
expect(again.pinned).toEqual([]);
999+
expect(localStorage.getItem(GLASS_CONFIG_STORAGE_KEY)).toBe(beforeRepeat);
1000+
expect(
1001+
(loadGlassConfig().layout ?? []).filter((entry) => entry.id === "policy"),
1002+
).toHaveLength(1);
1003+
});
1004+
8881005
test("fails honestly for rooms with no registered Office block yet", () => {
8891006
const result = pinRoomBlocksToOffice("reef");
8901007
expect(result.ok).toBe(false);

0 commit comments

Comments
 (0)