soma.cell

Human-in-the-loop approval gate guarding an irreversible action

Demonstrates: approve() as a lifecycle gate, a state machine where the "executing" path is only reachable past the gate, and a rejected path that lands in a distinct terminal. approve() never answers on its own: a test scripts it with `mock approve true|false`; `soma run` asks the person at the terminal; under `soma serve` (nobody attached) it raises kind "approval_required" unless SOMA_APPROVE=always|never states an explicit policy. Run: soma run approval_gate.cell deploy "v2.1.0" (asks y/N) SOMA_APPROVE=never soma run approval_gate.cell deploy "v2.1.0" Test: soma test approval_gate.cell

A Soma program from the agents part of the verified corpus: 70 lines, cells Deployer, DeployTests. It passes soma check with soma 3.2.4.

How it is built

The cell Deployer declares 1 signal in its face: deploy(version: String) -> Map; keeps 1 memory slot: history (List<String>, persistent). Its state machine pipeline starts in idle and allows 6 transitions: idle -> planning, planning -> awaiting, awaiting -> executing, awaiting -> rejected, executing -> done and * -> failed. done, failed and rejected are terminal. Handlers: deploy (line 34).

The file also holds 1 test cell (DeployTests) with 4 assertions, run by soma test.

Generated from soma describe on this file.

The source

soma.toml

[package]
name = "agents-corpus"
version = "0.1.0"
description = "Verified AI-agent example cells (training corpus)"

# Agent LLM config. `mock = "echo"` keeps every example in this directory
# hermetic: `soma test` / `soma run` need no network and no API key, and
# results are deterministic. For a live run, delete the `mock` line (or
# override with SOMA_LLM_MOCK) and provide the key via env.
[agent]
provider = "anthropic"
model    = "claude-opus-4-6"
key      = "${ANTHROPIC_API_KEY}"
mock     = "echo"

# Termination proof for verified_termination.cell's `workflow` machine:
# every path eventually reaches a terminal, no deadlocks.
# `cells` scopes these properties to Solver — the other agents in this
# directory have their own state names and must not inherit them.
[verify]
cells = ["Solver"]
deadlock_free = true
eventually    = ["done", "failed"]

[verify.after.understanding]
eventually = ["solving", "failed"]

[verify.after.solving]
eventually = ["verifying", "failed"]

[verify.after.verifying]
eventually = ["done", "failed"]

approval_gate.cell

// Human-in-the-loop approval gate guarding an irreversible action.
// Demonstrates: approve() as a lifecycle gate, a state machine where the
// "executing" path is only reachable past the gate, and a rejected path
// that lands in a distinct terminal.
//
// approve() never answers on its own: a test scripts it with
// `mock approve true|false`; `soma run` asks the person at the terminal;
// under `soma serve` (nobody attached) it raises kind "approval_required"
// unless SOMA_APPROVE=always|never states an explicit policy.
//
// Run:  soma run approval_gate.cell deploy "v2.1.0"          (asks y/N)
//       SOMA_APPROVE=never soma run approval_gate.cell deploy "v2.1.0"
// Test: soma test approval_gate.cell

cell agent Deployer {
    face {
        signal deploy(version: String) -> Map
    }

    memory {
        history: List<String> [persistent]
    }

    state pipeline {
        initial: idle
        idle      -> planning
        planning  -> awaiting
        awaiting  -> executing
        awaiting  -> rejected
        executing -> done
        *         -> failed except [done, rejected]
    }

    on deploy(version: String) {
        set_budget(1500)
        let id = to_string(next_id())
        transition(id, "planning")
        transition(id, "awaiting")

        let approved = approve("Deploy {version} to production?")
        if !approved {
            transition(id, "rejected")
            return map("status", "rejected", "version", version)
        }

        transition(id, "executing")
        history.push(version)
        transition(id, "done")
        map("status", "done", "version", version, "count", len(history))
    }
}

cell test DeployTests {
    rules {
        mock approve true
        assert deploy("v1").status == "done"

        mock approve false
        assert deploy("v2").status == "rejected"

        // nobody answers: the gate fails closed and the handler rolls back
        assert_fails deploy("v3") matching "approval_required"

        // one answer per call: only v1 and v4 were ever deployed
        mock approve true
        let d4 = deploy("v4")
        assert d4.count == 2
    }
}

What the toolchain says

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ soma check approval_gate.cell
✓ All checks passed.
$ soma test approval_gate.cell
test DeployTests ...
  ✓ assert deploy("v1").status == "done"
  ✓ assert deploy("v2").status == "rejected"
  ✓ assert_fails deploy("v3") matching "approval_required" — raised approval_required: approve("Deploy v3 to production?") has no one to answer it — script it in the test: `mock approve true` / `mock approve false` before the call
  ✓ assert d4.count == 2

4 tests: 4 passed, 0 failed
$ soma verify approval_gate.cell
Verifying approval_gate.cell...
note: soma.toml [verify] cells names 'Solver', which this file does not define — its properties are not checked here

State machine 'pipeline': 7 states, initial 'idle'
  States: [awaiting, done, executing, failed, idle, planning, rejected]

  ✓ 7 states, 9 transitions
  ✓ all states reachable from 'idle'
  ✓ terminal states: [rejected, done, failed]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ✓ wildcard transitions: * -> [failed]
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (1 handlers, 5 literal transitions, 0 dynamic)
  ✓ termination: all 1 handlers structurally terminate
  ⚠ refinement: declared transition `* → failed` is never reached by any handler — spec may be aspirational or stale
  ✓ refinement: handler `deploy` ⟶ {planning, planning → awaiting, rejected [if !approved], executing, executing → done}
  · transition sources: `deploy` calls transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)

9 passed, 1 warnings, 0 failures

  Temporal properties for 'pipeline':
  ✓ deadlock_free — no deadlocks in any reachable state
soma.toml: 4 user-defined properties loaded — NONE applies to this file ([verify] cells = ["Solver"] names no cell of it)
Temporal: 1 passed, 0 failed
VERIFY OK

Builtins used

approve, len, map, next_id, set_budget, to_string, transition

Source file: /corpus/agents/approval_gate.cell · examples/corpus/agents/approval_gate.cell