Documentation

Configuration

The manifest is the contract between your product and Duct. It tells the shell what your product can do, what requires auth, what is safe for agents, and where users can be navigated.

Minimal Manifest

ts
import { defineDuctConfig } from '@duct-sdk/sdk';

Top-Level Sections

product
Product name, description, logo, accent color, and required baseUrl / apiBaseUrl — the public origin Duct uses for deeplinks and action proxying.
auth
How Duct should request delegated user tokens and which session fields are safe to include.
routes
User-facing pages Duct can describe or navigate to.
actions
API operations Duct can invoke after policy, auth, and confirmation checks.
deeplinks
Signed navigation targets with required state fields.
agentPermissions
Shell-wide agent policy and cross-shell access. defaultLevel, actionOverrides, requireUserConsent, and allowedActions are enforced at runtime by the shared authorize kernel for agent callers on /invoke, /message, and page-resolve action paths — not config-only. For cross-shell: set registry_visibility: public and intershell_enabled: true to join the Duct network. allowedCallers is an optional restriction.
registry_visibility
'public' | 'private' — whether this shell is discoverable cross-shell. Default: 'private'.
intershell_enabled
boolean — master switch for accepting cross-shell calls. Default: false.
widget
Chat placeholder, theme, size preset (tall/square/large), launcher position, welcome text, and agent-loop limits.

Agent page context (pageResolver.actionId)

When a deeplink declares pageResolver, agent page-context resolution requires pageResolver.actionId referencing a manifest action. Raw endpoint-only resolvers are skipped for agents (fail closed). Human shell chat may still use endpoint resolvers where configured.

Widget appearance

Theme, size preset, welcome text, placeholder, and accent color can be set in duct.config.tswidget or saved from Dashboard → Playground → Customize. Whichever you update most recently wins on the live shell. Agent-loop limits (maxToolIterations, maxPlanSteps, compactionTurns) still follow your manifest push when provided.

Playground vs embedded shell

Dashboard playground Human mode keeps its own conversation and session on your browser, separate from the shell embedded on your product — even when both use the same shell ID. Playground turns appear in Audit → Conversations with a Playground caller badge. Use New chat in either surface to start fresh.

Product fields

name
Product display name in the shell header and dashboard.
description
Short summary for manifest context and dashboard cards.
baseUrl
Public UI origin — deeplinks and action proxy target when apiBaseUrl is absent.
apiBaseUrl
Optional separate API hostname; omit when API shares baseUrl.
logoUrl
Optional HTTPS logo URL — open shell header and dashboard avatars. Launcher bubble always uses Duct. duct init can auto-detect public/logo.* and resolve against baseUrl. Set on product, not widget.

Auth fields

The auth block controls how the embed requests delegated user tokens and where unauthenticated guests are sent when they ask for protected data.

strategy
nextauth | clerk | auth0 | custom-jwt | session-cookie | none | unknown — used by duct init token-route codegen.
sessionFields
Scalar session keys safe to embed in duct_delegated (userId, email, orgId, etc.).
guestAllowed
When false (default for secure shells), requiresAuth actions need a user token. Guests are offered sign-in handoff instead of a dead-end error.
tokenExpirySeconds
TTL for duct_delegated tokens minted by your /api/duct/token route.
tokenEndpoint
Optional full URL for the token route (must pass Zod .url() if set).
loginRoutePath
App route for sign-in (e.g. /login). Shell navigates here via HandoffCard when a guest hits requiresAuth. duct init auto-detects common login paths.
loginDeeplinkId
Optional deeplinks[].id override for sign-in navigation when you prefer a declared deeplink over loginRoutePath.

Action security fields

Every action declares agentAccessible, sideEffects, sensitivityLevel, and requiresAuth. They are enforced before Duct calls your API — for human chat, Agent API, and cross-shell callers.

Choosing values & troubleshooting

Field reference, caller-by-caller enforcement, scenario checklist, and error codes: Action Permissions.
agentAccessible
boolean — expose to agents / cross-shell? Default false after init.
sideEffects
boolean — mutates state? Writes forced true by CLI; human ConfirmCard + agent 202.
sensitivityLevel
none | low | medium | high | critical — data risk; high/critical needs agent consent.
requiresAuth
boolean — your API needs a user session; agents need user_consent_token.

requiresAuth and programmatic callers

Whether the action needs an authenticated end-user on your API. For programmatic callers, Duct blocks the call unless user_consent_token is present. Company backends typically pass duct_delegated minted server-side; partner agents use uct_ or company OAuth delegation. Your API must accept token types each caller can actually obtain — not only session JWTs from your login form.

