Skip to content
This repository was archived by the owner on Oct 22, 2026. It is now read-only.
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
16 changes: 4 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
# CorpOS

> **Company-day simulation** of a firm — work contracts, PDP/PEP policy gates, and humans who Approve / Reject / Kill. Not an autonomous-company SaaS, and not LangGraph, CrewAI, or any multi-agent orchestration framework.
One scripted company day. Support hands a $49 refund to Finance. Finance settles `billing.issue_refund` through the tool gateway and hands the work to Ops. Ops queues `ops.restart_service` for billing-api. The refund stays on the ledger. The restart stays queued until Approve, Reject, or Kill.

> **Permit rule:** Approve / Reject / Kill is the closer. Default actors are scripted; live LLMs are optional. This is not a live EHS, health, or finance workforce. Same instinct: [SafetyMP](https://github.com/SafetyMP/SafetyMP).
`POST /api/company-day` runs that day. It does not settle the restart, and it does not honor `autoApproveException` from the JSON body. An unauthenticated caller cannot auto-approve.

[![CI](https://github.com/SafetyMP/CorpOS/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/SafetyMP/CorpOS/actions/workflows/ci.yml)
[![CodeQL](https://github.com/SafetyMP/CorpOS/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/SafetyMP/CorpOS/actions/workflows/codeql.yml)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/SafetyMP/CorpOS/badge)](https://scorecard.dev/viewer/?uri=github.com/SafetyMP/CorpOS)
[![License: Apache-2.0](https://img.shields.io/github/license/SafetyMP/CorpOS)](LICENSE)
[![Node](https://img.shields.io/badge/node-%E2%89%A522-339933?logo=node.js&logoColor=white)](#quick-start)

CorpOS simulates a company day: department actors settle work contracts under a policy gate, and **humans govern by exception** (Approve / Reject / Kill in the ops console). Autonomy is earned from evidence, not granted in prompts. Interop protocols (MCP) are transport; firm-side governance includes **G1 membership** and **G4 dissent** today, with a broader G1–G6 crosswalk in the docs. Positioning: [`docs/DESIGN-PIVOT.md`](docs/DESIGN-PIVOT.md).

> **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`) is an optional actor inside the sim 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). Docs index: [`docs/README.md`](docs/README.md).

**Jump to:** [Demo](#demo) · [Quick start](#quick-start) · [Architecture](#architecture) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) · [ADRs](docs/adr/README.md)

---
Expand All @@ -37,7 +29,7 @@ Read the thesis: [`docs/future-of-the-firm.md`](docs/future-of-the-firm.md). Gov
- **Local demo:** `npm install && npm run build && npm run dev` → [http://localhost:3000](http://localhost:3000)
- **Regenerate GIF/PNGs:** start the console, then `npm run screenshots` — see [`docs/assets/README.md`](docs/assets/README.md)

Click **Run company day** to watch the agent activity timeline: support → finance → ops handoffs, an autonomous settle, an exception approval path, compensation, and trust unlock. The console sends `autoApproveException: false` so the exception stays in the HITL queue for Approve/Reject.
Click **Run company day** to watch the activity timeline: support → finance → ops. The refund stays in place. The service restart stays queued until Approve, Reject, or Kill.

---

Expand All @@ -53,7 +45,7 @@ Contributors and CI should use `npm ci --include=dev` (see [CONTRIBUTING.md](CON

```bash
npm test
npm run scenario # HITL default-off; exits non-zero when exception is not auto-settled
npm run scenario # default day exits 0: refund stays, restart stays queued
npm run audit:verify
./scripts/harness/verify.sh # functional / static acceptance
./scripts/harness/adversarial.sh # authorized local adversarial probes (CI also runs this)
Expand Down
12 changes: 4 additions & 8 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,10 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula

app.post("/api/company-day", async (c) => {
await expireExceptionTtl(company);
let autoApproveException = false;
try {
const body = await c.req.json<{ autoApproveException?: boolean }>();
if (body.autoApproveException === true) autoApproveException = true;
} catch {
/* empty body */
}
const { result } = await runCompanyDay({ company, autoApproveException });
// The JSON body cannot settle the restart. autoApproveException is ignored
// for every caller, including an unauthenticated one. Approve, Reject, or
// Kill is what moves a queued restart.
const { result } = await runCompanyDay({ company });
return c.json(result);
});

Expand Down
28 changes: 18 additions & 10 deletions packages/core/src/company-day.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,9 @@ export async function runCompanyDay(opts?: {
}
}

const compensated = await company.gateway.compensate(contractId);
if (compensated > 0) {
push({
agentId: "agent_finance",
role: "Finance",
kind: "compensate",
summary: `Compensated ${compensated} settled action(s); ledger restored.`,
});
}
// Success leaves the settled refund in place. Explicit gateway.compensate()
// may still reverse it; this script does not.
const compensated = 0;

const short = await openContract(company, {
title: "Expiring SLA",
Expand All @@ -334,6 +328,20 @@ export async function runCompanyDay(opts?: {
}

const trustAfter = (await company.trust.get("agent_support"))?.maxAutonomousRisk ?? 0;
const refundHeld = company.seed.refunds.some(
(refund) => refund.amount === 49 && refund.customer === "ada@example.com",
);
const restartPending = (await company.db.select().from(exceptions)).some(
(row) =>
row.contractId === contractId &&
row.tool === "ops.restart_service" &&
row.state === "pending",
);
// The scripted day succeeds when the refund is still on the ledger and the
// restart is waiting for Approve, Reject, or Kill. Settling that restart is
// an explicit opt-in, not a requirement for ok.
const dayComplete = handoffs >= 2 && autonomousSettles >= 1 && compensated === 0 && refundHeld;
const restartDisposition = autoApprove ? exceptionSettles >= 1 : restartPending;

const result: CompanyDayResult = {
contractId,
Expand All @@ -344,7 +352,7 @@ export async function runCompanyDay(opts?: {
trustAfter,
slaExceptions,
auditHead: await company.audit.head(),
ok: handoffs >= 2 && autonomousSettles >= 1 && exceptionSettles >= 1 && trustAfter >= 2,
ok: dayComplete && restartDisposition,
timeline,
providerMode,
};
Expand Down
76 changes: 73 additions & 3 deletions test/company-day.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { describe, expect, it } from "vitest";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { buildApp } from "@corpos/api";
import {
computeMaxAutonomousRisk,
counterfactualReplay,
createCompany,
exceptions,
runCompanyDay,
} from "@corpos/core";

describe("company day", () => {
it("runs multi-handoff day with autonomy, exception, compensation, sla, trust", async () => {
it("runs multi-handoff day with autonomy, exception, sla, trust", async () => {
const { company, result } = await runCompanyDay({
dbPath: ":memory:",
autoApproveException: true,
});
expect(result.handoffs).toBeGreaterThanOrEqual(2);
expect(result.autonomousSettles).toBeGreaterThanOrEqual(1);
expect(result.exceptionSettles).toBeGreaterThanOrEqual(1);
expect(result.compensated).toBeGreaterThanOrEqual(1);
expect(result.compensated).toBe(0);
expect(company.seed.refunds).toEqual([
expect.objectContaining({ amount: 49, customer: "ada@example.com" }),
]);
expect(result.slaExceptions).toBeGreaterThanOrEqual(1);
expect(result.trustAfter).toBeGreaterThanOrEqual(2);
expect(result.ok).toBe(true);
Expand All @@ -33,14 +38,79 @@ describe("company day", () => {
company.close();
});

it("does not auto-approve exceptions by default", async () => {
it("is ok when the refund stays and the restart is pending", async () => {
const { company, result } = await runCompanyDay({ dbPath: ":memory:" });
expect(result.exceptionSettles).toBe(0);
expect(result.compensated).toBe(0);
expect(result.timeline.some((e) => e.summary.includes("Human approved"))).toBe(false);
expect(result.timeline.some((e) => e.summary.includes("Demo auto-approved"))).toBe(false);
expect(company.seed.refunds).toEqual([
expect.objectContaining({ amount: 49, customer: "ada@example.com" }),
]);
const restarts = (await company.db.select().from(exceptions)).filter(
(row) => row.contractId === result.contractId && row.tool === "ops.restart_service",
);
expect(restarts).toHaveLength(1);
expect(restarts[0]?.state).toBe("pending");
expect(result.ok).toBe(true);
company.close();
});

it("leaves a successful refund in place", async () => {
const { company, result } = await runCompanyDay({ dbPath: ":memory:" });
expect(result.autonomousSettles).toBeGreaterThanOrEqual(1);
expect(result.compensated).toBe(0);
expect(result.timeline.some((e) => e.kind === "compensate")).toBe(false);
expect(company.seed.refunds).toEqual([
expect.objectContaining({ id: "ref_1", amount: 49, customer: "ada@example.com" }),
]);
const reversed = await company.gateway.compensate(result.contractId);
expect(reversed).toBe(1);
expect(company.seed.refunds).toEqual([]);
company.close();
});

it("does not let an unauthenticated company-day call auto-approve", async () => {
const previous = {
token: process.env.DASHBOARD_API_TOKEN,
allow: process.env.CORPOS_ALLOW_UNAUTHENTICATED,
};
delete process.env.DASHBOARD_API_TOKEN;
delete process.env.CORPOS_ALLOW_UNAUTHENTICATED;
try {
const company = await createCompany({ dbPath: ":memory:" });
const app = buildApp(company, "simulation");
const res = await app.request("/api/company-day", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ autoApproveException: true }),
});
expect(res.status).toBe(200);
const body = (await res.json()) as {
exceptionSettles: number;
compensated: number;
timeline: Array<{ summary: string }>;
};
expect(body.exceptionSettles).toBe(0);
expect(body.compensated).toBe(0);
expect(body.timeline.some((e) => e.summary.includes("Demo auto-approved"))).toBe(false);
const restarts = (await company.db.select().from(exceptions)).filter(
(row) => row.tool === "ops.restart_service",
);
expect(restarts.length).toBe(1);
expect(restarts[0]?.state).toBe("pending");
expect(company.seed.refunds).toEqual([
expect.objectContaining({ amount: 49, customer: "ada@example.com" }),
]);
company.close();
} finally {
if (previous.token === undefined) delete process.env.DASHBOARD_API_TOKEN;
else process.env.DASHBOARD_API_TOKEN = previous.token;
if (previous.allow === undefined) delete process.env.CORPOS_ALLOW_UNAUTHENTICATED;
else process.env.CORPOS_ALLOW_UNAUTHENTICATED = previous.allow;
}
});

it("fail-closed unknown tool and kill switch", async () => {
const company = await createCompany({ dbPath: ":memory:" });
const denied = await company.gateway.invoke(
Expand Down
Loading