PROMPT governance
The PROMPT action suspends an in-flight AI request and holds it for explicit admin review. The request does not proceed to the model provider until an admin approves it, or until the hold expires. This page describes the hold lifecycle, the admin API, SSE event format, and configuration options.
Overview
Section titled “Overview”When a policy rule fires with action=PROMPT, the requesting client is suspended — it blocks waiting for a decision. The proxy does not forward the request to the AI provider until an admin approves it. If the admin denies it, or if the hold expires without a decision, the client receives an HTTP 403 error as though the request had been blocked outright.
When to use PROMPT instead of BLOCK.
Use BLOCK when a policy match should always be rejected with no exceptions. Use PROMPT when the appropriate outcome depends on context that only a human reviewer can evaluate — for example, a request that matches a sensitive-data pattern but may be legitimately authorized by someone in the user’s management chain. PROMPT is most useful for interactive callers (browser/SSE); it is generally not appropriate for automated pipelines where a blocked response is expected to be handled programmatically rather than waited on by a human. The channel rule condition can be used to restrict PROMPT rules to "interactive" callers only.
Admin experience.
When a hold is created, all connected admin clients subscribed to the SSE stream (GET /admin/api/prompt-holds/events) receive a real-time event containing the hold ID and request context. The admin reviews the context, then calls the approve or deny endpoint. The suspended client immediately resumes or receives a 403. The hold and its decision are written to the audit log.
How the hold flow works
Section titled “How the hold flow works”- The ProxyRouter evaluates the DLP pipeline and policy engine for the incoming request.
- A rule fires with
action=PROMPT. PromptHoldStore.create_hold()is called, which creates aPromptHoldrecord with a UUID, a UNIX timestamp, and the request context (model, matched rule, and related metadata). Anasyncio.Eventis associated with the hold.- The new hold is broadcast to all admin clients subscribed to the SSE stream via
GET /admin/api/prompt-holds/eventsas aprompt_holdevent. - The ProxyRouter awaits the
asyncio.Event. The request is suspended until the event is set or the timeout elapses. - An admin calls
POST /admin/api/prompt-holds/{hold_id}/approveorPOST /admin/api/prompt-holds/{hold_id}/deny. - The store sets the
decisionfield on the hold, recordsresolved_at, and fires the event. - The ProxyRouter unblocks and acts on the decision:
- approve — the request is forwarded to the AI provider normally and the response is returned to the client.
- deny — the client receives HTTP 403. No request is made to the provider.
- timeout (hold age exceeds
PROMPT_HOLD_TIMEOUT_SECONDS) — the hold is automatically resolved as denied. The client receives HTTP 403.
If PromptHoldStore is not configured, the proxy defaults to denying any request that matches a PROMPT rule, treating it as though the hold was immediately denied.
Admin API reference
Section titled “Admin API reference”All /admin/api/ routes are served on port 8301 (the admin application) and require admin authentication via the _verify_admin_auth dependency.
| Method | Path | Description |
|---|---|---|
GET | /admin/api/prompt-holds | List all holds, both pending and resolved. Returns {"holds": [...], "pending_count": N}. |
POST | /admin/api/prompt-holds/{hold_id}/approve | Approve the specified hold. Returns {"hold_id": "...", "decision": "approve"}. Returns 404 if the hold does not exist or has already been resolved. |
POST | /admin/api/prompt-holds/{hold_id}/deny | Deny the specified hold. Returns {"hold_id": "...", "decision": "deny"}. Returns 404 if the hold does not exist or has already been resolved. |
GET | /admin/api/prompt-holds/events | Server-sent events stream. On connect, sends all currently pending holds as initial events. Subsequently delivers real-time events as holds are created, resolved, or timed out. |
PromptHold object fields
Section titled “PromptHold object fields”| Field | Type | Description |
|---|---|---|
hold_id | string (UUID) | Unique identifier for the hold. |
created_at | float | UNIX timestamp when the hold was created. |
context | object | Request context provided to the admin for review: model, matched rule, and related metadata. |
decision | string | null | "approve", "deny", or null if not yet resolved. |
resolved_at | float | null | UNIX timestamp when the hold was resolved. null if still pending. |
pending | boolean | true if the hold has not yet been resolved. |
SSE event format
Section titled “SSE event format”The /admin/api/prompt-holds/events endpoint delivers three event types as JSON objects.
| Event type | When it fires | Fields |
|---|---|---|
prompt_hold | A new hold has been created | type, hold_id, context |
prompt_hold_resolved | An admin approved or denied a hold | type, hold_id, decision |
prompt_hold_timeout | A hold expired without a decision | type, hold_id, timeout_seconds |
Example event payloads
Section titled “Example event payloads”New hold created:
{ "type": "prompt_hold", "hold_id": "a3f2c1d4-7e88-4b12-9f01-2c3d4e5f6a7b", "context": { "model": "gpt-4o", "matched_rule": "finance-sensitive-data-review", "user": "alice@example.com", "entity_types": ["credit_card"] }}Admin decision recorded:
{ "type": "prompt_hold_resolved", "hold_id": "a3f2c1d4-7e88-4b12-9f01-2c3d4e5f6a7b", "decision": "approve"}Hold timed out:
{ "type": "prompt_hold_timeout", "hold_id": "a3f2c1d4-7e88-4b12-9f01-2c3d4e5f6a7b", "timeout_seconds": 300}On initial connection to the SSE stream, the server replays all currently pending holds as prompt_hold events before switching to real-time delivery. This allows a reconnecting admin client to recover the full current queue without missing holds that were created while the client was disconnected.
Approving and denying holds
Section titled “Approving and denying holds”Use the admin API directly to approve or deny a hold. All requests must include admin credentials.
List pending holds:
curl -s \ -u admin:${ARBITEX_ADMIN_PASSWORD} \ http://localhost:8301/admin/api/prompt-holds \ | jq '.holds[] | select(.pending)'Approve a hold:
curl -s -X POST \ -u admin:${ARBITEX_ADMIN_PASSWORD} \ http://localhost:8301/admin/api/prompt-holds/a3f2c1d4-7e88-4b12-9f01-2c3d4e5f6a7b/approveDeny a hold:
curl -s -X POST \ -u admin:${ARBITEX_ADMIN_PASSWORD} \ http://localhost:8301/admin/api/prompt-holds/a3f2c1d4-7e88-4b12-9f01-2c3d4e5f6a7b/denySubscribe to the SSE stream:
curl -s \ -u admin:${ARBITEX_ADMIN_PASSWORD} \ -H "Accept: text/event-stream" \ http://localhost:8301/admin/api/prompt-holds/eventsAttempting to approve or deny a hold that has already been resolved (or does not exist) returns HTTP 404. You can confirm the current state of a hold by calling GET /admin/api/prompt-holds and inspecting the pending field.
Timeout behavior
Section titled “Timeout behavior”PROMPT_HOLD_TIMEOUT_SECONDS controls how long the proxy waits for an admin decision before automatically resolving the hold as denied. The default is 300 seconds (5 minutes).
When a hold times out:
- The proxy unblocks and returns HTTP 403 to the requesting client.
- The hold’s
decisionis set to"deny"andresolved_atis recorded. - A
prompt_hold_timeoutevent is sent to all connected SSE clients. - The timeout resolution is written to the audit log.
To adjust the timeout, set PROMPT_HOLD_TIMEOUT_SECONDS in the Outpost environment:
PROMPT_HOLD_TIMEOUT_SECONDS=600 # 10 minutesSetting this value too high increases the window during which a client is blocked waiting for a decision. If no admin is available to act within the configured window, clients will receive 403 responses after the timeout elapses — the same outcome as an explicit deny. Consider your operational staffing when choosing this value.
Audit logging
Section titled “Audit logging”Both approve and deny actions are written to the audit log via audit_logger.log_request. The log entry includes the following fields:
| Field | Value |
|---|---|
action | "prompt_hold_approve" or "prompt_hold_deny" |
hold_id | UUID of the resolved hold |
admin_user | Identity of the admin who took the action, as resolved from the authenticated request |
Timeout-triggered resolutions are also logged with action="prompt_hold_timeout".
Audit log entries are queryable through the standard audit log interface. See Audit log for query instructions and field reference.
Setting up PROMPT rules
Section titled “Setting up PROMPT rules”PROMPT is a terminal rule action configured in the policy engine. A rule with action=PROMPT fires when all of its conditions match, suspends the request, and initiates the hold workflow described on this page.
Example rule that requires admin approval for any request from the trading-desk group that the DLP pipeline flags with a credit_card entity:
{ "name": "trading-desk-credit-card-review", "conditions": { "user_groups": ["trading-desk"], "entity_types": ["credit_card"], "entity_confidence_min": 0.85, "channel": ["interactive"] }, "action": { "type": "PROMPT" }}The channel: ["interactive"] condition restricts this rule to browser and SSE callers. Automated API callers that match the same conditions will not receive a PROMPT hold — they will fall through to the next rule. If you want automated callers to be blocked instead, add a separate BLOCK rule with channel: ["api"] before this rule in the evaluation chain.
For the complete list of rule conditions, action types, and evaluation order, see Policy rule reference.
See also
Section titled “See also”- Policy rule reference — all rule actions including
ALLOW,BLOCK,CANCEL,REDACT,ROUTE_TO, andPROMPT; condition fields; evaluation order - DLP pipeline — technical reference — how entity detection and confidence scoring work before the policy engine evaluates conditions