Documentation

Threat Model

How Duct enforces safety between humans, agents, peer shells, and your upstream APIs — and what integrators must still verify on their side.

Operator reference

This page summarizes the current platform enforcement model for integrators and operators.

Actors and trust boundaries

End user
Trusted for their own session; approves side-effects in the shell UI.
Your backend
Trusted by you — holds DUCT_SECRET_KEY and user sessions.
Agent (at_)
Untrusted automation — cannot act as a user without a verified consent token.
Peer shell
Untrusted cross-tenant caller — subject to intershell gates and delegation.
Duct gate
Primary policy enforcement point before your upstream APIs.
Intent router / LLM
Untrusted computation — deterministic guards apply before tool execution.

Your API must verify Bearer tokens independently. X-Duct-Proxy is a hint, not proof of identity.

Token model

shell_token
Shell identity for agents/services — typically 1 hour TTL.
duct_delegated
User session delegated to the shell. Default TTL 60s; doc examples often use 300s. Always verify with expectedShellId.
uct_
User consent for agent-scoped actions. Verified whenever any gate relied on consent.
dt_
Cross-shell delegation — 60s TTL, single-use per jti (IS-36).
Deeplink JWT
Browser handoff — verify server-side; enforce jti replay protection in production.

Confirmations (side-effects)

Side-effect actions return ask from the permission gate. Agents receive 202 confirmation_required with an approval_url. After approval, retry with the issued execution_token.

Operator session (default). DUCT_APPROVAL_MODE=operator_session — a signed-in dashboard operator who owns the shell approves via the confirmation page or dashboard JSON API. This proves an operator account session, not end-user presence.

Link holder (opt-in). DUCT_APPROVAL_MODE=link_holder — approval uses a browser-bound httpOnly approval-nonce cookie. The approval_url is opaque (no nonce in the URL or status poll). This proves channel possession only — optional CAPTCHA can be enabled. CLI API keys do not qualify as operator approval credentials.

Audit records which factor class gated each decision (operator_session, link_holder, webauthn, policy_only). Duct does not infer a verified human from HTML or cookies alone.

Approval trust by caller type

How approval is proven depends on caller profile and deployment mode. Integrators requiring provable human-in-the-loop must choose the right case — not all paths guarantee an accountable human reviewed the action.

Case 1 — Human-delegated
Shell embed with user consent. In-chat ConfirmCard → user or operator approves in the shell UI → execution_token retry. Human or operator in the approval UI path; bound to session/shell context.
Case 2 — Operator session
Default mode. Signed-in dashboard operator with shell ownership approves via /confirmations/:id. Operator account session; does not assert end-user presence.
Case 3 — Link holder
link_holder mode. Browser-bound approval-nonce cookie on the approval URL. Channel possession only; optional CAPTCHA.
Case 4 — Computer-use agents
Planned webauthn transaction-bound approval. Until shipped, treat as Case 3 at best — no trusted display or possession factor enforced in production.
Case 5 — Policy-only
Regulated or policy-cap workflows. Policy gate without an independent human factor — does_not_assert_human_factor.
Case 6 — Agent-only
Independent agent caller with no delegated user context. Side effects still require confirmation, but no operator, end-user, or possession factor is bound to the approving party. No human-in-the-loop guarantee.

Case 6 — explicit non-guarantee

When the caller is an autonomous agent with no delegated user context and no enforced possession factor, Duct still blocks silent mutation (consent + confirmation tokens), but cannot guarantee that a specific accountable human reviewed the action. Do not rely on agent-only confirmation polling alone for provable HITL — use Cases 1–2 (or future Case 4 WebAuthn).

Cross-shell calls

Cross-shell agent traffic must use the intershell API (/v1/intershell/invoke, /v1/intershell/message, and related endpoints). Direct POST /v1/shells/:id/invoke or POST /v1/shells/:id/message where the caller shell differs from the target returns 403 use_intershell_endpoint. Same-shell agent tokens, embeddable-shell bypass, and delegation tokens on same-target invoke are unchanged.

Intershell calls are gated on intershell_enabled, registry_visibility: public, explicit agentAccessible: true, optional allowedCallers, and the consent matrix. The same gates apply on HTTP and WebSocket transports.

IS-5
Call-chain HMAC required when a signing key is configured; max depth 5.
IS-17
Max one cross-shell hop per human chat turn.
IS-18
30 req/min per caller→target; 120 req/min per target inbound.
IS-19
Bidirectional audit on caller and target.
IS-36
Delegation tokens single-use per verified jti.
IS-14 / IS-49
Cross-shell results tagged [EXTERNAL DATA]; tags stripped from user-visible synthesis.

Details: Inter-Shell Protocol.

Execution ambiguity (outcome_unknown)

After your upstream API accepts a side-effect but before Duct durably records idempotency completion, retries with the same Idempotency-Key return 409 outcome_unknown (DUCT_OUTCOME_UNKNOWN, retryable: true) instead of dispatching a blind second mutation. Consumed execution tokens without a replayable idempotency entry follow the same rule. Integrators should backoff and retry — or reconcile with your upstream using the idempotency key.

SSRF posture

Upstream fetches resolve DNS once, reject private/loopback/metadata addresses, pin the resolved IP, and use redirect: manual with per-hop re-validation. In production, ALLOW_PRIVATE_UPSTREAM must remain unset so upstream targets cannot include private or loopback addresses.

Residual risk

A manifest baseUrl pointing at a public attacker origin can still harvest delegated tokens. Treat manifest push as a privileged operation and monitor baseUrl changes.

Prompt injection

Cross-shell payloads are tagged as external data. Deterministic guards in the intent router enforce hop caps and tool policy — not prompt text alone. Synthesis is terminal: the shell ends the turn after synthesis. Side-effects still require human confirmation even if routing misfires.

Integrator checklist

Secret key
DUCT_SECRET_KEY stays server-side — symmetric HS256 signing secret, never in client bundles.
Delegated tokens
verifyDuctToken with expectedShellId in middleware.
Consent tokens
POST /v1/verify/consent-token for uct_ when partner agents call user-scoped APIs.
Deeplinks
JTI replay protection in production.
Manual embeds
Validate postMessage origin and source.