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-oversight

JS/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.

FormatChannelExample
name@company.comEmailalice@acme.com
mailto:name@company.comEmail (explicit)mailto:alice@acme.com
sms:+15551234567SMS (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:

  1. The caller's explicit approvers=[...]
  2. The tenant's saved default_approvers
  3. The global DEFAULT_APPROVERS env var
  4. 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 it

Verify 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 digest
  • X-Sentinel-Eventapproval.approved or approval.rejected
  • X-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=50

11. Configuration

Env varDefaultDescription
SENTINEL_API_URLhttps://api.pauseapi.appBackend URL
SENTINEL_API_KEYrequiredYour tenant API key
SENTINEL_TIMEOUT300Default timeout (s)
SENTINEL_FALLBACKrejectreject or execute on timeout

Need help? Open an issue on GitHub or DM @PetrefiedThunder.