diff --git a/AGENTS.md b/AGENTS.md index 8abc170..4d11b7a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,12 +7,21 @@ | `./scripts/harness/verify.sh` | Functional and static acceptance | | `./scripts/harness/adversarial.sh` | Authorized local adversarial probes | -Record `verification_scripts` as the site directory `scripts/harness` (exactly those -two scripts). Optional wrappers may remain at `scripts/verify.sh` / -`scripts/adversarial.sh` for humans; they are outside the digest boundary. +Record `verification_scripts` as the site directory `scripts/harness`. Required +entrypoints are `verify.sh` and `adversarial.sh`. Digest-bound companions under +the same directory (included in the harness digest) are: + +- `check-stub-canary.sh` — mandatory non-trivial verify canary +- `adversarial-run.mjs` — adversarial probe implementation + +Optional wrappers may remain at `scripts/verify.sh` / `scripts/adversarial.sh` +(and `scripts/check-stub-canary.sh`) for humans; they are outside the digest +boundary. The corporate handoff fixes scope. The site manager assigns ADRs; site specialists write; operations excellence reviews current evidence. Work in isolated roots, never edit corporate approval state, and never self-approve. Site id: `corpos`. Prior Cursor Harness v4 is under `_archives/harness-v4/`. +Product company-day demos must not auto-approve exceptions unless a test/CI +caller passes `autoApproveException: true` explicitly. diff --git a/apps/api/package.json b/apps/api/package.json index c2a4a1e..c6615db 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -3,6 +3,14 @@ "version": "0.2.0", "private": true, "type": "module", + "main": "./dist/app.js", + "types": "./dist/app.d.ts", + "exports": { + ".": { + "types": "./dist/app.d.ts", + "import": "./dist/app.js" + } + }, "scripts": { "build": "tsc -p tsconfig.json", "typecheck": "tsc -p tsconfig.json --noEmit", diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index a0da96a..34c02dc 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -21,7 +21,8 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -function requireAuth(c: { req: { header: (n: string) => string | undefined } }): boolean { +/** Dashboard bearer gate — exported for adversarial behavioral probes. */ +export function requireAuth(c: { req: { header: (n: string) => string | undefined } }): boolean { if (process.env.CORPOS_MODE !== "shared") return true; const expected = process.env.DASHBOARD_API_TOKEN?.trim(); if (!expected) return false; @@ -97,10 +98,11 @@ export function buildApp(company: Company, mode: "simulation" | "live" = "simula }); app.post("/api/company-day", async (c) => { - let autoApproveException = true; + // Default off: do not imply human approval. Explicit body opt-in for demos/tests. + let autoApproveException = false; try { const body = await c.req.json<{ autoApproveException?: boolean }>(); - if (body.autoApproveException === false) autoApproveException = false; + if (body.autoApproveException === true) autoApproveException = true; } catch { /* empty body */ } diff --git a/apps/console/src/main.tsx b/apps/console/src/main.tsx index c661413..4d6592d 100644 --- a/apps/console/src/main.tsx +++ b/apps/console/src/main.tsx @@ -113,7 +113,7 @@ function App() { const r = (await fetch("/api/company-day", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ autoApproveException: true }), + body: JSON.stringify({ autoApproveException: false }), }).then((res) => res.json())) as CompanyDay; setDay(r); await refresh(); diff --git a/docs/governance-crosswalk.md b/docs/governance-crosswalk.md index dfe1dfe..c040634 100644 --- a/docs/governance-crosswalk.md +++ b/docs/governance-crosswalk.md @@ -13,7 +13,7 @@ Pedagogical mapping only — **not** a certification claim. ## OWASP Top 10 for Agentic Applications 2026 -See Governor `/api/governance` and `scripts/adversarial-run.mjs` ASI cells. +See Governor `/api/governance` and `scripts/harness/adversarial-run.mjs` ASI cells. ## Related standards diff --git a/evidence/site-delivery/receipt.json b/evidence/site-delivery/receipt.json index dead9c9..78e52a1 100644 --- a/evidence/site-delivery/receipt.json +++ b/evidence/site-delivery/receipt.json @@ -1,8 +1,45 @@ { "schema": "corpos-site-delivery-receipt/v1", "program_id": "corpos-autonomous-company-r2", - "recorded_at": "2026-07-26T13:13:35.124777+00:00", + "site_id": "corpos", + "recorded_at": "2026-07-26T13:33:11.756553+00:00", + "captured_at": "2026-07-26T13:33:11.756553+00:00", + "node": "v26.4.0", "verify": "PASS", "adversarial": "PASS", - "notes": "Orchestrator + governance plane revision; shared firm store; ASI matrix." + "verification_scripts": "scripts/harness", + "verify_argv": ["./scripts/harness/verify.sh"], + "adversarial_argv": ["./scripts/harness/adversarial.sh"], + "verification_scripts_sha256": "8415380abcd32346db4a1c151dd4c063dddc58bb3f561780360c0eae62489538", + "harness_file_sha256": { + "adversarial-run.mjs": "3438da18530a0a6de798566eacd97a08456054edbedb026a0563e6475cae66fd", + "adversarial.sh": "c5c40ecb99ba3c5322d036ed7c6838a6f969e343003f5da3148a17288cb11d52", + "check-stub-canary.sh": "7bf1f3ceda1fa7e5f5feee2da0bbc17597c457ffa8b491801162ed8d05e59db5", + "verify.sh": "688518e9a148710768bb565525cac41c78a4092208069b6fea08825ed1483df8" + }, + "surfaces": { + "firm_work_control": "packages/core/src", + "api": "apps/api/src", + "console": "apps/console/src", + "mcp_knowledge": "packages/mcp-knowledge/src", + "adrs": "docs/adr", + "essay": "docs/future-of-the-firm.md" + }, + "company_day": { + "contractId": "ctr_g16siuyoYE", + "handoffs": 2, + "autonomousSettles": 1, + "exceptionSettles": 1, + "compensated": 1, + "trustAfter": 2, + "slaExceptions": 1, + "auditHead": "913d51c35e75fe4c148b6f893b2add53975564e8033874f24ce8be8737280e5e", + "ok": true + }, + "adversarial_stdout": "adversarial: ok", + "stack_forbidden_absent": { + "better-sqlite3": true, + "express_import": true + }, + "notes": "Harness effectiveness remediation: digest-bound probes/canary; behavioral AUTH/OTEL/ASI05; HITL auto-approve default off." } diff --git a/packages/core/src/company-day.ts b/packages/core/src/company-day.ts index cd00857..8f54a32 100644 --- a/packages/core/src/company-day.ts +++ b/packages/core/src/company-day.ts @@ -50,7 +50,10 @@ export async function runCompanyDay(opts?: { company?: Company; withMcp?: boolean; serverCommand?: { command: string; args: string[] }; - /** Auto-approve ops exception for deterministic CI (console can decide manually). */ + /** + * Explicit opt-in only. Default false so demo/API paths do not imply human + * approval. Tests/CI that need a settled exception must pass true. + */ autoApproveException?: boolean; }): Promise<{ company: Company; result: CompanyDayResult }> { resetSpans(); @@ -194,7 +197,7 @@ export async function runCompanyDay(opts?: { delegationDepth: 2, }); - const autoApprove = opts?.autoApproveException !== false; + const autoApprove = opts?.autoApproveException === true; if (ops.awaitingExceptionId) { push({ agentId: "agent_ops", @@ -209,7 +212,7 @@ export async function runCompanyDay(opts?: { agentId: "agent_ops", role: "Ops", kind: "exception", - summary: "Human approved restart; billing-api restarted.", + summary: "Demo auto-approved restart (explicit opt-in); billing-api restarted.", }); await company.trust.recordAccept("agent_support"); push({ diff --git a/scripts/check-stub-canary.sh b/scripts/check-stub-canary.sh index 47c4a01..bb9d1e8 100755 --- a/scripts/check-stub-canary.sh +++ b/scripts/check-stub-canary.sh @@ -1,10 +1,3 @@ -#!/usr/bin/env bash -# DO_NOT_DELETE_STUB_CANARY -set -euo pipefail -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT" -if [[ -f scripts/verify.sh ]] && grep -q 'TODO: add real test' scripts/verify.sh; then - echo "STUB_CANARY: placeholder verify.sh" >&2 - exit 1 -fi -echo "check-stub-canary: ok" +#!/usr/bin/env bash +set -euo pipefail +exec "$(cd "$(dirname "$0")" && pwd)/harness/check-stub-canary.sh" "$@" diff --git a/scripts/adversarial-run.mjs b/scripts/harness/adversarial-run.mjs similarity index 73% rename from scripts/adversarial-run.mjs rename to scripts/harness/adversarial-run.mjs index 2be2280..752e929 100644 --- a/scripts/adversarial-run.mjs +++ b/scripts/harness/adversarial-run.mjs @@ -6,6 +6,7 @@ import { listSpans, resetSpans, } from "@corpos/core"; +import { buildApp } from "@corpos/api"; import { eq } from "drizzle-orm"; import fs from "node:fs"; import path from "node:path"; @@ -19,6 +20,8 @@ function cell(id, ok, detail = "") { if (!ok) throw new Error(`${id} failed: ${detail}`); } +const DANGEROUS_TOOL_RE = /\b(shell|eval|exec|spawn|child_process|bash|powershell|os\.system)\b/i; + // ASI02 / gateway bypass const bypass = await company.gateway.invoke( "evil.shell", @@ -37,13 +40,17 @@ cell( "unknown tool", ); -// ASI05 attestation — no shell/eval tools -const aibomPath = path.resolve("docs/aibom.json"); -const aibom = JSON.parse(fs.readFileSync(aibomPath, "utf8")); +// ASI05 — live tool registry scan (not AIBOM self-attestation alone) +const registered = company.gateway.list(); +const dangerousLive = registered.filter( + (t) => DANGEROUS_TOOL_RE.test(t.name) || DANGEROUS_TOOL_RE.test(t.description ?? ""), +); cell( "ASI05", - Array.isArray(aibom.dangerous_tools_registered) && aibom.dangerous_tools_registered.length === 0, - "no dangerous tools", + registered.length > 0 && dangerousLive.length === 0, + dangerousLive.length + ? `dangerous tools: ${dangerousLive.map((t) => t.name).join(",")}` + : "no shell/eval tools in live registry", ); // ASI03 three-layer privilege expansion @@ -127,6 +134,8 @@ cell("ASI01", untrusted, "KB marked untrusted"); cell("ASI06", untrusted, "context poisoning boundary"); // ASI04 AIBOM +const aibomPath = path.resolve("docs/aibom.json"); +const aibom = JSON.parse(fs.readFileSync(aibomPath, "utf8")); cell("ASI04", Boolean(aibom.policyBundleHash) && Array.isArray(aibom.mcp_servers), "aibom"); // ASI07 handoff envelope via deny missing originator @@ -153,10 +162,35 @@ const email = await company.gateway.invoke( ); cell("ASI09", email.decision.effect === "approve" && Boolean(email.decision.approvalId), "HITL"); -// Auth shared mode +// AUTH — exercise real requireAuth via HTTP on /api/kill (shared mode) +const prevMode = process.env.CORPOS_MODE; +const prevToken = process.env.DASHBOARD_API_TOKEN; process.env.CORPOS_MODE = "shared"; process.env.DASHBOARD_API_TOKEN = "secret"; -cell("AUTH", `Bearer ${process.env.DASHBOARD_API_TOKEN}` === "Bearer secret", "token"); +const app = buildApp(company, "simulation"); +const unauth = await app.request("/api/kill", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ killed: true }), +}); +const authed = await app.request("/api/kill", { + method: "POST", + headers: { + "content-type": "application/json", + authorization: "Bearer secret", + }, + body: JSON.stringify({ killed: false }), +}); +if (prevMode === undefined) delete process.env.CORPOS_MODE; +else process.env.CORPOS_MODE = prevMode; +if (prevToken === undefined) delete process.env.DASHBOARD_API_TOKEN; +else process.env.DASHBOARD_API_TOKEN = prevToken; +const authedBody = await authed.json(); +cell( + "AUTH", + unauth.status === 401 && authed.status === 200 && authedBody.killed === false, + `unauth=${unauth.status} authed=${authed.status}`, +); // Audit forge await company.audit.append("a", { n: 1 }); @@ -173,9 +207,13 @@ const { resolveProvider } = await import("@corpos/core"); const resolved = resolveProvider(process.env); cell("HEALTH", resolved.mode === "simulation", "no live without allow"); -// OTel spans emitted on gateway -void listSpans(); -cell("OTEL", true, "span buffer available"); +// OTel — require recorded spans from prior gateway invokes +const spans = listSpans(); +cell( + "OTEL", + spans.some((s) => s.operation === "execute_tool" || s.operation === "invoke_agent"), + `spans=${spans.length}`, +); company.close(); console.log("adversarial: ok"); diff --git a/scripts/harness/adversarial.sh b/scripts/harness/adversarial.sh index eeedcad..abfadaf 100755 --- a/scripts/harness/adversarial.sh +++ b/scripts/harness/adversarial.sh @@ -2,4 +2,10 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" cd "$ROOT" -node scripts/adversarial-run.mjs +# Probe logic lives in digest-bound scripts/harness/adversarial-run.mjs +PROBE="./scripts/harness/adversarial-run.mjs" +if [[ ! -f "${PROBE}" ]]; then + echo "adversarial: missing ${PROBE}" >&2 + exit 1 +fi +node "${PROBE}" diff --git a/scripts/harness/check-stub-canary.sh b/scripts/harness/check-stub-canary.sh new file mode 100755 index 0000000..da6a3fb --- /dev/null +++ b/scripts/harness/check-stub-canary.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# DO_NOT_DELETE_STUB_CANARY — fail closed if harness verify is missing or trivial. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +VERIFY_HARNESS="scripts/harness/verify.sh" +VERIFY_WRAPPER="scripts/verify.sh" + +fail() { + echo "STUB_CANARY: $*" >&2 + exit 1 +} + +[[ -f "${VERIFY_HARNESS}" ]] || fail "missing ${VERIFY_HARNESS}" +[[ -x "${VERIFY_HARNESS}" ]] || fail "${VERIFY_HARNESS} is not executable" + +for candidate in "${VERIFY_HARNESS}" "${VERIFY_WRAPPER}"; do + if [[ -f "${candidate}" ]] && grep -Fq 'TODO: add real test' "${candidate}"; then + fail "placeholder verify in ${candidate}" + fi +done + +bytes="$(wc -c < "${VERIFY_HARNESS}" | tr -d ' ')" +if (( bytes < 1500 )); then + fail "${VERIFY_HARNESS} too small (${bytes} bytes); expected real DoD script" +fi + +# Reject a near-empty "exit 0" verify (no DoD stages). +line_count="$(grep -Ev '^[[:space:]]*(#|$)' "${VERIFY_HARNESS}" | wc -l | tr -d ' ')" +if (( line_count < 20 )); then + fail "${VERIFY_HARNESS} has too few non-comment lines (${line_count})" +fi + +for stage in 'run build' 'typecheck' 'run test' 'run lint' 'format:check'; do + if ! grep -Fq "${stage}" "${VERIFY_HARNESS}"; then + fail "${VERIFY_HARNESS} missing required stage: ${stage}" + fi +done + +echo "check-stub-canary: ok" diff --git a/scripts/harness/verify.sh b/scripts/harness/verify.sh index fd7b692..4880cd8 100755 --- a/scripts/harness/verify.sh +++ b/scripts/harness/verify.sh @@ -164,9 +164,13 @@ if pending: PY } -if [[ -x ./scripts/check-stub-canary.sh ]]; then - ./scripts/check-stub-canary.sh +# Mandatory stub canary (digest-bound companion under scripts/harness). +CANARY="./scripts/harness/check-stub-canary.sh" +if [[ ! -x "${CANARY}" ]]; then + echo "verify: missing executable ${CANARY}" >&2 + exit 1 fi +"${CANARY}" acquire_lock resolve_npm diff --git a/test/company-day.test.ts b/test/company-day.test.ts index 3a396ee..763c7e5 100644 --- a/test/company-day.test.ts +++ b/test/company-day.test.ts @@ -10,7 +10,10 @@ import { describe("company day", () => { it("runs multi-handoff day with autonomy, exception, compensation, sla, trust", async () => { - const { company, result } = await runCompanyDay({ dbPath: ":memory:" }); + const { company, result } = await runCompanyDay({ + dbPath: ":memory:", + autoApproveException: true, + }); expect(result.handoffs).toBeGreaterThanOrEqual(2); expect(result.autonomousSettles).toBeGreaterThanOrEqual(1); expect(result.exceptionSettles).toBeGreaterThanOrEqual(1); @@ -30,6 +33,14 @@ describe("company day", () => { company.close(); }); + it("does not auto-approve exceptions by default", async () => { + const { company, result } = await runCompanyDay({ dbPath: ":memory:" }); + expect(result.exceptionSettles).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); + company.close(); + }); + it("fail-closed unknown tool and kill switch", async () => { const company = await createCompany({ dbPath: ":memory:" }); const denied = await company.gateway.invoke(