Cloud|

Background authority mandates

Let durable app work call another application without storing a user's session or API key.

9 min read Updated 2026-10-18 #identity#background#capabilities#mandates

A mandate records what one durable workload may ask Core to do later for a current user or resource service account. It is not a bearer token, does not grant resource access, and cannot be sent to a target application by itself.

Use a mandate when a job, workflow, or automation must call another Cloud application after the originating browser session may have expired. Do not store a browser cookie, OAuth access token, or personal API key with the job.

text
interactive create/update                 later background run

user -> owning app -> mandate in Postgres
                         |
                         v
worker -- app credential + mandate id --> Core
                                          | validate current mandate + subject
                                          | sign exact 30-second invocation
                                          v
                                   target capability
                                          | current domain authorization
                                          v
                                        result

Only the Core-to-target hop carries the invocation JWT. The worker never gets signing power and the target never receives the app credential, mandate ID as authority, or original user credential.

Create one mandate with the workload

Create exactly one mandate for one durable workload, not one per target app. Its bounded policy lists every target application and canonical operation the workload may request.

ts
import { mandates } from "@k2b/cloud/services";

const mandate = await mandates.create(
  {
    authority: { kind: "interactive", userId: user.id },
    subject: { type: "user", id: user.id },
    ownerAppId: "inventory",
    workloadType: "scheduled-report",
    workloadId: report.id,
    policy: {
      version: 1,
      apps: ["mail"],
      operations: ["capability.action.run:message.send"],
      actions: "require_approval",
    },
  },
  { db: transaction },
);

A mandate keeps acting after the person's phone is removed, so the mobile app's session never creates, widens, or resumes one. Before you create a mandate, change its policy, or resume it with an interactive authority, reject such requests: auth.rejectAppSession on the route, or isAppSessionActor(actor) in a service or capability. Answer 403 with { "code": "FORBIDDEN", "message": "Use Cloud on the web for this." }. Pausing and revoking stay allowed. POST /api/me/mandates, Assistant routines (scheduled tasks), and Mail incoming automations with Spaces steps follow this rule.

Built-in applications that share Postgres create the mandate and owning workload in one transaction. A separately deployed application that cannot share that transaction creates a pending mandate, persists the workload, and then confirms it with the owning app's workload credential. A pending mandate cannot issue invocations. Revoke it when persistence fails; Core also revokes unconfirmed mandates after the bounded confirmation window.

Only confirmed active or paused mandates reserve an owning workload coordinate. Pending registrations cannot block a legitimate workload. Confirmation returns HTTP 409 if another confirmed mandate already owns the coordinate. Revoked metadata remains visible, and a failed registration can retry with a new mandate.

Each creator may have at most 100 live pending mandates during the 15-minute confirmation window. Paused pending mandates count too; expired ones do not. At HTTP 409, confirm or revoke outstanding registrations before creating more.

Use POST /api/me/mandates from the signed-in browser session for that remote sequence. Core always derives the user subject from that session; the request contains only the bounded owner app, workload coordinates, policy, and optional expiry. Persist the returned ID and revision first, then call POST /api/_internal/identity/v1/mandates/<mandateId>/confirm with the same revision and the app workload credential. Ordinary mandates.create(...) remains the atomic same-transaction path and is confirmed immediately. Before confirming, the owning app must verify that the persisted workload belongs to the mandate's subject; knowing a workload ID is not proof of ownership.

Policy values are:

  • apps: an explicit target-app allowlist, or "*" only for a deliberately open user-sponsored agent;
  • operations: canonical values such as capability.query:item.read, capability.action.review:item.rename, capability.action.run:item.rename, search.query, or widget.read:weather;
  • actions: deny, require_approval, or preapproved;
  • optional grants: exact capability pairs and fixed inputs, checked in addition to the app and operation allowlists.

preapproved requires explicit non-empty app and operation allowlists. A wildcard mandate always requires approval. It cannot silently approve an operation that the product or capability contract says needs user attention.

Background work does not inherit remembered interactive approvals, neither those for a chat, including a website allowed for a chat, nor those that apply everywhere. Use a narrowly scoped preapproved mandate only for explicitly authorized unattended operations. Missing, paused, expired or revoked authority must not be replaced with a user's browser session or silently recreated by a worker.