requiresAuth and programmatic callers

Human shell chat can work while partner agents get 403 if your middleware only accepts duct_delegated from a logged-in browser session. Internal backends on your servers can mint duct_delegated after login — partner agents need uct_ or OAuth. See partner agents and user identity.

Action chaining (dependsOn)

Declare hard ordering between actions with dependsOn on the consumer action. Each entry names a prerequisite action and optional paramMapping from the dependency's response fields into params for the dependent step.

ts
{

The human shell expands the DAG when the router picks a dependent action. POST /v1/shells/:id/message does the same — prerequisites run in order inside one orchestration turn. Use POST /invoke when your agent already has all params.

Field priority between steps

When forwarding output from step A into step B: (1) paramMappings on action A, else (2) dependsOn.paramMapping on action B, else (3) auto-extract scalar *_id fields. Human shell and Agent API use the same order.

Manifest Versions & Side-Effect Changes

Every npx @duct-sdk/sdk push (and every rollback or promote) increments manifest_version. After activation, enforcement uses the current live manifest — cache invalidation runs on push, and versioned cache keys mitigate concurrent stale refills — so updated permissions apply promptly under normal conditions. Permission and consent checks always evaluate the current version, not the version in effect when a chat session started.

Adding sideEffects to an existing action

Fresh consent is required on the next invocation. If an action previously had sideEffects: false and you push a manifest where it is true, the runtime treats it as a side-effect action from that point forward. Earlier read-only executions in the same conversation do not carry over as approval for the mutation.

Human chat (ConfirmCard)

The next time the shell invokes that action, the user sees a ConfirmCard and must approve before anything reaches your API. Each side-effect call requires its own confirmation — there is no blanket “user already consented in this session” bypass for mutations.

While a confirmation card, in-chat form, or multiple-choice clarification is showing, the message input is disabled until the user responds on that card — so accidental typing cannot interrupt an approval or disambiguation step.

Agent API

Side-effect actions return 202 confirmation_required with a short-lived execution_token on every call, even when a valid X-Duct-User-Consent header is present. A user consent token authorizes the agent to request execution; it does not auto-execute irreversible work. If the action newly requires side-effect handling, agents without a consent token receive 403 until the user approves through your consent flow.

Confirmation approval mode

Set DUCT_APPROVAL_MODE on the platform stack. Default operator_session requires a signed-in dashboard operator to approve side-effect confirmations. Set link_holder to allow independent agents to forward approval_url to a human browser — this proves channel possession, not verified human identity (optional CAPTCHA via DUCT_APPROVAL_CAPTCHA_*). Legacy DUCT_STRICT_OPERATOR_APPROVAL=true maps to operator_session.

User consent after a manifest change

Consent is bound to what the user approved — not just the action name. If you change an action's method, path, sideEffects, or sensitivityLevel and push, existing user_consent_token values no longer cover that action. The next invoke returns 409 until the user re-approves through your consent flow. Tokens that still match the current definition remain valid until expiry (24 hours) or explicit revocation.

Side-effect confirmation is separate: even with a valid consent token, agents still receive 202 confirmation_required on each mutating call.

Agent tokens after a push

Agent access profiles exchange for short-lived tokens tied to the manifest version at exchange time. After push, rollback, or promote, invoke may return 409 with MANIFEST_STALE until the agent re-exchanges at /v1/agent-access-token. See agent access after a push.

Session notice

Chat sessions started before a push may show a soft “shell configuration updated” banner. Enforcement still uses the latest manifest; the banner is a UX hint that action schemas may have changed mid-conversation.

Config files are TypeScript today

The CLI config file is TypeScript so it can be type-checked and autocompleted. That does not require your app backend to be TypeScript. Python, Ruby, Go, Java, and other stacks can still expose HTTP endpoints and use the REST API.

What Is Not In duct.config.ts

The manifest controls the shell's capabilities. A few settings live outside it and are managed elsewhere:

Webhook URLs
Dashboard → Shell → Settings → Webhooks. Not part of duct.config.ts; duct push does not persist webhook config.
Signing secret
Generated in the dashboard. Used to verify X-Duct-Signature on webhook payloads. Not in the manifest.
LLM provider / API key
Dashboard → Shell → Settings → LLM. Override the default model per shell. Saved API keys are write-only — GET returns keyConfigured and keyPreview only; a blank update preserves the existing key.
Registered domains
Dashboard → Shell → Settings → Domains. Required for postMessage origin validation when embedding.
DUCT_SECRET_KEY
Server environment variable — never in duct.config.ts or client code.