Background authority mandates
Let durable app work call another application without storing a user's session or API key.
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.
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
resultOnly 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.
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 ascapability.query:item.read,capability.action.review:item.rename,capability.action.run:item.rename,search.query, orwidget.read:weather;actions:deny,require_approval, orpreapproved;- 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.
{
"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:
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> --yesThe 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:
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:
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.