security
Intended Documentation
Fail-Closed Controls
How Intended behaves when a decision cannot be established — default-DENY, the 500 AUTHORITY_LOOP_FAILED fail-closed path, single-use authority tokens, and adapter-enforced "no token, no action".
Fail-Closed Controls#
Intended is fail-closed: when a decision cannot be established, the runtime denies rather than allows. There is no best-effort or fail-open path. This page enumerates the exact behaviors — at the decision engine, at token issuance, and at the enforcement boundary — that a reviewer can verify against the code.
Fail-closed vs. fail-open#
Most authorization layers fail open: if the policy service is unreachable, the request passes. That prioritizes availability over correctness. Intended takes the opposite stance at every layer.
| Scenario | Fail-open (typical) | Fail-closed (Intended) |
|---|---|---|
| No policy matches the intent | Allow (no deny rule) | Deny — absence of a matching allow is a deny |
| Authority evaluation throws | Allow with degraded logging | Deny — POST /intent returns 500 AUTHORITY_LOOP_FAILED |
| Token cannot be minted or signed | Allow with warning | Deny — no token is issued, so no action proceeds |
| Adapter cannot verify the token | Execute anyway | Deny — the adapter never runs the action |
| Token replayed or expired | Allow if signature parses | Deny — nonce already consumed / past expiry |
Info
Fail-closed means availability of downstream actions depends on the health of the authority path. This is an intentional trade-off: Intended prioritizes never authorizing an action it cannot justify over maximizing uptime of the systems it governs.
Default-DENY at the decision engine#
The modern policy engine (evaluateAuthority) is rule-based and resolves conflicts by most-severe-wins:
Two properties make this fail-closed:
- No match → DENY. If no rule matches the intent, the engine returns DENY. The absence of a permitting rule is never an implicit allow.
- Interpretation signals upgrade only. Signals from the interpretation layer (LIM) can only raise severity (e.g. nudge an ALLOW toward ESCALATE) — they can never downgrade a decision toward allow. A noisy or adversarial interpretation cannot weaken a deny.
Note
POST /intent reports the legacy decision vocabulary — APPROVED (200) / ESCALATED (202) / DENIED (403) — while the modern POST /authority/evaluate reports ALLOW / DENY / REQUIRE_APPROVAL / ESCALATE. Keep the two vocabularies distinct when handling responses.
The fail-closed path on /intent#
POST /intent maps outcomes to HTTP status deterministically. Crucially, any unexpected failure during evaluation produces a deny, not a pass-through:
| Outcome | HTTP | Notes |
|---|---|---|
| Approved and executed | 200 | Action ran; audit entries written |
| Escalated | 202 | Returns an escalationId / approvalRequestId |
| Denied | 403 | Explicit policy or default-deny |
| Approved but execution blocked | 403 | Decision allowed, enforcement still denied |
| Request invalid | 400 VALIDATION_ERROR | Malformed IntentRequest |
| Any failure in the authority loop | 500 AUTHORITY_LOOP_FAILED | Fail-closed: the action does not proceed |
A 500 AUTHORITY_LOOP_FAILED is a safe failure — the runtime could not complete the decision, so it refused to authorize. Treat it as "denied due to system fault," not as a transient you can retry into an allow.
"No token, no action" at the enforcement boundary#
Even after an APPROVED decision, execution is gated by an Authority Token that the executing adapter must verify. The token is an Ed25519 JWT with a mandatory kid, a 300-second TTL (default and hard maximum), and a single-use nonce.
The adapter validates the token before any side effect. Replay, expiry, and key-id mismatch each reject the action — fail-closed by construction.
BaseAdapter.execute() (in @intended/connector-sdk) always validates before doing anything, and never runs the action if validation fails. The validation sequence is:
Decode the kid
Read the kid from the token header. A token without a resolvable kid is rejected.
Resolve the tenant public key
Look up the tenant's public key by kid. Keys are per-tenant Ed25519; rotation is expressed through kid.
Verify the Ed25519 signature with kid pinning
Verify the signature against the resolved key. The verifier pins the expected kid, so a token signed under a different key is rejected.
Assert tenant, adapter, and target match
Confirm the token's tenantId, adapterId, and adapterTarget match this adapter. A token minted for a different tenant or adapter cannot be replayed here.
Assert decision is APPROVED and not expired
Reject anything other than decision === "APPROVED", and reject tokens past expiresAt (≤300 s after issue).
Consume the single-use nonce
Atomically consume the nonce against the DB-unique @@unique([tenantId, nonce]) constraint. A replayed token whose nonce is already consumed is rejected. Only now does the action execute.
Each of these is a fail-closed gate: if any step cannot pass, the adapter raises a validation error and the action does not run.
Token issuance is fail-closed too#
A token is only minted for an APPROVED decision. If signing cannot complete — for example the tenant signing key is unavailable — no token is produced, so no downstream adapter can be satisfied, and the action cannot proceed. There is no "unsigned but trusted" fallback.
No global fail-open override#
Danger
Intended deliberately provides no global fail-open switch. Such a switch would become the single most valuable target for any attacker. To loosen behavior, change policy through the audited policy pipeline — a permissive policy is reversible and leaves a record, whereas a runtime bypass would not.
To widen what is permitted, the supported path is to author and deploy a more permissive policy through the normal, audited pipeline (see Deploy a Policy). That change is versioned, attributable, and reversible — and every decision it produces is still recorded in the hash chain.
Related Resources#
- Enforcement Lineage — how every decision, including denials, is recorded and verifiable
- Decision & Token Model — the Ed25519 token structure and lifecycle
- Operational Readiness — verifying fail-closed behavior before going live
- Trust Model — the security model these controls enforce