Approvals, receipts, and verification
This page is for operators and anyone deciding whether to trust agent work in Company OS: when work pauses for a human, what evidence an action leaves, and how to check that evidence yourself.
Related pages: CLI reference, connecting agents, Console guide.
In the commands below, companyos is the CLI binary. The CLI's own usage text prints the same commands as company-os. Every command accepts --company-id ID to name the company explicitly.
1. Why approvals exist
Some actions are costly or hard to undo, so Company OS pauses them until a person decides. A fluent agent answer is not proof that an action was allowed or that it worked.
Which actions pause
The production write policy (PRODUCTION_READINESS_POLICIES.md) states the default: agents work in dry-run shadow mode. They may inspect, plan, and validate, but they may not perform production writes until a workflow has an explicit mutation policy and an evidence trail.
Human approval is required for actions that are non-reversible, high-value, customer-facing, or permission-changing. These are blocked until an explicit policy exists: delete, bulk update, external payment or purchase, customer email send, credential or permission change, and cross-tenant data move.
Other pause points in the sources:
- Agent-proposed email replies stay
pending_approvalfor a separate human verifier; Company OS does not send them (services/company-os-mcp-server/README.md). - Learned autonomy (
HITL_AND_SKILL_PROMOTION_MOAT_CONTRACT.md): a missing or incomplete decision shape always pauses. A learnedautopreference cannot authorize a different resource, scope, value band, policy revision, or a higher risk band.legacy-unboundpreference rows never authorize autonomous proceed.
Who may decide
company_agents.roleis a job title; onlyownerandadmincarry authority (src/company_os/governance_doc.rs).- Some actions are member-reserved: only an active human owner may take them; an admin gets
403 member_reserved_owner_requiredand a refusal receipt. They include granting owner or admin, changing an owner, changing a model agent'sadapter_typeor model config, granting a root delegation, minting a token that acts as another owner, lifting a malfunction flag, and deciding an approval whoserequest_payload.required_authorityis"owner"(MEMBER_RESERVED_AUTHORITY_AND_AGENT_AUTO_SUSPEND.md). That document says no approval producer declares that value yet. - "Active human owner": an active
ownercompany_agentsrow with no model adapter, or an activeownercompany_human_membershipsrow. - An approval cannot be used if it was decided by its own requester (
approvalBindingProbleminservices/company-os-mcp-server/src/client.ts).
What is not approval
- A model-supplied
confirm=trueis not human approval (services/company-os-mcp-server/README.md). --confirmon a CLI command is not an approval by itself. API principal, idempotency, spend, settlement, and Gate checks still apply (CLI usage text insrc/cli.ts).- MCP
strictmode (the default) returnsapproval_requiredfor mutating tools unless the caller passesconfirm=trueafter explicit operator approval.permissiveis for trusted local automation only. The remote Grok bridge always uses strict. - In
boundapproval mode (defined inclient.ts, not described in the package README), a mutating call must carry one approval id that isapproved, has a deciding principal, was not decided by its requester, is unconsumed, and is unexpired. The id is cleared after one use. A model-driven MCP session cannot decide approvals in this mode; a human uses the Console or CLI. - The desktop app does not manufacture approval locally (
COMPANY_OS_START_HERE.md).
2. Lifecycle of an approval
requested -> shown -> decided (approved | rejected) -> executed, or not- Requested. Policy pauses the work. An approval row is created with risk domain, risk level, action type, subject,
requested_by, optionalexpires_at, and arequest_payload. - Shown. It appears in the company's inbox. Rows with a decision envelope carry subject, presentation, and request digests plus a
presentation_revision, describing what the approver was shown. - Decided. A person approves or rejects;
decided_byanddecided_atare recorded. - Executed or not. In
boundmode an approved, unconsumed, unexpired approval authorizes one action. A rejected, expired, or consumed one cannot.
Digest-bound decisions
For a row with a decision envelope, the CLI reads the authoritative inbox first, then submits your decision together with the server-stored subject_digest, presentation_digest, request_digest, and presentation_revision. It never accepts a caller-invented approval projection. If the stored envelope is incomplete, the command fails rather than guessing.
legacy_unverifiable applies to approval rows, not receipts. It classifies rows created before the decision envelope existed. Their decision_contract reports verification_supported: false because no digests exist to bind a decision to; Company OS cannot prove what the approver was shown. The CLI sends digests only for rows with an envelope, so a decision on such a row is not digest-bound by the CLI. Whether the server accepts it is not stated in the sources.
Where to decide
| Place | How |
|---|---|
| Console | Open Approvals (see the Console guide). |
| CLI | companyos approval approve ID --confirm or companyos approval reject ID --confirm |
| Tern palette | "Open Company OS approvals waiting on you" only lists pending approvals; decide in the Console or CLI (integrations/tern-companyos/README.md). |
CLI commands
# List the inbox (filter by state, page with --limit and --offset)
companyos approvals --status pending
# Inspect one approval before deciding
companyos approval show APPROVAL_ID
# Decide (all three forms require --confirm)
companyos approval approve APPROVAL_ID --reason "why" --confirm
companyos approval reject APPROVAL_ID --reason "why" --confirm
companyos approval decide APPROVAL_ID --decision approved|rejected --reason "why" --confirm--reason is optional. --dry-run runs the real handler path, including the authoritative reads, and prints the planned request instead of sending it; no --confirm is needed. Run approval show first: the decision binds to the digests the server stored, not to what you remember seeing.
3. Receipts
A receipt records what happened during important work, read from the company ledger and scoped to the authenticated company. Refusals are receipted too, never silent (src/company_os/governance_doc.rs).
Receipt id families
Ids are family-qualified so receipt show resolves the exact tenant-scoped row.
| Prefix | Meaning | Verifiable |
|---|---|---|
terminal: | Terminal receipt for a task or run outcome. Carries sequence, terminal_generation, previous_hash, receipt_hash. | Yes |
run_event: | Run execution event. | No |
governance: | Governance event, such as an approval decision or a refusal. | No |
spend: | Spend event. | No |
The list code also emits review_delivery:; the package README names only the four above.
List, show, verify
companyos receipt list
companyos receipt show terminal:RECEIPT_UUID
companyos receipt verify terminal:RECEIPT_UUID
companyos verify task TASK_ID
companyos verify run RUN_IDWhat receipt verify checks
For a terminal: receipt, the server recomputes the database-owned canonical hash and checks both chain linkage and the stored company head. The response (hsm.company_os.receipt_verification.v1) includes:
verified: true only ifhash_matches(stored equals recomputed hash),previous_link_matches(previous_hashequals the expected previous hash), andcompany_chain.validall hold.evidence:sequence,stored_hash,computed_hash,previous_hash,expected_previous_hash, andcompany_chain(valid,checked_count,first_bad_sequence,expected_head_hash).
What verification_supported=false means
Timeline families without a digest contract (run_event:, governance:, spend:) return verification_supported=false. They are readable evidence, but they never claim to be verified. The MCP presentation layer reports this as not_available ("This result family does not expose a verification contract"). not_available is not a failure and it is not a pass.
A successful tool call is never treated as verification. The result envelope (hsm.company_os.result.v1) says not_available unless the canonical payload explicitly reports verification.
verify task and verify run
These replay the immutable lifecycle history and compare it with the live projection, using the same authenticated company principal.
- Task responses (
hsm.company_os.task_verification.v1) includeverified,has_drift,drift_kind, andaggregate_versionsfor state, metadata, context, operational, and run snapshot, plus redacted evidence for each. - Run responses (
hsm.company_os.run_verification.v1) includeverified,has_drift,drift_kind,aggregate_version, andevidence.lifecycle. verifiedis!has_drift. Missing history or a replay failure is never reported as verified.
4. Auto-suspension and safety rails for agents
Source: MEMBER_RESERVED_AUTHORITY_AND_AGENT_AUTO_SUSPEND.md. Rules live in src/company_os/agent_oversight.rs.
A model-driven principal is suspended, fail closed, while either condition holds:
oversight_vacant: the company has no active human owner.agent_malfunction_flagged: an unlifted row incompany_agent_malfunction_flagsnames the agent.Suspension is derived on every read. It is never written into agent status. A read failure refuses.
Human principals are never suspended, so an owner can always act.
Every refusal is
423 Lockedwithcode: "agent_suspended"and thereason_codeabove.Enforcement points and their receipts:
| Check point | Receipt |
|---|---|
| Worker dispatch | task_events.dispatch_refused |
| Tool calls inside a run (live read for the run's agent) | task-trail tool_denied |
| Principal-token mutations | governance_events.company_mutation_refused |
Signed run-capability authorize | governance_events.run_capability_action_refused |
- Settlement of an already-authorized run-capability action stays open, so its outcome is still recorded.
Flagging and lifting:
GET /api/company/companies/:company_id/agents/:agent_id/suspension
POST /api/company/companies/:company_id/agents/:agent_id/suspension {"reason": "..."}
POST /api/company/companies/:company_id/agents/:agent_id/suspension/lift {"reason": "..."}- Any active human owner or admin may flag a model-backed agent. Repeating the flag while one is open is idempotent. Flags cannot be raised on human rows.
- Only an owner may lift. A lifted flag is final; a new incident is a new flag.
- Flag rows are evidence: the database refuses any rewrite except the single lift, and the runtime role has no
DELETE.
Related rails:
- Delegation enforcement is on by default; a live principal token is not authority without a scoped delegation from an owner or admin (
src/company_os/governance_doc.rs). - With no active human owner, an active human admin may install a human (adapter-less) agent as owner, and nothing else. With no usable owner or admin credential, use the existing root-transport owner recovery.
5. FAQ
Is --confirm the same as approving? No. --confirm acknowledges a mutating CLI command, and the other server checks still apply. To decide an approval, use companyos approval approve|reject ID --confirm as an authenticated principal.
Can an agent approve its own request? Not through the checked paths. A model-supplied confirm=true is not human approval; in bound mode a model-driven MCP session cannot decide approvals; an approval decided by its requester cannot authorize an action; and required_authority: "owner" approvals refuse an admin.
A receipt says verification_supported=false. Is something wrong? No. That family (run_event:, governance:, spend:) has no digest contract. Use the terminal: receipt, verify task, or verify run instead.
receipt verify returned verified: false. What now? Read evidence: hash_matches, previous_link_matches, and company_chain.first_bad_sequence show which check failed. The sources do not describe a repair procedure.
Why was my agent refused with 423?code: "agent_suspended": either oversight_vacant (no active human owner) or agent_malfunction_flagged. Restore an owner, or have an owner lift the flag.