← Building a solo company with AI agents

Part 16 of 40 · 7 min read

Risk to control: the same rule must apply in every loop

Experimental · Work in progress · Internal R&D

Apply shared controls across loops, separate requesting and approving actors, and test the limits of the current policy boundary.

Arunank Sharan · Kuzushi Labs ·

In this article

If the sales agent needs approval to offer a discount but the support agent can issue the same amount as a credit without a check, the company has not controlled the commitment. It has only moved it to another route. Risk-to-Control gives the business one place to define and inspect such boundaries.

The prototype includes controls, approval requests, separation-of-duties checks and cross-loop approval routing. It is a shared capability used by other loops, not a final step after their work is complete.

A control needs an owner and evidence

A useful control describes the action being protected, the condition to enforce, its owner, how it is tested and the evidence of the result. “Payments are reviewed” is too vague to implement. “A new beneficiary requires a separately recorded approval before the payment action can execute” identifies an actual boundary.

The local control lifecycle models design, testing and effectiveness. The action dispatcher can compose several policies, with denials taking precedence. This lets a purchasing action combine role checks, spend authority and a separation rule without trusting a prompt to remember all three.

Follow an approval across two loops

The Operate-to-Improve loop can propose giving an agent more autonomy. It creates a held action and passes the decision through the approval router. A human approval can resume the action; rejection leaves the increased authority inactive.

Improvement loop proposes promotion
  → dispatcher requires approval
  → review task and approval request are recorded
  → authorised reviewer inspects evidence
  → decision recorded
  → held action resumes or is denied

This local round trip is implemented and tested. A live deployment still needs the authenticated identity, stale-state checks and atomic resume protections described in the action article.

Two agents are not two independent people

The separation-of-duties policy can deny an action when the current actor matches the prior actor. That is useful for preventing an agent from approving its own proposal. In the current code, however, the prior actor can come from stored properties or request arguments. A production implementation should derive it from authoritative history and authenticated identity.

For a solo founder, some decisions have one real human owner. I would record that reality rather than manufacture independence by giving the same person two role names. The policy may keep the action manual, require an external reviewer, or allow a documented founder override for a bounded class of decision. The chosen rule should be visible in the audit record.

Denied work still produces information

An audit entry should include the requested action, actor, tenant, target version, policy version, result and relevant evidence references. A refusal without its reason creates support work. A broad “allowed” record without the facts used to allow it cannot explain a later failure.

The prototype emits audit records for allow, deny and escalation. The richer operational design needs durable storage, tenant-filtered access and retention rules. It also needs to preserve the policy explanation consistently; some current records are thinner than the decision object they accompany.

Test policy with a decision table

Here is a small proposed table for quote dispatch:

cases:
  - name: approved_version
    quote_version: 3
    approved_version: 3
    expected: allow
  - name: changed_scope
    quote_version: 4
    approved_version: 3
    expected: require_approval
  - name: wrong_tenant
    request_tenant: studio-a
    object_tenant: studio-b
    expected: deny
  - name: expired_approval
    approval_expired: true
    expected: require_approval

I would run these cases before integrating any reasoning model. The platform's gateway already includes an OPA policy layer and policy tests, while several business-loop compositions use in-process policies. Connecting those layers consistently is work still to do. OPA's test runner provides a way to exercise policy rules independently of the application. OPA policy testing.

The founder's control surface

The daily view should prioritise consequential and ageing decisions: a credit beyond the agreed threshold, a payment with changed beneficiary data, an agent requesting expanded authority, or a delivery that lacks acceptance evidence. Each should include what will happen if approved and what remains blocked if rejected.

The objective is predictable delegation. I can give an agent a broader job when the business rules remain understandable and the exception queue remains manageable. A proliferation of policy checks that no one can explain would fail that objective just as surely as uncontrolled automation.

Evaluate authority from trusted context

A request saying role=founder is not proof of authority. The application authenticates the actor, resolves tenant membership and derives the principal used by the dispatcher. The requested business arguments remain untrusted input. That separation should hold for browser requests, scheduled jobs, connector callbacks and model tool calls.

The reference actor-separation rule is small:

from company_lab.business import separate_approver

assert not separate_approver("sales-agent", "sales-agent", {"approver"})
assert separate_approver("sales-agent", "founder", {"approver"})
assert not separate_approver("sales-agent", "viewer", {"viewer"})

These are trusted fixtures, not an identity provider. The live application must establish who owns each identity and which permissions are current. Two different display names controlled by the same automated principal do not create meaningful separation of duties.

Make the policy input complete and reproducible

A policy decision needs actor, tenant, action, target kind, current target state, requested amount or scope, prior actors, relevant limits and policy version. If a precondition such as “invoice matched” arrives as a caller-supplied string, the caller can make the policy evaluate the wrong fact. The dispatcher should load the authoritative match record itself.

A practical decision receipt records the normalised input hash, rule version, allow/deny/review outcome and reason codes. Do not store secrets in that record. The operator needs enough information to reproduce the decision and understand the consequence of a policy update.

{
  "action": "approve_campaign_budget",
  "tenant": "studio",
  "requester": "marketing-agent",
  "reviewer": "founder",
  "target_version": 5,
  "policy_version": "campaign-budget-v2",
  "decision": "REQUIRES_REVIEW",
  "reasons": ["new_spend_above_delegated_limit"]
}

Authenticated identity and current object facts become a versioned policy decision; agents cannot supply their own authority.

Enforce tenant scope below the prompt

The context builder should only retrieve records the principal may read. Every write must use the same trusted tenant. In PostgreSQL, row security policies can restrict reads and writes, but privileged roles can bypass them; the application role and table ownership therefore matter. PostgreSQL row security.

The database policy is one layer. Provider connections, object-storage keys, caches, search indexes and audit queries also need tenant scope. A perfectly filtered object table does not help if a global audit endpoint returns another company's payment details.

For one founder running several businesses, cross-company reuse should be explicit. A generic outreach template can be shared; a customer's agreement should not enter another company's prompt. The permission model needs to express that difference without relying on the model to remember which brand it is representing.

Test policy changes as migrations

A new limit can affect queued actions, held approvals and running episodes. Decide whether existing approvals remain valid, expire immediately or require review against the new version. The choice should be encoded and auditable. A worker that resumes tomorrow must not silently apply yesterday's permission because it cached a tool list.

I would test each policy with allowed, denied and review-required cases, plus missing fields and unknown prior actors. Treat missing evidence according to an explicit rule. The current prototype has permissive paths that are useful for local development; live routes should not inherit them accidentally.

Use a stop control that reaches the actual effect boundary

A stop flag in the UI is insufficient if workers already hold pending sends. The effect executor should check the current tenant and connector stop state immediately before the operation. Already-confirmed effects remain confirmed; queued work stops; uncertain effects go to reconciliation.

The exercise is to revoke a role after approval but before execution. The send must remain unsent even though the old approval exists. Then replay a completed operation after revocation: the system should report its historical receipt without treating that read as permission for a new effect. This distinction makes the operator's controls both safe and understandable.