Skip to content

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 "no token, no action" enforced where your service verifies the token.

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.

ScenarioFail-open (typical)Fail-closed (Intended)
No policy matches the intentAllow (no deny rule)Deny — absence of a matching allow is a deny
Authority evaluation throwsAllow with degraded loggingDeny — POST /intent returns 500 AUTHORITY_LOOP_FAILED
Token cannot be minted or signedAllow with warningDeny — no token is issued, so no action proceeds
Your service cannot verify the tokenAct anywayDeny — the service refuses the action
Token replayed or expiredAllow if signature parsesDeny — 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:

text
DENY  >  REQUIRE_APPROVAL  >  ESCALATE  >  ALLOW

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:

OutcomeHTTPNotes
Approved200Token issued; audit entries written; your agent may act
Escalated202Returns an escalationId / approvalRequestId
Denied403Explicit policy or default-deny
Request invalid400 VALIDATION_ERRORMalformed IntentRequest
Any failure in the authority loop500 AUTHORITY_LOOP_FAILEDFail-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#

Intended decides and signs; it never runs the action, holds your third-party keys or calls your systems. Even after an APPROVED decision, the action is gated by an Authority Token that the service your agent calls (or a gateway you run) 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.

Token verification is the enforcement boundary
An Ed25519 authority token travels with the agent's request to the customer's service; the service resolves the kid to the tenant public key, checks the Ed25519 signature, tenant and target match, and expiry, then atomically consumes the single-use nonce before it acts. Replay, expiry, and kid-mismatch all reject.

Your service validates the token before any side effect. Replay, expiry, and key-id mismatch each reject the action — fail-closed by construction.

Your service should validate before doing anything, and never act if validation fails. @intended/verify implements the signature and claim checks; your service adds nonce consumption. 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 and target match

Confirm the token's tenantId, adapterId, and adapterTarget match what this service expects. A token minted for a different tenant or target 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 record the nonce, unique per tenant (a database unique constraint on (tenantId, nonce) works well). A replayed token whose nonce is already consumed is rejected. Only now does the service act.

Each of these is a fail-closed gate: if any step cannot pass, the service rejects the request 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 verifying service 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.

Fail-Closed Controls | Intended