Sentinel docs
One wrapper. Your function pauses, a human gets an email with Approve / Reject buttons, and the function only runs once they click Approve.
1. Install
Python:
pip install sentinel-oversightJS/TS SDK — coming soon. The TypeScript package is not published to npm yet; the examples below show the planned API.
2. Quick start
Grab an API key from pauseapi.app/signup, then wrap any function:
# Python
from sentinel import configure, oversight
configure(api_key="sk_live_...")
@oversight(risk_level="high", approvers=["alice@acme.com"])
def transfer_funds(amount: int, recipient: str):
return stripe.transfers.create(amount=amount, destination=recipient)
transfer_funds(50_000_00, "acct_xyz") # pauses until approved// TypeScript
import { configure, oversight } from 'sentinel-oversight';
configure({ apiKey: process.env.SENTINEL_API_KEY! });
const wireTransfer = oversight(
{ riskLevel: 'high', approvers: ['alice@acme.com'] },
async (amountCents: number, recipient: string) => {
return stripe.transfers.create({ amount: amountCents, destination: recipient });
}
);
await wireTransfer(50_000_00, 'acct_acme_corp');When the agent calls this: Sentinel pauses execution, an email goes to alice@acme.com with Approve / Reject buttons, and on approve the function runs with its original arguments — return value flows back.
3. Approvers
Each entry in approvers=[...] is a string. The format determines the notification channel.
| Format | Channel | Example |
|---|---|---|
| name@company.com | alice@acme.com | |
| mailto:name@company.com | Email (explicit) | mailto:alice@acme.com |
| sms:+15551234567 | SMS (requires consent) | sms:+14155550123 |
Mix formats — every approver receives a notification, the first decision wins.
SMS approvers must be added in the dashboard with active consent before they can receive approval texts. See the SMS consent flow.
4. Risk levels
risk_level is a string the dashboard uses for prioritization. Allowed values: low, medium, high, critical. Required.
5. Errors
# Python
from sentinel import (
ApprovalRejected, ApprovalTimeout,
SentinelAPIError, SentinelConfigError,
)
try:
transfer_funds(50_000_00, "acct_xyz")
except ApprovalRejected as e:
log.info(f"rejected: {e.reason}")
except ApprovalTimeout as e:
log.warn(f"no decision in {e.timeout_seconds}s")
except SentinelAPIError as e:
log.error(f"API {e.status_code}: {e.message}")// TypeScript
import {
ApprovalRejected, ApprovalTimeout,
SentinelAPIError, SentinelConfigError,
} from 'sentinel-oversight';
try {
await wireTransfer(50_000_00, 'acct_xyz');
} catch (e) {
if (e instanceof ApprovalRejected) console.log('rejected:', e.reason);
if (e instanceof ApprovalTimeout) console.log('no decision:', e.actionId);
if (e instanceof SentinelAPIError) console.log('API err:', e.statusCode);
}6. Async functions
The decorator transparently supports async def (Python) and async functions (TypeScript). No additional config — just wrap as usual.
7. LangChain
Sentinel ships a LangChain callback handler for both Python and TypeScript. Drop it into a callbacks=[...] array and every tool call pauses for approval.
# Python
from sentinel.adapters.langchain import SentinelCallbackHandler
agent.run("...", callbacks=[SentinelCallbackHandler(risk_level="high")])// TypeScript
import { SentinelCallbackHandler } from 'sentinel-oversight/langchain';
await agent.invoke(
{ input: 'send Alice $50,000' },
{
callbacks: [
new SentinelCallbackHandler({
riskLevel: 'high',
approvers: ['alice@acme.com'],
toolAllowlist: ['wire_transfer', 'delete_database'],
}),
],
}
);8. Tenant settings (default approvers)
If you don't want to pass approvers=[...] on every decorator, set a default on your tenant:
from sentinel import SentinelClient
client = SentinelClient()
client.set_default_approvers([
"sms:+15551234567",
"ops@yourcompany.com",
])Resolution order when an approval is created:
- The caller's explicit
approvers=[...] - The tenant's saved
default_approvers - The global
DEFAULT_APPROVERSenv var - 400 error if all three are empty
9. Audit log
Every approval creates a hash-chained audit trail. Fetch it:
from sentinel import SentinelClient
client = SentinelClient()
events = client.list_audit_events(action_id="act_...")
# each event has prev_hash + event_hash (SHA-256)Chain integrity can be verified by recomputing sha256(prev_hash + json(payload)) for each event.
10. Webhooks
Register a URL on your tenant and Sentinel will POST to it whenever an approval is decided (approved or rejected). Use this to mirror events into your own audit system, ping Slack, fan out to other services, etc.
Register a new endpoint:
curl -X POST https://api.pauseapi.app/v1/webhooks \
-H "Authorization: Bearer $SENTINEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.yourcompany.com/sentinel-webhook",
"description": "audit fan-out",
"event_filter": ["approval.approved", "approval.rejected"]
}'
# returns: { id, url, secret: "whsec_...", ... }
# the secret is shown ONCE — store itVerify the signature on your side (every delivery has X-Sentinel-Signature header):
# Python
import hashlib, hmac
def is_authentic(request) -> bool:
raw_body = request.body # bytes — read BEFORE parsing JSON
sent_sig = request.headers.get("X-Sentinel-Signature", "")
expected = hmac.new(
WEBHOOK_SECRET.encode(),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, sent_sig)// TypeScript / Node
import { createHmac, timingSafeEqual } from 'node:crypto';
function isAuthentic(req: Request): boolean {
const sig = req.headers.get('x-sentinel-signature') ?? '';
const expected = createHmac('sha256', WEBHOOK_SECRET)
.update(req.rawBody) // bytes, BEFORE parsing JSON
.digest('hex');
return sig.length === expected.length &&
timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}Headers on every delivery:
X-Sentinel-Signature— HMAC-SHA256 hex digestX-Sentinel-Event—approval.approvedorapproval.rejectedX-Sentinel-Delivery— unique id per attempt
Retry policy: up to 3 attempts (1s, 4s, 16s backoff) on 5xx and network errors. 4xx responses are treated as terminal rejections — we won't retry. Inspect attempts:
GET /v1/webhooks/deliveries?endpoint_id=whk_...&limit=5011. Configuration
| Env var | Default | Description |
|---|---|---|
| SENTINEL_API_URL | https://api.pauseapi.app | Backend URL |
| SENTINEL_API_KEY | required | Your tenant API key |
| SENTINEL_TIMEOUT | 300 | Default timeout (s) |
| SENTINEL_FALLBACK | reject | reject or execute on timeout |
Need help? Open an issue on GitHub or DM @PetrefiedThunder.