Only a current interactive subject or administrator can create, resume, or broaden a mandate. The owning workload may narrow, pause, or revoke it. A revoked mandate is terminal.

Grant capabilities to an unattended Assistant routine

Assistant routines use explicit grants in their mandate policy. Each grant pairs one appId, local capabilityId, and kind (query or action) with optional fixed top-level inputs. The agent proposes the scope; the user reviews it when creating or updating the routine. Capabilities need no additional metadata.

json
{
  "appId": "notebooks",
  "capabilityId": "note.edit",
  "kind": "action",
  "fixedInput": { "noteId": "abc123" }
}

This allows changes to that note while leaving the edit content unrestricted. An empty fixedInput allows any inputs to that exact capability within the user's current access. Fixed JSON values use exact equality: nested objects must match completely, and array order matters. There are no wildcards, implicit parent-resource checks, or expressions. A notebook restriction cannot be inferred from a capability that only accepts a note ID.

Background runs discover and load only capabilities their mandate policy and routine grants can permit. Previously loaded tools outside that scope stay unavailable. Discovery ignores fixed inputs; Core checks them on every call. If the mandate cannot be read, has changed, or its policy is invalid, the run stops before the model starts and the routine needs attention.

Core checks the current mandate revision, grant pair, fixed values, and live capability approval mode before every invocation. Only actions with approval: "none" or approval: "rememberable" can pass the routine mandate policy. Remembered interactive approvals do not authorize background work. The target application still checks the user's current resource permissions.

Before each change, a routine run checks the mandate first, then calls the app's review for every Action with approval: "rememberable". It runs the Action only when the review returns an approvalScope. An Action that needs approval and is not rememberable never runs in a routine, regardless of the review. A missing review, a review error, or a result without that scope refuses the call; the app's run is never invoked. The review only narrows the mandate and never grants access. The same rule applies to background Code Mode.

The model receives a short NEEDS_APPROVAL error telling it to stop and say what was missing. Even if the model completes its reply, the routine becomes needs_attention with reason needs_approval and the action name in the routine's locale. A one-time routine needs a new time before it can run again.

Changing grants updates the mandate revision, invalidating older runs' authority. The agent can inspect a failed run and propose revised grants in the normal chat; expanding the scope requires a new reviewed routine update. A worker cannot grant itself more authority. A capability call rejected by the grants or fixed inputs returns a tool error so the model can continue with granted tools or explain what is missing. The routine also needs attention when its mandate is unavailable, paused, revoked, expired, or changed, when the target app rejects the sponsor with HTTP 401 or 403, or when an action's outcome is unknown. Scheduled turns run server Code Mode tools under the same routine-scoped authority; see Scheduled Code Mode.

Invoke from a worker

Provision a resource-bound service-account credential for the owning application with binding { appId, resourceType: "cloud.app", resourceId: appId } and the identity:invoke scope. Store it as CLOUD_APP_CREDENTIAL in that application only. Also set CLOUD_CORE_INTERNAL_ORIGIN to Core's private service origin. Workload broker routes are not reachable through the public gateway, and the helper does not fall back to the public origin for mandate calls.

An administrator opens Administration → App credentials (/admin/app-credentials), chooses Create credential, then selects the owning app and enters a name and optional expiration time in the dialog. The token is shown once. Save it in the deployment's secret store and inject it only into that app. The table lists credentials across all apps and supports revocation.

The CLI exposes the same operations. For example, using an administrator profile:

sh
cld admin app-credentials apps
cld admin app-credentials list mail
(umask 077; cld admin app-credentials create mail --name production --yes > mail-credential.txt)
cld admin app-credentials revoke mail <credential-id> --yes

The create command prints the token once (structured output includes token and metadata). Transfer it into the deployment secret store and remove the temporary file. Use --expires-at with a future ISO 8601 timestamp to set the expiration time. Create/list/revoke also work for an explicit app ID before it registers; the UI selector and apps command show registered applications.

Core's administrator API provides the same creation operation:

http
POST /api/admin/identity/workloads/inventory/credentials
Content-Type: application/json

{"name":"Inventory background work","scopes":["identity:invoke"]}

