Skip to content

Commit d91c282

Browse files
committed
Add policy engine and human-in-the-loop approvals
Sensitive actions now stop for a human. Every tool call runs through the policy engine, which allows it, denies it, or holds it for approval. Workspaces can set their own rules per tool - always ask, auto approve, or never allow. When an action needs approval the run pauses, keeping a snapshot of where it was, and an entry shows up in the approval inbox with the exact tool and arguments. Approving runs the action and lets the agent carry on from where it paused; rejecting sends it back so the agent can pick something else. Approvals can expire and can be limited to a role, and only people with approve rights can decide. Adds the approval model and migration, the inbox endpoints, and writes every policy decision to the audit log.
1 parent 1c94440 commit d91c282

19 files changed

Lines changed: 1060 additions & 89 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ RUN_MAX_WALLCLOCK_SEC=600
4949
AGENT_RUN_INLINE=false
5050
# When true, document ingestion runs inline instead of via a Celery task (tests/dev).
5151
KB_INGEST_INLINE=false
52+
# How long a pending approval stays valid before it auto-expires (0 = never).
53+
APPROVAL_TTL_HOURS=24

PROGRESS.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Tracks build progress against the phases in `LEGATE_AGENT_BUILD_PLAN.md` (Sectio
1111
| 2 | LLM layer and Agent Core | ✅ Complete |
1212
| 3 | RAG knowledge layer | ✅ Complete |
1313
| 4 | Connector framework and credential vault | ✅ Complete |
14-
| 5 | Policy Engine and approvals | ⬜ Not started |
14+
| 5 | Policy Engine and approvals | ✅ Complete |
1515
| 6 | Workflow engine and triggers | ⬜ Not started |
1616
| 7 | Dashboard UX | ⬜ Not started |
1717
| 8 | Enterprise hardening | ⬜ Not started |
@@ -106,9 +106,26 @@ Tracks build progress against the phases in `LEGATE_AGENT_BUILD_PLAN.md` (Sectio
106106
- One credential per connector (1:1); multi-account per type and per-connector tool disambiguation are future work.
107107
- Webhook, Stripe, and Database connectors are deferred to later phases.
108108

109+
## Phase 5 — Policy Engine and approvals ✅
110+
111+
**Done**
112+
- Full Policy Engine (`legate/policy/engine.py`): `allow` / `deny` / `require_approval`, with per-workspace approval rules (`auto_approve` / `deny` lists) read from `workspace.settings_json`.
113+
- `Approval` model (+ `ApprovalStatus` enum) and Alembic migration.
114+
- Suspend/resume in the runner: on `require_approval` it snapshots the transcript into an `Approval`, sets the run to `waiting_approval`, and stops; `AgentRunner.resume` rebuilds the transcript and continues — executing the action on approve, or feeding a rejection observation back to replan on reject.
115+
- Approvals inbox API: `GET /approvals`, `GET /approvals/{id}`, `POST /approvals/{id}/approve|reject`. Deciding requires `approvals:decide`; resume dispatches inline (tests) or in the background (prod).
116+
- Expiry (`APPROVAL_TTL_HOURS`) with auto-expire on read/decide; optional per-approval role gate; `409` on expired or already-decided approvals.
117+
- Every policy decision audited (`policy.deny` / `policy.require_approval` / `policy.auto_approve`) plus `approval.approve` / `approval.reject`.
118+
119+
**Acceptance:** an agent attempting a sensitive action is paused; an authorized approver approves it from the inbox and the action then executes; rejection resumes the run so the agent replans; an unauthorized user cannot approve; all decisions are audited. Verified by policy-engine unit tests and a full approval-flow suite (suspend / approve+execute / reject+replan / RBAC / expiry / double-decide / auto-approve / tenant isolation / audit), plus a 28/28 live-server smoke run.
120+
121+
**Decisions / TODOs**
122+
- Reject resumes the run so the agent can replan (rather than hard-stopping); a "reject-and-cancel" option can be added later.
123+
- Approval expiry is evaluated lazily (on read/decide); a Celery-beat sweep for proactive expiry is a later addition.
124+
- `required_role` is stored and enforced when set; the UI to set it arrives with the dashboard.
125+
109126
---
110127

111-
## Next: Phase 5Policy Engine and approvals
112-
Full allow/deny/require_approval rules, `Approval` model + inbox API, suspend/resume
113-
around approvals, per-workspace approval rules, expiry and role-gated approvals, and
114-
a full audit of every policy decision.
128+
## Next: Phase 6Workflow engine and triggers
129+
Workflow model + graph executor, node executors (trigger/agent/tool/condition/
130+
transform/approval/delay), sandboxed `{{ }}` templating, manual/schedule/signed-webhook
131+
triggers, pause/resume for approval nodes, and per-node retry with backoff.

apps/api/legate/agent/enums.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,16 @@ class StepType(str, Enum):
3030
TRANSFORM = "transform"
3131
APPROVAL = "approval"
3232
TRIGGER = "trigger"
33+
34+
35+
class ApprovalStatus(str, Enum):
36+
PENDING = "pending"
37+
APPROVED = "approved"
38+
REJECTED = "rejected"
39+
EXPIRED = "expired"
40+
41+
42+
#: Approval statuses that are final.
43+
DECIDED_APPROVAL_STATUSES: frozenset[ApprovalStatus] = frozenset(
44+
{ApprovalStatus.APPROVED, ApprovalStatus.REJECTED, ApprovalStatus.EXPIRED}
45+
)

0 commit comments

Comments
 (0)