Edictum
Edictum Control Plane

Human-in-the-Loop Approvals

How `action: ask` maps to the shipped approval records, HTTP endpoints, and Telegram webhook callback route in edictum-api.

AI Assistance

Right page if: you need the shipped approval lifecycle in edictum-api -- create, poll, decide, list, and timeout behavior. Wrong page if: you want to author a ruleset with `action: ask` -- see https://docs.edictum.ai/docs/rulesets/yaml-reference. Gotcha: the current edictum-api server stores approvals, exposes HTTP endpoints, and ships a Telegram webhook callback route. Do not claim Slack or Discord interaction routes unless you verify they exist.

When a rule uses action: ask, the SDK pauses the tool call and hands off to an approval backend. In the shipped edictum-api server, that means storing an approval record and exposing it over HTTP.

This repo gives you the approval state machine and persistence layer:

  • POST /v1/approvals creates a pending approval
  • GET /v1/approvals/{id} returns the current state
  • POST /v1/approvals/{id}/decide resolves it as approved or rejected
  • GET /v1/approvals lists approvals with basic filters

The API surface is the source of truth here. A dashboard or operator tool can sit on top of these endpoints, but the server behavior comes from edictum-api.

Lifecycle

SDK hits POST /v1/approvals
    |
    v
Server stores approval with status=pending
    |
    +-- Agent or operator polls GET /v1/approvals/{id}
    |
    +-- Human or automation resolves via POST /v1/approvals/{id}/decide
    |
    +-- Background sweeper marks stale approvals timed_out

The approvals service also publishes best-effort bus messages:

  • approvals.new
  • approvals.decided

The store is still authoritative. If bus delivery fails, the approval record remains correct in the database.

States

Approvals use four persisted states:

StateMeaning
pendingWaiting for a decision
approvedResolved by a human or operator
rejectedExplicitly rejected by a human or operator
timed_outExpired without a human decision

Transitions are one-way. Once an approval is resolved, another decide call returns a conflict instead of reopening it.

Timeout Behavior

Each approval stores:

  • timeout
  • timeout_action

timeout_action is either:

  • block
  • allow

The important detail is that the stored approval status still becomes timed_out. timeout_action controls what the calling guard does with the tool call after the timeout, not the terminal approval state itself.

The sweeper runs on APPROVAL_SWEEP_EVERY, which defaults to 1m in the current server config.

API Flow

Create a pending approval:

POST /v1/approvals
Authorization: Bearer edk_...
Content-Type: application/json
{
  "agent_id": "mimi",
  "session_id": "sess-1",
  "tool_name": "bash",
  "tool_args": { "cmd": "rm -rf /tmp/nope" },
  "message": "Need approval before running this command",
  "rule_name": "dangerous-command",
  "timeout": 300,
  "timeout_action": "block"
}

Response:

{
  "id": "1c1a1cb7-4a4d-43e7-a9f7-c8432f8cf1f7",
  "status": "pending"
}

Poll the record:

GET /v1/approvals/1c1a1cb7-4a4d-43e7-a9f7-c8432f8cf1f7
Authorization: Bearer edk_...

Resolve it:

POST /v1/approvals/1c1a1cb7-4a4d-43e7-a9f7-c8432f8cf1f7/decide
Authorization: Bearer edk_...
Content-Type: application/json
{
  "decision": "approved",
  "decided_by": "arnold",
  "decided_via": "api",
  "reason": "looks safe"
}

Telegram approval callbacks are handled by the shipped webhook route:

POST /v1/telegram/webhook/{id}

That route is the current interactive callback path for Telegram-based approvals. Do not generalize it to Slack or Discord unless those routes are actually shipped.

Current shipped decision values for POST /decide are:

  • approved
  • rejected

timed_out is reserved for the sweeper and is not a valid human decision payload.

Stored Fields

These are the approval fields the server persists and returns today:

FieldNotes
idPublic UUID generated by the server
agent_idAgent identifier
session_idOptional session lineage identifier
tool_nameTool name
tool_argsTool arguments
messageHuman approval prompt
rule_nameRule that triggered the approval
statuspending, approved, rejected, or timed_out
timeoutTimeout in seconds
timeout_actionblock or allow
decided_byResolver identity
decided_atDecision timestamp
decided_viaResolution channel label supplied by the caller, for example api
decision_reasonOptional decision reason
created_atCreation timestamp

Listing Approvals

GET /v1/approvals supports these filters:

  • status
  • agent_id
  • session_id
  • limit
  • offset

Example:

GET /v1/approvals?status=pending&agent_id=mimi&session_id=sess-1
Authorization: Bearer edk_...

The server returns:

{
  "approvals": [
    {
      "id": "1c1a1cb7-4a4d-43e7-a9f7-c8432f8cf1f7",
      "agent_id": "mimi",
      "session_id": "sess-1",
      "tool_name": "bash",
      "tool_args": { "cmd": "rm -rf /tmp/nope" },
      "message": "Need approval before running this command",
      "rule_name": "dangerous-command",
      "status": "pending",
      "timeout": 300,
      "timeout_action": "block",
      "created_at": "2026-04-06T12:00:00Z"
    }
  ]
}

What This Repo Does Not Ship

This repo does not currently expose:

  • Slack interaction endpoints
  • Discord interaction endpoints
  • approval-specific SSE endpoints
  • approval creation rate limiting

If you need those, document them only after the routes and service behavior exist in edictum-api.

Next Steps

Last updated on

On this page