|
| 1 | +# Predicate Authority User Manual |
| 2 | + |
| 3 | +This guide explains how to use `predicate-authority` in real projects with |
| 4 | +practical examples. It is written for application developers and platform |
| 5 | +operators who want deterministic, pre-execution authorization for AI agents. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## What is `predicate-authority`? |
| 10 | + |
| 11 | +`predicate-authority` is an authorization layer for AI agents. |
| 12 | + |
| 13 | +It checks risky actions **before execution** (fail-closed by default), issues |
| 14 | +short-lived mandates for allowed actions, and records proof events for audit. |
| 15 | + |
| 16 | +Core use cases: |
| 17 | + |
| 18 | +- protect outbound API calls, |
| 19 | +- protect tool invocation (MCP/function calls), |
| 20 | +- enforce policy invariants using state and verification evidence, |
| 21 | +- centralize revocation and operational controls through a sidecar. |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## Package overview |
| 26 | + |
| 27 | +- `predicate_contracts`: shared typed contracts and protocols. |
| 28 | +- `predicate_authority`: policy engine, guard, sidecar, identity bridge, |
| 29 | + revocation, proof ledger. |
| 30 | +- `predicate-authorityd`: optional local sidecar daemon (CLI service). |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +## Install |
| 35 | + |
| 36 | +```bash |
| 37 | +pip install predicate-contracts predicate-authority |
| 38 | +``` |
| 39 | + |
| 40 | +For local development from source: |
| 41 | + |
| 42 | +```bash |
| 43 | +cd /path/to/AgentIdentity |
| 44 | +pip install -e ./predicate_contracts |
| 45 | +pip install -e ./predicate_authority |
| 46 | +``` |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## Fastest local validation path (Day 1) |
| 51 | + |
| 52 | +Use this path if you want fast feedback with minimal setup. |
| 53 | + |
| 54 | +You do **not** need: |
| 55 | + |
| 56 | +- Entra ID / enterprise IdP setup, |
| 57 | +- two browser agents, |
| 58 | +- hosted control plane. |
| 59 | + |
| 60 | +### Step 1: run one local authorize/deny script |
| 61 | + |
| 62 | +- Use in-process `ActionGuard` with a tiny local policy. |
| 63 | +- Build one allowed request and one denied request. |
| 64 | +- Confirm deny reason is deterministic. |
| 65 | + |
| 66 | +### Step 2: simulate delegation with two Python scripts |
| 67 | + |
| 68 | +- Script A (root) requests a mandate with limited delegation depth. |
| 69 | +- Script B (worker) uses the received token and attempts delegated action. |
| 70 | +- Validate expected behavior for: |
| 71 | + - valid delegation, |
| 72 | + - over-depth delegation (must fail), |
| 73 | + - revoked root/intent (must fail). |
| 74 | + |
| 75 | +### Step 3: optional sidecar smoke test |
| 76 | + |
| 77 | +- Start `predicate-authorityd` in local mode. |
| 78 | +- Call `/status`, `/ledger/flush-now`, and `/ledger/dead-letter`. |
| 79 | +- Confirm operations safety endpoints work before enterprise integration. |
| 80 | + |
| 81 | +When this passes, add enterprise IdP (OIDC/Entra) and real web-agent E2E flows. |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Mental model |
| 86 | + |
| 87 | +1. Build an `ActionRequest` from current agent context. |
| 88 | +2. Call `ActionGuard.authorize(request)` (or sidecar equivalent). |
| 89 | +3. If allowed, execute action (with mandate attached if needed). |
| 90 | +4. If denied, stop action and handle deny reason. |
| 91 | +5. Emit/store proof events for governance. |
| 92 | + |
| 93 | +--- |
| 94 | + |
| 95 | +## Quick start (in-process guard) |
| 96 | + |
| 97 | +```python |
| 98 | +from predicate_authority import ActionGuard, InMemoryProofLedger, LocalMandateSigner, PolicyEngine |
| 99 | +from predicate_contracts import ( |
| 100 | + ActionRequest, |
| 101 | + ActionSpec, |
| 102 | + PolicyEffect, |
| 103 | + PolicyRule, |
| 104 | + PrincipalRef, |
| 105 | + StateEvidence, |
| 106 | + VerificationEvidence, |
| 107 | + VerificationSignal, |
| 108 | + VerificationStatus, |
| 109 | +) |
| 110 | + |
| 111 | +rules = ( |
| 112 | + PolicyRule( |
| 113 | + name="allow-orders-create", |
| 114 | + effect=PolicyEffect.ALLOW, |
| 115 | + principals=("agent:checkout",), |
| 116 | + actions=("http.post",), |
| 117 | + resources=("https://api.vendor.com/orders",), |
| 118 | + required_labels=("on_checkout_page",), |
| 119 | + ), |
| 120 | +) |
| 121 | + |
| 122 | +guard = ActionGuard( |
| 123 | + policy_engine=PolicyEngine(rules=rules), |
| 124 | + mandate_signer=LocalMandateSigner(secret_key="replace-with-strong-secret"), |
| 125 | + proof_ledger=InMemoryProofLedger(), |
| 126 | +) |
| 127 | + |
| 128 | +request = ActionRequest( |
| 129 | + principal=PrincipalRef(principal_id="agent:checkout", tenant_id="tenant-a"), |
| 130 | + action_spec=ActionSpec( |
| 131 | + action="http.post", |
| 132 | + resource="https://api.vendor.com/orders", |
| 133 | + intent="submit customer order", |
| 134 | + ), |
| 135 | + state_evidence=StateEvidence(source="sdk-python", state_hash="sha256:abc123"), |
| 136 | + verification_evidence=VerificationEvidence( |
| 137 | + signals=( |
| 138 | + VerificationSignal( |
| 139 | + label="on_checkout_page", |
| 140 | + status=VerificationStatus.PASSED, |
| 141 | + required=True, |
| 142 | + ), |
| 143 | + ) |
| 144 | + ), |
| 145 | +) |
| 146 | + |
| 147 | +decision = guard.authorize(request) |
| 148 | +if not decision.allowed: |
| 149 | + raise RuntimeError(f"Denied: {decision.reason.value}") |
| 150 | + |
| 151 | +print("Allowed. mandate_id=", decision.mandate.claims.mandate_id if decision.mandate else None) |
| 152 | +``` |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## Policy basics |
| 157 | + |
| 158 | +A policy rule matches: |
| 159 | + |
| 160 | +- principal (`agent:*`, `agent:checkout`, etc.), |
| 161 | +- action (`http.post`, `tool.execute`, `browser.click`), |
| 162 | +- resource (`https://...`, `mcp://...`, etc.), |
| 163 | +- optional required verification labels. |
| 164 | + |
| 165 | +If no allow rule matches, behavior is deny (`NO_MATCHING_POLICY`). |
| 166 | + |
| 167 | +Common deny reasons: |
| 168 | + |
| 169 | +- `NO_MATCHING_POLICY` |
| 170 | +- `EXPLICIT_DENY` |
| 171 | +- `MISSING_REQUIRED_VERIFICATION` |
| 172 | +- `INVALID_MANDATE` |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## Using the sidecar (`predicate-authorityd`) |
| 177 | + |
| 178 | +Start local sidecar with file policy: |
| 179 | + |
| 180 | +```bash |
| 181 | +PYTHONPATH=. predicate-authorityd \ |
| 182 | + --host 127.0.0.1 \ |
| 183 | + --port 8787 \ |
| 184 | + --mode local_only \ |
| 185 | + --policy-file examples/authorityd/policy.json |
| 186 | +``` |
| 187 | + |
| 188 | +Health and status: |
| 189 | + |
| 190 | +```bash |
| 191 | +curl -s http://127.0.0.1:8787/health | jq |
| 192 | +curl -s http://127.0.0.1:8787/status | jq |
| 193 | +``` |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +## Identity modes |
| 198 | + |
| 199 | +`predicate-authorityd` supports multiple identity bridge modes: |
| 200 | + |
| 201 | +- `local` (default): local deterministic bridge, |
| 202 | +- `local-idp`: local IdP-style signed token mode (offline/dev/air-gapped), |
| 203 | +- `oidc`: enterprise OIDC bridge, |
| 204 | +- `entra`: Microsoft Entra bridge. |
| 205 | + |
| 206 | +Example (`local-idp`): |
| 207 | + |
| 208 | +```bash |
| 209 | +export LOCAL_IDP_SIGNING_KEY="replace-with-strong-secret" |
| 210 | +predicate-authorityd \ |
| 211 | + --host 127.0.0.1 \ |
| 212 | + --port 8787 \ |
| 213 | + --mode local_only \ |
| 214 | + --policy-file examples/authorityd/policy.json \ |
| 215 | + --identity-mode local-idp \ |
| 216 | + --local-idp-issuer "http://localhost/predicate-local-idp" \ |
| 217 | + --local-idp-audience "api://predicate-authority" |
| 218 | +``` |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +## Local identity registry + flush queue |
| 223 | + |
| 224 | +Enable ephemeral task identity registry and local ledger queue: |
| 225 | + |
| 226 | +```bash |
| 227 | +PYTHONPATH=. predicate-authorityd \ |
| 228 | + --host 127.0.0.1 \ |
| 229 | + --port 8787 \ |
| 230 | + --mode local_only \ |
| 231 | + --policy-file examples/authorityd/policy.json \ |
| 232 | + --local-identity-enabled \ |
| 233 | + --local-identity-registry-file ./.predicate-authorityd/local-identities.json \ |
| 234 | + --local-identity-default-ttl-s 900 \ |
| 235 | + --flush-worker-enabled \ |
| 236 | + --flush-worker-interval-s 2.0 \ |
| 237 | + --flush-worker-max-batch-size 50 \ |
| 238 | + --flush-worker-dead-letter-max-attempts 5 |
| 239 | +``` |
| 240 | + |
| 241 | +Useful endpoints: |
| 242 | + |
| 243 | +- `POST /identity/task` |
| 244 | +- `GET /identity/list` |
| 245 | +- `POST /identity/revoke` |
| 246 | +- `GET /ledger/flush-queue` |
| 247 | +- `POST /ledger/flush-now` |
| 248 | +- `GET /ledger/dead-letter` |
| 249 | +- `POST /ledger/requeue` |
| 250 | + |
| 251 | +--- |
| 252 | + |
| 253 | +## Operations safety patterns |
| 254 | + |
| 255 | +Recommended production defaults: |
| 256 | + |
| 257 | +- keep fail-closed for protected actions, |
| 258 | +- use dead-letter threshold to quarantine persistent failures, |
| 259 | +- expose `/status` metrics to monitoring, |
| 260 | +- provide runbooks for manual flush and dead-letter requeue. |
| 261 | + |
| 262 | +Example manual recovery: |
| 263 | + |
| 264 | +```bash |
| 265 | +# trigger immediate flush |
| 266 | +curl -s -X POST http://127.0.0.1:8787/ledger/flush-now \ |
| 267 | + -H "Content-Type: application/json" \ |
| 268 | + -d '{"max_items":50}' | jq |
| 269 | + |
| 270 | +# inspect quarantined items |
| 271 | +curl -s http://127.0.0.1:8787/ledger/dead-letter | jq |
| 272 | + |
| 273 | +# requeue a quarantined item |
| 274 | +curl -s -X POST http://127.0.0.1:8787/ledger/requeue \ |
| 275 | + -H "Content-Type: application/json" \ |
| 276 | + -d '{"queue_item_id":"q_abc123"}' | jq |
| 277 | +``` |
| 278 | + |
| 279 | +--- |
| 280 | + |
| 281 | +## `sdk-python` integration example (boundary adapter flow) |
| 282 | + |
| 283 | +If your web agent uses `sdk-python`, build shared contract evidence before |
| 284 | +calling authority: |
| 285 | + |
| 286 | +```python |
| 287 | +from predicate.agent_runtime import AgentRuntime |
| 288 | + |
| 289 | +# after snapshot + assertions |
| 290 | +request = runtime.build_authority_action_request( |
| 291 | + principal_id="agent:web-checkout", |
| 292 | + action="browser.click", |
| 293 | + resource="https://example.com/checkout", |
| 294 | + intent="click submit order", |
| 295 | + tenant_id="tenant-a", |
| 296 | +) |
| 297 | + |
| 298 | +# send request to your authority hook/client |
| 299 | +decision = my_authorizer(request) |
| 300 | +if not decision.allowed: |
| 301 | + raise RuntimeError("Denied by authority") |
| 302 | +``` |
| 303 | + |
| 304 | +--- |
| 305 | + |
| 306 | +## Troubleshooting |
| 307 | + |
| 308 | +- Denied with `MISSING_REQUIRED_VERIFICATION`: |
| 309 | + - ensure required labels are present and `PASSED` in evidence. |
| 310 | +- Denied with `NO_MATCHING_POLICY`: |
| 311 | + - verify principal/action/resource match patterns in active policy. |
| 312 | +- Token exchange errors in connected mode: |
| 313 | + - verify identity mode config and credential/refresh-token setup. |
| 314 | +- Queue not draining: |
| 315 | + - check `/status` flush counters and control-plane connectivity. |
| 316 | + |
| 317 | +--- |
| 318 | + |
| 319 | +## Where to go next |
| 320 | + |
| 321 | +- Operations guide: `docs/authorityd-operations.md` |
| 322 | +- Architecture proposal: `docs/predicate_authority_docs/better-sdk-opportunity-proposal.md` |
| 323 | +- Governance sign-off tracker: `docs/predicate_authority_docs/governance-signoff-tracker.md` |
0 commit comments