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
4 changes: 2 additions & 2 deletions .corp-harness/site.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"schema": "corporate-site-site/v1",
"site_id": "corpos",
"corporate_program": "corpos-autonomous-company-r2",
"corporate_handoff_sha256": "15052d5905e7e73687465dbf6d1d801d31105a7c7abfd33ea50ceb2652dcbeb6",
"corporate_program": "corpos-autonomous-company-r3",
"corporate_handoff_sha256": "ae1ffdc303c5a4589a210113d268551c2a6327b6841eb85eeef7bc7a6e6a717d",
"verify_argv": ["./scripts/harness/verify.sh"],
"adversarial_argv": ["./scripts/harness/adversarial.sh"]
}
5 changes: 3 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ Workspaces: `packages/core`, `packages/mcp-knowledge`, `apps/api`, `apps/console
Commands: `npm run dev`, `npm test`, `npm run scenario`, `npm run audit:verify`,
`./scripts/harness/verify.sh`, `./scripts/harness/adversarial.sh`.

Site id: `corpos`. Program: `corpos-autonomous-company-r2`.
Live LLM requires `CORPOS_ALLOW_LIVE=1` and `OPENROUTER_API_KEY`.
Site id: `corpos`. Program: `corpos-autonomous-company-r3`.
Live LLM requires `CORPOS_ALLOW_LIVE=1` and `OPENROUTER_API_KEY` (company-day uses HttpLLMProvider when live).
G1–G6 firm governance coded; TTL scheduler; console Bearer; orchestrator-driven day; live SSE.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ CorpOS shows how a firm operates when department agents do most work and **human

> **Scope:** Reference architecture and runnable demo — **not** a production-hardened SaaS. See [SECURITY.md](SECURITY.md).

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.
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.

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).

Expand Down
4 changes: 3 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ Treat it as a design artifact you can run locally.

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

## Not provided
Expand Down
78 changes: 77 additions & 1 deletion apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ import { cors } from "hono/cors";
import { streamSSE } from "hono/streaming";
import {
agents,
appealException,
contracts,
controlState,
counterfactualReplay,
createCompany,
decideException,
deliberationEntries,
departments,
exceptions,
expireExceptionTtl,
listSpans,
resolveProvider,
runCompanyDay,
subscribeFirmEvents,
recentFirmEvents,
transparencyRecords,
traces,
type Company,
type EnforcementMode,
} from "@corpos/core";
import { eq } from "drizzle-orm";
import fs from "node:fs";
Expand Down Expand Up @@ -45,12 +52,18 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
const app = new Hono();
app.use("*", cors());

const envMode = process.env.CORPOS_ENFORCEMENT;
if (envMode === "strict" || envMode === "audit") {
company.policy.setEnforcementMode(envMode);
}

app.get("/api/health", (c) =>
c.json({
ok: true,
mode,
product: "autonomous-company-reference",
provider: mode === "live" ? "HttpLLMProvider" : "SimulationProvider",
enforcement: company.policy.getEnforcementMode(),
}),
);

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

app.get("/api/exceptions/:id/deliberation", async (c) => {
const rows = await company.db
.select()
.from(deliberationEntries)
.where(eq(deliberationEntries.exceptionId, c.req.param("id")));
return c.json(rows);
});

app.post("/api/exceptions/:id/decide", async (c) => {
if (!requireAuth(c)) return c.json({ error: "dashboard authentication required" }, 401);
await expireExceptionTtl(company);
const body = await c.req.json<{
decision: "approved" | "rejected";
by?: string;
Expand All @@ -90,6 +112,19 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
return c.json({ ok: true, ...out });
});

app.post("/api/exceptions/:id/appeal", async (c) => {
if (!requireAuth(c)) return c.json({ error: "dashboard authentication required" }, 401);
const body = await c.req.json<{ by?: string; reason?: string }>();
const out = await appealException(
company,
c.req.param("id"),
body.by ?? "operator",
body.reason ?? "appeal",
);
if (!out.ok) return c.json(out, 400);
return c.json(out);
});

app.post("/api/kill", async (c) => {
if (!requireAuth(c)) return c.json({ error: "dashboard authentication required" }, 401);
const body = await c.req.json<{ killed: boolean }>();
Expand All @@ -98,7 +133,7 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
});

app.post("/api/company-day", async (c) => {
// Default off: do not imply human approval. Explicit body opt-in for demos/tests.
await expireExceptionTtl(company);
let autoApproveException = false;
try {
const body = await c.req.json<{ autoApproveException?: boolean }>();
Expand All @@ -117,11 +152,22 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
const ctrl = (
await company.db.select().from(controlState).where(eq(controlState.id, "global"))
)[0];
const transparency = (await company.db.select().from(transparencyRecords)).slice(-50);
return c.json({
aibom,
spans,
auditOk: recentDenies,
killed: Boolean(ctrl?.killed),
enforcement: company.policy.getEnforcementMode(),
transparency,
gLabels: {
G1: "membership/active",
G2: "deliberation trail",
G3: "quorum N-of-M",
G4: "dissent on reject",
G5: "decision transparency",
G6: "appeal/escalation",
},
asiControls: {
ASI01: "untrusted KB/CRM boundary",
ASI02: "fail-closed gateway + draft/settle",
Expand All @@ -144,6 +190,16 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
});
});

app.post("/api/governance/enforcement", async (c) => {
if (!requireAuth(c)) return c.json({ error: "dashboard authentication required" }, 401);
const body = await c.req.json<{ mode: EnforcementMode }>();
if (body.mode !== "strict" && body.mode !== "audit") {
return c.json({ error: "mode must be strict|audit" }, 400);
}
company.policy.setEnforcementMode(body.mode);
return c.json({ ok: true, mode: body.mode });
});

app.get("/api/traces/:taskId", async (c) => {
const row = (await company.db.select().from(traces)).find(
(t) => t.taskId === c.req.param("taskId"),
Expand Down Expand Up @@ -174,6 +230,7 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
await stream.writeSSE({
data: JSON.stringify({
type: "snapshot",
recent: recentFirmEvents(),
firm: {
agents: await company.db.select().from(agents),
exceptions: (await company.db.select().from(exceptions)).filter(
Expand All @@ -182,6 +239,18 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula
},
}),
});
const unsub = subscribeFirmEvents((event) => {
void stream.writeSSE({ data: JSON.stringify(event) });
});
// Keep stream open; client disconnect ends the handler when write fails.
try {
while (true) {
await stream.writeSSE({ data: JSON.stringify({ type: "heartbeat", at: Date.now() }) });
await stream.sleep(15_000);
}
} finally {
unsub();
}
}),
);

Expand All @@ -194,5 +263,12 @@ export async function createDefaultCompany(): Promise<{
}> {
const { mode } = resolveProvider();
const company = await createCompany({ dbPath: process.env.CORPOS_DB });
await expireExceptionTtl(company);
return { company, mode };
}

export function startTtlScheduler(company: Company, intervalMs = 30_000): NodeJS.Timeout {
return setInterval(() => {
void expireExceptionTtl(company);
}, intervalMs);
}
3 changes: 2 additions & 1 deletion apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { serve } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { buildApp, createDefaultCompany } from "./app.js";
import { buildApp, createDefaultCompany, startTtlScheduler } from "./app.js";
import { runCompanyDay } from "@corpos/core";

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

const { company, mode } = await createDefaultCompany();
startTtlScheduler(company);
const app = buildApp(company, mode);

const here = path.dirname(fileURLToPath(import.meta.url));
Expand Down
77 changes: 55 additions & 22 deletions apps/console/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ type Governance = {

const REVEAL_MS = 500;

function authHeaders(extra: Record<string, string> = {}): Record<string, string> {
const token = (import.meta as { env?: { VITE_DASHBOARD_API_TOKEN?: string } }).env
?.VITE_DASHBOARD_API_TOKEN;
const headers: Record<string, string> = { ...extra };
if (token) headers.Authorization = `Bearer ${token}`;
return headers;
}

function App() {
const [firm, setFirm] = useState<Firm | null>(null);
const [exceptions, setExceptions] = useState<Exception[]>([]);
Expand All @@ -72,6 +80,7 @@ function App() {
const [running, setRunning] = useState(false);
const [tab, setTab] = useState<"ops" | "governor">("ops");
const [dissent, setDissent] = useState("");
const [liveTimeline, setLiveTimeline] = useState<TimelineEvent[]>([]);

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

useEffect(() => {
const es = new EventSource("/api/events");
es.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data) as {
type?: string;
payload?: TimelineEvent;
kind?: string;
};
if (data.type === "timeline" && data.payload) {
setLiveTimeline((prev) => [...prev, data.payload as TimelineEvent]);
} else if (data.kind && (data as unknown as TimelineEvent).summary) {
setLiveTimeline((prev) => [...prev, data as unknown as TimelineEvent]);
}
} catch {
/* ignore heartbeat/parse */
}
};
return () => es.close();
}, []);

useEffect(() => {
if (!day?.timeline.length) {
setVisibleCount(0);
Expand All @@ -109,10 +139,11 @@ function App() {
setRunning(true);
setDay(null);
setVisibleCount(0);
setLiveTimeline([]);
try {
const r = (await fetch("/api/company-day", {
method: "POST",
headers: { "content-type": "application/json" },
headers: authHeaders({ "content-type": "application/json" }),
body: JSON.stringify({ autoApproveException: false }),
}).then((res) => res.json())) as CompanyDay;
setDay(r);
Expand All @@ -125,7 +156,7 @@ function App() {
const decide = async (id: string, decision: "approved" | "rejected") => {
await fetch(`/api/exceptions/${id}/decide`, {
method: "POST",
headers: { "content-type": "application/json" },
headers: authHeaders({ "content-type": "application/json" }),
body: JSON.stringify({
decision,
by: "operator@console",
Expand All @@ -141,7 +172,7 @@ function App() {
if (next && !window.confirm("Engage kill switch? All tool invokes will deny.")) return;
await fetch("/api/kill", {
method: "POST",
headers: { "content-type": "application/json" },
headers: authHeaders({ "content-type": "application/json" }),
body: JSON.stringify({ killed: next }),
});
await refresh();
Expand Down Expand Up @@ -256,29 +287,31 @@ function App() {
data-timeline-visible={String(visibleCount)}
>
<h2>Company day</h2>
{!day ? (
{!day && liveTimeline.length === 0 ? (
<p class="muted">Not run yet — watch agents hand off work across the firm.</p>
) : (
<>
<div class="metrics" data-testid="day-metrics">
<span>
Handoffs <strong>{day.handoffs}</strong>
</span>
<span>
Autonomous <strong>{day.autonomousSettles}</strong>
</span>
<span>
Exceptions <strong>{day.exceptionSettles}</strong>
</span>
<span>
Trust <strong>{day.trustAfter}</strong>
</span>
<span>
Status <strong>{day.ok ? "ok" : "incomplete"}</strong>
</span>
</div>
{day ? (
<div class="metrics" data-testid="day-metrics">
<span>
Handoffs <strong>{day.handoffs}</strong>
</span>
<span>
Autonomous <strong>{day.autonomousSettles}</strong>
</span>
<span>
Exceptions <strong>{day.exceptionSettles}</strong>
</span>
<span>
Trust <strong>{day.trustAfter}</strong>
</span>
<span>
Status <strong>{day.ok ? "ok" : "incomplete"}</strong>
</span>
</div>
) : null}
<ol class="timeline" aria-live="polite">
{day.timeline.slice(0, visibleCount).map((evt) => (
{(day ? day.timeline.slice(0, visibleCount) : liveTimeline).map((evt) => (
<li key={evt.id} data-timeline-id={evt.id} data-timeline-kind={evt.kind}>
<div class="role">{evt.role}</div>
<div>
Expand Down
15 changes: 6 additions & 9 deletions docs/adr/0011-provider-strategy.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
# ADR-11: Provider strategy
# ADR-11: Provider strategy (r3 amendment)

## Status
Accepted (corpos-autonomous-company-r2)

## Context
July 2026 autonomous-company reference revision.
Accepted (corpos-autonomous-company-r3)

## Decision
Implement Provider strategy as specified in corporate master-spec ADR-11.

## Consequences
Bound by site harness verify/adversarial gates.
SimulationProvider remains the CI/default scripted company-day provider.
When `CORPOS_ALLOW_LIVE=1` and `OPENROUTER_API_KEY` are set, `resolveProvider()`
returns HttpLLMProvider and company-day/orchestrator use that live provider.
`/api/health.mode` is `live` only when HttpLLMProvider is active.
Loading
Loading