Approval Workflow¶
The approval gate is SynthOrg's human-in-the-loop control surface: certain actions (deploy to production, rotate a secret, kill a runaway agent) pause until an authorised operator approves. The gate lives at synthorg.engine.approval_gate and integrates with the audit chain so every decision is signed and chained.
Concepts¶
- Escalation: a structured request that an action requires approval. Carries
approval_id,tool_name,action_type,risk_level, and areason(EscalationInfoinsynthorg.approval.models);agent_idandtask_idare supplied separately when the agent parks. - Parked context: the agent state frozen while an escalation is pending (
ParkedContext, serialised via the park service). - Approval item: the REST-visible queue entry (
ApprovalIteminsynthorg.core.approval), withstatus,risk_level,decided_by, anddecision_reason. - Approval verdict:
approved/rejected, with an actor identity, timestamp, and reason.
Configuration¶
There is no dedicated approval.* settings namespace; the gate is governed by the
security namespace and by the security policy document:
| Key | Type | Default | Purpose |
|---|---|---|---|
security.enabled |
bool | true |
Master switch for the security subsystem, including the approval gate. |
security.audit_enabled |
bool | true |
Whether security audit entries (including approval decisions) are recorded. |
security.timeout_check_interval_seconds |
float | 60.0 |
How often the approval-timeout scheduler scans pending approvals and applies the timeout policy. |
observability.audit_chain_signing_timeout_seconds |
float | 5.0 |
Timeout for signing and timestamping an audit-chain entry. |
Who may approve or reject is not a configurable role list: the REST endpoints are
guarded to the CEO, Manager, and Board Member human roles (require_approval_roles in
synthorg.api.guards).
Two independent timeout mechanisms apply:
- Item expiry: an approval item may carry its own
ttl_secondsat creation (60 s to 7 days); an item created without one never expires on its own. A read pastexpires_atlazily moves the item toexpiredand logsapi.approval.expired. - The timeout scheduler: polls PENDING items on
security.timeout_check_interval_secondsand applies a configurable timeout policy: wait indefinitely, deny outright, or a per-risk-tier policy that can approve, deny, or escalate through a chain (auto-approval is always overridden to deny forHIGHandCRITICALrisk). A policy resolution moves the item straight toapprovedorrejected, attributed to the timeout-policy system actor rather than a human reviewer.
The active policy is part of the security policy document, viewed and edited via
GET/POST /api/v1/settings/security/export and /import or the dashboard's Security
settings page, not a flat approval.* key.
Worked example: a manual approval round-trip¶
The agent emits a pre-tool escalation:
from synthorg.approval.state import ApprovalStateSlice
from synthorg.engine.approval_gate import ApprovalGate
gate: ApprovalGate = app_state.slice(ApprovalStateSlice).gate
parked = await gate.park_context(
escalation=escalation,
context=task_context,
agent_id="agent-007",
task_id="123e4567-e89b-12d3-a456-426614174000",
)
print(parked.id, parked.approval_id)
The dashboard at /approvals lists pending requests. The reviewer clicks Approve; the
API persists a verdict via the dedicated /approve endpoint (optional comment, plus a
chosen_option_id when the escalation offered options) or /reject endpoint (mandatory
reason). Both endpoints require a caller-supplied Idempotency-Key header, so a retried
request returns the original decision rather than deciding twice:
curl -s -b cookies.txt -X POST http://localhost:3001/api/v1/approvals/7c9e6679-7425-40de-944b-e07fc1f90ae7/approve \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f6a1e2a-9c3b-4b2e-8f2b-9a2e6b1c7d10" \
--data '{"comment": "Looks good; canary signal is clean."}'
The gate unparks the context and resumes the agent loop. The observability stream records:
api.approval.createdat park time (one row per pending approval).security.approval.approved/security.approval.rejectedat verdict time, with the reviewer identity.approval.status_transitionedimmediately after the persistence write.
security.*-prefixed events (including the two approval verdict events above) are picked
up automatically by the audit-chain sink and signed into the hash chain; see
Audit chain below.
Operator surface¶
The approvals page surfaces pending requests with:
- Action type, agent, task, requested change.
- Time-since-raised urgency badge, with thresholds set by risk level.
- One-click
Approve/Rejectactions, plus an option picker when the escalation offered a structured choice; rejection requires a reason. - Filters on
status,risk_level,action_type, andsource.
For terminal automation, the MCP approvals domain exposes approve and reject tools
that take the same approval id. Both are destructive, admin-guardrailed tools: each
requires confirm: true and a non-blank reason, refused at the schema level if either
is missing. Neither tool can answer a question an agent parked for a human; that goes
through the chat question surface instead.
Observability¶
api.approval.created(info): one per pending approval row written to the store.security.approval.approved/security.approval.rejected(info): one per verdict.approval.status_transitioned(info): after the persistence write, withfrom_statusandto_status; also emitted for a timeout-scheduler resolution.api.approval.expired(warning): emitted when a read finds an item past its ownttl_seconds.
The synthorg_approval_decisions_total counter has a bounded outcome label in
VALID_APPROVAL_OUTCOMES: approved, rejected, expired.
Audit chain¶
security.*-prefixed log events, including both approval verdict events, are captured by
AuditChainSink (synthorg.observability.audit_chain), a logging handler that signs each
one and appends it to a hash chain: each entry's signature and hash are bound to the prior
entry, so a tampered entry breaks the chain from that point on. A trusted timestamp is
requested per entry where a TSA is configured; the append still proceeds on a TSA failure,
recorded as a fallback rather than a signed append. Entries are hydrated from durable
storage at startup so verification survives a restart.
Operators query the recorded security audit trail (filterable by agent_id, tool_name, action_type, verdict):
Threat model¶
The approval gate's reliance on identity-aware reviewers means the surrounding auth surface MUST be tight:
- Session validation at the controller.
- Approval-role membership (
require_approval_roles) checked AT decide time, not just at session start. security.audit_enabledleft on in production (a disabled audit log silently loses tamper evidence for the events the chain would otherwise capture).
See docs/reference/sec-prompt-safety.md for the redaction rules around the rationale payload.