Skip to content

Commit 0c97b1f

Browse files
SafetyMPSage Hart
andauthored
feat: complete unfinished autonomous-company r3 depth (#45)
Wire orchestrator-driven company day, G1–G6 firm governance, live provider path, TTL/Bearer/SSE, elicitation, and enforcement modes after approved corpos-autonomous-company-r3 handoff. Co-authored-by: Sage Hart <sagehart@MacBookAir.attlocal.net>
1 parent 4529f86 commit 0c97b1f

30 files changed

Lines changed: 1365 additions & 185 deletions

.corp-harness/site.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"schema": "corporate-site-site/v1",
33
"site_id": "corpos",
4-
"corporate_program": "corpos-autonomous-company-r2",
5-
"corporate_handoff_sha256": "15052d5905e7e73687465dbf6d1d801d31105a7c7abfd33ea50ceb2652dcbeb6",
4+
"corporate_program": "corpos-autonomous-company-r3",
5+
"corporate_handoff_sha256": "ae1ffdc303c5a4589a210113d268551c2a6327b6841eb85eeef7bc7a6e6a717d",
66
"verify_argv": ["./scripts/harness/verify.sh"],
77
"adversarial_argv": ["./scripts/harness/adversarial.sh"]
88
}

CONTEXT.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ Workspaces: `packages/core`, `packages/mcp-knowledge`, `apps/api`, `apps/console
88
Commands: `npm run dev`, `npm test`, `npm run scenario`, `npm run audit:verify`,
99
`./scripts/harness/verify.sh`, `./scripts/harness/adversarial.sh`.
1010

11-
Site id: `corpos`. Program: `corpos-autonomous-company-r2`.
12-
Live LLM requires `CORPOS_ALLOW_LIVE=1` and `OPENROUTER_API_KEY`.
11+
Site id: `corpos`. Program: `corpos-autonomous-company-r3`.
12+
Live LLM requires `CORPOS_ALLOW_LIVE=1` and `OPENROUTER_API_KEY` (company-day uses HttpLLMProvider when live).
13+
G1–G6 firm governance coded; TTL scheduler; console Bearer; orchestrator-driven day; live SSE.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ CorpOS shows how a firm operates when department agents do most work and **human
1212

1313
> **Scope:** Reference architecture and runnable demo — **not** a production-hardened SaaS. See [SECURITY.md](SECURITY.md).
1414
15-
Default mode is **simulation** (`SimulationProvider`) for deterministic CI. Live LLM (`HttpLLMProvider`) only when `CORPOS_ALLOW_LIVE=1` and `OPENROUTER_API_KEY` are set — `/api/health.mode` never lies.
15+
Default mode is **simulation** (`SimulationProvider`) for deterministic CI. Live LLM (`HttpLLMProvider`) drives company-day/orchestrator only when `CORPOS_ALLOW_LIVE=1` and `OPENROUTER_API_KEY` are set — `/api/health.mode` never lies. G1–G6 firm governance, orchestrator-driven day, TTL scheduler, console Bearer (shared mode), and live `/api/events` SSE are implemented.
1616

1717
Read the thesis: [`docs/future-of-the-firm.md`](docs/future-of-the-firm.md). Governance crosswalk: [`docs/governance-crosswalk.md`](docs/governance-crosswalk.md). AIBOM: [`docs/aibom.json`](docs/aibom.json).
1818

SECURITY.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ Treat it as a design artifact you can run locally.
99

1010
- ToolGateway is the sole chokepoint for consequential tools.
1111
- Unknown tools fail closed.
12-
- Exception HITL with TTL; kill switch; department capital caps.
12+
- Exception HITL with scheduled TTL fail-closed; kill switch; department capital caps.
13+
- Shared-mode console sends Bearer (`VITE_DASHBOARD_API_TOKEN` / `DASHBOARD_API_TOKEN`).
14+
- G3 quorum, G6 appeal, and enforcement `strict`/`audit` modes.
1315
- Hash-chained audit receipts (`npm run audit:verify`).
1416

1517
## Not provided

apps/api/src/app.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,25 @@ import { cors } from "hono/cors";
33
import { streamSSE } from "hono/streaming";
44
import {
55
agents,
6+
appealException,
67
contracts,
78
controlState,
89
counterfactualReplay,
910
createCompany,
1011
decideException,
12+
deliberationEntries,
1113
departments,
1214
exceptions,
15+
expireExceptionTtl,
1316
listSpans,
1417
resolveProvider,
1518
runCompanyDay,
19+
subscribeFirmEvents,
20+
recentFirmEvents,
21+
transparencyRecords,
1622
traces,
1723
type Company,
24+
type EnforcementMode,
1825
} from "@corpos/core";
1926
import { eq } from "drizzle-orm";
2027
import fs from "node:fs";
@@ -45,12 +52,18 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
4552
const app = new Hono();
4653
app.use("*", cors());
4754

55+
const envMode = process.env.CORPOS_ENFORCEMENT;
56+
if (envMode === "strict" || envMode === "audit") {
57+
company.policy.setEnforcementMode(envMode);
58+
}
59+
4860
app.get("/api/health", (c) =>
4961
c.json({
5062
ok: true,
5163
mode,
5264
product: "autonomous-company-reference",
5365
provider: mode === "live" ? "HttpLLMProvider" : "SimulationProvider",
66+
enforcement: company.policy.getEnforcementMode(),
5467
}),
5568
);
5669

@@ -73,8 +86,17 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
7386
c.json((await company.db.select().from(exceptions)).filter((e) => e.state === "pending")),
7487
);
7588

89+
app.get("/api/exceptions/:id/deliberation", async (c) => {
90+
const rows = await company.db
91+
.select()
92+
.from(deliberationEntries)
93+
.where(eq(deliberationEntries.exceptionId, c.req.param("id")));
94+
return c.json(rows);
95+
});
96+
7697
app.post("/api/exceptions/:id/decide", async (c) => {
7798
if (!requireAuth(c)) return c.json({ error: "dashboard authentication required" }, 401);
99+
await expireExceptionTtl(company);
78100
const body = await c.req.json<{
79101
decision: "approved" | "rejected";
80102
by?: string;
@@ -90,6 +112,19 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
90112
return c.json({ ok: true, ...out });
91113
});
92114

115+
app.post("/api/exceptions/:id/appeal", async (c) => {
116+
if (!requireAuth(c)) return c.json({ error: "dashboard authentication required" }, 401);
117+
const body = await c.req.json<{ by?: string; reason?: string }>();
118+
const out = await appealException(
119+
company,
120+
c.req.param("id"),
121+
body.by ?? "operator",
122+
body.reason ?? "appeal",
123+
);
124+
if (!out.ok) return c.json(out, 400);
125+
return c.json(out);
126+
});
127+
93128
app.post("/api/kill", async (c) => {
94129
if (!requireAuth(c)) return c.json({ error: "dashboard authentication required" }, 401);
95130
const body = await c.req.json<{ killed: boolean }>();
@@ -98,7 +133,7 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
98133
});
99134

100135
app.post("/api/company-day", async (c) => {
101-
// Default off: do not imply human approval. Explicit body opt-in for demos/tests.
136+
await expireExceptionTtl(company);
102137
let autoApproveException = false;
103138
try {
104139
const body = await c.req.json<{ autoApproveException?: boolean }>();
@@ -117,11 +152,22 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
117152
const ctrl = (
118153
await company.db.select().from(controlState).where(eq(controlState.id, "global"))
119154
)[0];
155+
const transparency = (await company.db.select().from(transparencyRecords)).slice(-50);
120156
return c.json({
121157
aibom,
122158
spans,
123159
auditOk: recentDenies,
124160
killed: Boolean(ctrl?.killed),
161+
enforcement: company.policy.getEnforcementMode(),
162+
transparency,
163+
gLabels: {
164+
G1: "membership/active",
165+
G2: "deliberation trail",
166+
G3: "quorum N-of-M",
167+
G4: "dissent on reject",
168+
G5: "decision transparency",
169+
G6: "appeal/escalation",
170+
},
125171
asiControls: {
126172
ASI01: "untrusted KB/CRM boundary",
127173
ASI02: "fail-closed gateway + draft/settle",
@@ -144,6 +190,16 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
144190
});
145191
});
146192

193+
app.post("/api/governance/enforcement", async (c) => {
194+
if (!requireAuth(c)) return c.json({ error: "dashboard authentication required" }, 401);
195+
const body = await c.req.json<{ mode: EnforcementMode }>();
196+
if (body.mode !== "strict" && body.mode !== "audit") {
197+
return c.json({ error: "mode must be strict|audit" }, 400);
198+
}
199+
company.policy.setEnforcementMode(body.mode);
200+
return c.json({ ok: true, mode: body.mode });
201+
});
202+
147203
app.get("/api/traces/:taskId", async (c) => {
148204
const row = (await company.db.select().from(traces)).find(
149205
(t) => t.taskId === c.req.param("taskId"),
@@ -174,6 +230,7 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
174230
await stream.writeSSE({
175231
data: JSON.stringify({
176232
type: "snapshot",
233+
recent: recentFirmEvents(),
177234
firm: {
178235
agents: await company.db.select().from(agents),
179236
exceptions: (await company.db.select().from(exceptions)).filter(
@@ -182,6 +239,18 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
182239
},
183240
}),
184241
});
242+
const unsub = subscribeFirmEvents((event) => {
243+
void stream.writeSSE({ data: JSON.stringify(event) });
244+
});
245+
// Keep stream open; client disconnect ends the handler when write fails.
246+
try {
247+
while (true) {
248+
await stream.writeSSE({ data: JSON.stringify({ type: "heartbeat", at: Date.now() }) });
249+
await stream.sleep(15_000);
250+
}
251+
} finally {
252+
unsub();
253+
}
185254
}),
186255
);
187256

@@ -194,5 +263,12 @@ export async function createDefaultCompany(): Promise<{
194263
}> {
195264
const { mode } = resolveProvider();
196265
const company = await createCompany({ dbPath: process.env.CORPOS_DB });
266+
await expireExceptionTtl(company);
197267
return { company, mode };
198268
}
269+
270+
export function startTtlScheduler(company: Company, intervalMs = 30_000): NodeJS.Timeout {
271+
return setInterval(() => {
272+
void expireExceptionTtl(company);
273+
}, intervalMs);
274+
}

apps/api/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { serve } from "@hono/node-server";
22
import { serveStatic } from "@hono/node-server/serve-static";
33
import path from "node:path";
44
import { fileURLToPath } from "node:url";
5-
import { buildApp, createDefaultCompany } from "./app.js";
5+
import { buildApp, createDefaultCompany, startTtlScheduler } from "./app.js";
66
import { runCompanyDay } from "@corpos/core";
77

88
const scenario = process.argv.includes("--scenario");
@@ -14,6 +14,7 @@ if (scenario) {
1414
}
1515

1616
const { company, mode } = await createDefaultCompany();
17+
startTtlScheduler(company);
1718
const app = buildApp(company, mode);
1819

1920
const here = path.dirname(fileURLToPath(import.meta.url));

apps/console/src/main.tsx

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ type Governance = {
6262

6363
const REVEAL_MS = 500;
6464

65+
function authHeaders(extra: Record<string, string> = {}): Record<string, string> {
66+
const token = (import.meta as { env?: { VITE_DASHBOARD_API_TOKEN?: string } }).env
67+
?.VITE_DASHBOARD_API_TOKEN;
68+
const headers: Record<string, string> = { ...extra };
69+
if (token) headers.Authorization = `Bearer ${token}`;
70+
return headers;
71+
}
72+
6573
function App() {
6674
const [firm, setFirm] = useState<Firm | null>(null);
6775
const [exceptions, setExceptions] = useState<Exception[]>([]);
@@ -72,6 +80,7 @@ function App() {
7280
const [running, setRunning] = useState(false);
7381
const [tab, setTab] = useState<"ops" | "governor">("ops");
7482
const [dissent, setDissent] = useState("");
83+
const [liveTimeline, setLiveTimeline] = useState<TimelineEvent[]>([]);
7584

7685
const refresh = async () => {
7786
const f = await fetch("/api/firm").then((r) => r.json());
@@ -88,6 +97,27 @@ function App() {
8897
void refresh();
8998
}, []);
9099

100+
useEffect(() => {
101+
const es = new EventSource("/api/events");
102+
es.onmessage = (msg) => {
103+
try {
104+
const data = JSON.parse(msg.data) as {
105+
type?: string;
106+
payload?: TimelineEvent;
107+
kind?: string;
108+
};
109+
if (data.type === "timeline" && data.payload) {
110+
setLiveTimeline((prev) => [...prev, data.payload as TimelineEvent]);
111+
} else if (data.kind && (data as unknown as TimelineEvent).summary) {
112+
setLiveTimeline((prev) => [...prev, data as unknown as TimelineEvent]);
113+
}
114+
} catch {
115+
/* ignore heartbeat/parse */
116+
}
117+
};
118+
return () => es.close();
119+
}, []);
120+
91121
useEffect(() => {
92122
if (!day?.timeline.length) {
93123
setVisibleCount(0);
@@ -109,10 +139,11 @@ function App() {
109139
setRunning(true);
110140
setDay(null);
111141
setVisibleCount(0);
142+
setLiveTimeline([]);
112143
try {
113144
const r = (await fetch("/api/company-day", {
114145
method: "POST",
115-
headers: { "content-type": "application/json" },
146+
headers: authHeaders({ "content-type": "application/json" }),
116147
body: JSON.stringify({ autoApproveException: false }),
117148
}).then((res) => res.json())) as CompanyDay;
118149
setDay(r);
@@ -125,7 +156,7 @@ function App() {
125156
const decide = async (id: string, decision: "approved" | "rejected") => {
126157
await fetch(`/api/exceptions/${id}/decide`, {
127158
method: "POST",
128-
headers: { "content-type": "application/json" },
159+
headers: authHeaders({ "content-type": "application/json" }),
129160
body: JSON.stringify({
130161
decision,
131162
by: "operator@console",
@@ -141,7 +172,7 @@ function App() {
141172
if (next && !window.confirm("Engage kill switch? All tool invokes will deny.")) return;
142173
await fetch("/api/kill", {
143174
method: "POST",
144-
headers: { "content-type": "application/json" },
175+
headers: authHeaders({ "content-type": "application/json" }),
145176
body: JSON.stringify({ killed: next }),
146177
});
147178
await refresh();
@@ -256,29 +287,31 @@ function App() {
256287
data-timeline-visible={String(visibleCount)}
257288
>
258289
<h2>Company day</h2>
259-
{!day ? (
290+
{!day && liveTimeline.length === 0 ? (
260291
<p class="muted">Not run yet — watch agents hand off work across the firm.</p>
261292
) : (
262293
<>
263-
<div class="metrics" data-testid="day-metrics">
264-
<span>
265-
Handoffs <strong>{day.handoffs}</strong>
266-
</span>
267-
<span>
268-
Autonomous <strong>{day.autonomousSettles}</strong>
269-
</span>
270-
<span>
271-
Exceptions <strong>{day.exceptionSettles}</strong>
272-
</span>
273-
<span>
274-
Trust <strong>{day.trustAfter}</strong>
275-
</span>
276-
<span>
277-
Status <strong>{day.ok ? "ok" : "incomplete"}</strong>
278-
</span>
279-
</div>
294+
{day ? (
295+
<div class="metrics" data-testid="day-metrics">
296+
<span>
297+
Handoffs <strong>{day.handoffs}</strong>
298+
</span>
299+
<span>
300+
Autonomous <strong>{day.autonomousSettles}</strong>
301+
</span>
302+
<span>
303+
Exceptions <strong>{day.exceptionSettles}</strong>
304+
</span>
305+
<span>
306+
Trust <strong>{day.trustAfter}</strong>
307+
</span>
308+
<span>
309+
Status <strong>{day.ok ? "ok" : "incomplete"}</strong>
310+
</span>
311+
</div>
312+
) : null}
280313
<ol class="timeline" aria-live="polite">
281-
{day.timeline.slice(0, visibleCount).map((evt) => (
314+
{(day ? day.timeline.slice(0, visibleCount) : liveTimeline).map((evt) => (
282315
<li key={evt.id} data-timeline-id={evt.id} data-timeline-kind={evt.kind}>
283316
<div class="role">{evt.role}</div>
284317
<div>

docs/adr/0011-provider-strategy.md

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
# ADR-11: Provider strategy
1+
# ADR-11: Provider strategy (r3 amendment)
22

33
## Status
4-
Accepted (corpos-autonomous-company-r2)
5-
6-
## Context
7-
July 2026 autonomous-company reference revision.
4+
Accepted (corpos-autonomous-company-r3)
85

96
## Decision
10-
Implement Provider strategy as specified in corporate master-spec ADR-11.
11-
12-
## Consequences
13-
Bound by site harness verify/adversarial gates.
7+
SimulationProvider remains the CI/default scripted company-day provider.
8+
When `CORPOS_ALLOW_LIVE=1` and `OPENROUTER_API_KEY` are set, `resolveProvider()`
9+
returns HttpLLMProvider and company-day/orchestrator use that live provider.
10+
`/api/health.mode` is `live` only when HttpLLMProvider is active.

0 commit comments

Comments
 (0)