The raw token appears only in this credential-creation response. List bounded credential metadata across apps with GET /api/admin/identity/workloads/credentials, or for one app with GET /api/admin/identity/workloads/inventory/credentials, and revoke one exact credential with DELETE /api/admin/identity/workloads/inventory/credentials/<credentialId>. Rotate by creating a new credential, deploying it to the owning app, verifying broker calls, and then revoking the old credential. Do not reuse the OAuth broker secret; it is shared only by Core and OAuth for OAuth issuance.

The same app-bound credential owns the remote mandate lifecycle under /api/_internal/identity/v1/mandates/<mandateId>. It may read its mandate, confirm a pending workload, narrow policy, pause, or revoke. It cannot create user authority, broaden policy, or resume a paused mandate. Core derives the owner from the authenticated credential; an app ID in a request body is never trusted as owner authority.

Lifecycle request bodies are limited to 256 KiB, including streamed requests without a reliable Content-Length. Oversized bodies return HTTP 413. Missing mutation authority returns HTTP 403; an authorized but stale revision returns HTTP 409.

Use the server capability helper with the current mandate revision:

ts
import { invokeCapability } from "@k2b/cloud/capabilities/server";

const result = await invokeCapability(
  {
    appId: "mail",
    capabilityId: "message.send",
    kind: "action",
    input: { draftId: job.draftId },
    idempotencyKey: job.id,
  },
  {
    authorization: `Bearer ${process.env.CLOUD_APP_CREDENTIAL}`,
    mandate: {
      id: job.mandateId,
      revision: job.mandateRevision,
      callingAppId: "inventory",
    },
  },
);

The helper sends the app credential only to Core. Core checks the credential's exact app binding and identity:invoke scope, reloads the mandate and subject, matches the target and operation, signs one target-specific invocation, and dispatches through the live capability schema. The target then performs its ordinary effect-time permission checks.

An app workload credential cannot use the ordinary interactive capability route to bypass the mandate.

Assistant routine runtime grants

Routines keep capability, HTTP, and RSQL access in the same mandate grants list. Capability entries use appId, capabilityId, kind, and fixedInput. Code Mode entries use kind: "http" or kind: "database" and fixedInput: HTTP can fix an HTTPS origin, exact URL, and method; database access can fix a Studio app ID, operation, and table. Omitted fields are free for the routine to choose. An empty object explicitly grants unrestricted inputs for that kind, within the runtime's existing limits and current resource access.

Flat cloud.db operations list, get, insert, update, and delete match grants rows.list, rows.get, rows.insert, rows.update, and rows.delete; query matches query.

The ordinary routine approval describes these grants before they are saved. Runtime calls check the stored mandate revision and exact fixed values; HTTP checks again immediately before sending. Personal remembered approvals do not replace routine grants. Expanding a grant needs a new interactive confirmation. Table-scoped grants do not authorize arbitrary SQL, export, or connection setup; those operations need a separate matching grant. See Scheduled Code Mode for examples.

Handle lifecycle and retries

Persist the mandate ID and revision with the workload. A policy update, pause, resume, or revocation increments the revision. Core rejects requests carrying an old revision; the worker must reload current workload state before trying again. This does not require freezing the revision when work is queued.

Pause the mandate when the workload pauses. Revoke it when the workload is deleted or permanently disabled. Revocation blocks new issuance immediately; an invocation already admitted can remain valid for at most 32 seconds: its nominal 30-second lifetime plus the dedicated two-second clock tolerance.

Keep the capability's existing idempotency and approval rules. A mandate does not make an unsafe retry safe. If a non-idempotent Action loses its response, the helper returns ACTION_OUTCOME_UNKNOWN and the worker must reconcile instead of retrying blindly.

Users can inspect their own mandates through the bounded GET /api/me/mandates listing. Administrators use GET /api/admin/identity/mandates; owning applications keep product-specific routine and automation presentation in their own UI. Lifecycle and issuance audit records include subject and workload provenance, target application, operation, revision, and outcome, but never a JWT, app credential, policy body, or capability payload.

A signing failure is recorded as a failed issuance before an internal error is returned, and no target request is sent.

What still authorizes the effect

Authentication reconstructs the current actor and accessSubject. It does not copy or freeze application permissions. The target application must still check its current domain grants, resource ownership, object state, and any app-owned workflow snapshot before committing the effect.

Continue with App capabilities and Resource authorization.

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.