GenAI Approval

Interactive approval scripts for agents — a GenAI-ecosystem service.

An agent submits a small, deliberately non-Turing-complete script: an endpoint preamble (credential references, never secrets), typed variables, steps with conditional logic, and declared outputs. A human then drives the run call-by-call — step / next / run-all / edit / halt — with breakpoints, per-step permission gating, and scoped allow/block command rules ("allow this session", "allow for the next hour", "block always"). The structured result (per-step outcomes, notes, edit diffs, outputs, halt reason, grants) returns to the calling agent.

Design/PRD: project-management/PRDs/genai-interactive-approval-scripts.md in the Noizu master repo.

Documentation

DocContents
docs/arch/overview.mdComponent map, supervision, security invariants, milestone status
docs/arch/script-format.mdThe script language: sections, grammar, hard rules, error codes, AST
docs/arch/runner.mdRun state machine, commands, budgets, events, result contract
docs/arch/permissions.mdRule model, normative resolution order, stores, runner gating
docs/arch/ui.mdRendering model, Host behaviour, LiveView reference UI, escaping
docs/arch/mcp.mdMCP executor, submit_approval_script tool, SubmitHost behaviour

Status

M1 — grammar + engine ✅

M2 — session surface + LiveView reference UI ✅

Embed it with:

live_render(conn, GenAI.Approval.Live.RunView, session: %{"run_id" => run_id})

M3 — durable permission store ✅

M4 — MCP integration ✅ (optional noizu_mcp ~> 0.1 dep)

Next: elicitation fallback for plain-MCP operator hosts, noizu_mcp server-side permission backstop, M5 Hologram UI + com.noizu/approval-scripts extension draft.

Quick taste

source = """
{{#endpoint "local"}} transport = "local" {{/endpoint}}
{{#vars}} n : number = 21 {{/vars}}
{{#step "Double it" confirm="really double"}}
{{assign result = call("local", "math.double", n=n)}}
{{/step}}
{{#outputs}} answer = result.value {{/outputs}}
"""
{:ok, script} = GenAI.Approval.load(source)
{:ok, run} =
GenAI.Approval.start_run(script,
executors: %{
"local" => {GenAI.Approval.Executor.Local,
%{"math.double" => fn %{"n" => n}, _ctx -> {:ok, %{"value" => n * 2}} end}}
},
subscriber: self(),
permission: [store: {GenAI.Approval.Permission.Store.ETS, GenAI.Approval.PermissionStore},
subject: "keith", session_id: "sess-1"]
)
:ok = GenAI.Approval.command(run, :run_all)
# confirm step parks the run:
# {:genai_approval, _, %{type: :permission_required, confirm: "really double"}}
:ok = GenAI.Approval.command(run, :approve)
{:ok, %{status: :completed, outputs: %{"answer" => 42}}} = GenAI.Approval.await(run)

Tests

mix test # 111 tests: parser golden/rejection/property, runner state machine,
# budgets, failure paths, permission matrix, store, runner gating,
# LiveView interaction + escaping, DETS durability, MCP executor + submit tool