App capabilities
Publish a small, versioned RPC surface for cross-app calls, agents, CLI, and MCP.
Capabilities are an application's small, versioned machine interface. An app
publishes addressable resource Types, read-only Queries, and mutating
Actions, and interactive Commands from one defineCapabilities() declaration.
The declaration exists so a separately deployed provider can describe a stable operation once while Cloud projects it into cross-app calls, AI tools, the authenticated Cloud MCP server, HTTP, and CLI. Consumers discover the current live contract; they do not import the provider's source code or private DTOs.
Use capabilities only for stable operations that should work through several of those consumers. Keep complete administrative APIs, bulk transfers, specialized transport behavior, and unstable internal operations in REST and app-specific CLI modules.
A capability is discoverable, not authorized. The owning application must authenticate the request and check current resource access for every call.
Choose what to publish
Publish an operation when it is:
- stable enough to name and version;
- bounded in input, output, and work;
- useful to more than one machine client;
- clear from its title, description, and field descriptions;
- safe after the owning app performs its normal authorization.
Do not mirror every REST endpoint. Capabilities are a curated semantic surface, not a second complete application API.
| Surface | Use it for |
|---|---|
| Capabilities | Stable cross-app reads and mutations, agent tools, generic RPC |
| REST API | Complete application behavior and specialized HTTP contracts |
| App CLI module | Full application-specific terminal workflows |
| Generic capability CLI | Discovering and invoking the curated capability surface |
Declare the surface
Keep capability definitions in src/capabilities.ts, next to modules such as
src/notifications.ts. This example publishes one resource Type, one Query,
and one Action. The sample store keeps the example complete; a real application
performs these reads and mutations in its service layer.
src/capabilities.ts
import { defineCapabilities } from "@k2b/cloud";
import type { AccessSubject } from "@k2b/cloud/contracts";
import { ok } from "@k2b/stdlib";
import { z } from "zod";
type Item = {
id: string;
ownerId: string;
name: string;
quantity: number;
};
const items = new Map<string, Item>([
[
"k3P9xQ",
{
id: "k3P9xQ",
ownerId: "user-42",
name: "USB-C adapter",
quantity: 4,
},
],
]);
const visibleItem = (id: string, subject: AccessSubject): Item | null => {
const item = items.get(id);
return item && subject.type === "user" && subject.userId === item.ownerId
? item
: null;
};
export const inventoryCapabilities = defineCapabilities({
protocolVersion: 2,
types: {
item: {
title: "Inventory item",
description: "One item in the inventory catalog.",
icon: "ti ti-package",
reader: "item.read",
},
},
queries: {
"item.read": {
title: "Read inventory item",
description: "Read one visible inventory item by stable ID.",
input: z
.object({
id: z.string().regex(/^[A-Za-z0-9]{6}$/).describe("Stable inventory item ID."),
})
.strict(),
data: z
.object({
id: z.string().regex(/^[A-Za-z0-9]{6}$/),
name: z.string(),
quantity: z.number().int(),
})
.strict(),
openWorld: false,
run: async ({ id }, context) => {
const item = visibleItem(id, context.accessSubject);
if (!item) {
return {
ok: false,
error: {
code: "NOT_FOUND",
message: "Inventory item not found",
status: 404,
},
} as const;
}
return ok({
data: { id: item.id, name: item.name, quantity: item.quantity },
refs: [{ type: "inventory.item", id: item.id, title: item.name, icon: "ti ti-package" }],
links: [{ rel: "open", href: `/app/inventory/items/${item.id}` }],
});
},
},
},
actions: {
"item.rename": {
title: "Rename inventory item",
description: "Rename one inventory item the caller may edit.",
input: z
.object({
itemId: z.string().regex(/^[A-Za-z0-9]{6}$/).describe("Stable inventory item ID."),
name: z.string().trim().min(1).max(120).describe("New item name."),
})
.strict(),
data: z.object({ id: z.string().regex(/^[A-Za-z0-9]{6}$/), name: z.string() }).strict(),
destructive: true,
openWorld: false,
idempotency: "none",
approval: "rememberable",
review: async ({ itemId, name }, context) => {
const item = visibleItem(itemId, context.accessSubject);
if (!item) {
return {
ok: false,
error: {
code: "NOT_FOUND",
message: "Inventory item not found",
status: 404,
},
} as const;
}
return ok({
message: "This inventory item will be renamed.",
details: [
{ label: "Current name", value: item.name },
{ label: "New name", value: name },
],
links: [{ rel: "open", href: `/app/inventory/items/${item.id}` }],
approvalScope: "inventory",
});
},
run: async ({ itemId, name }, context) => {
const item = visibleItem(itemId, context.accessSubject);
if (!item) {
return {
ok: false,
error: {
code: "NOT_FOUND",
message: "Inventory item not found",
status: 404,
},
} as const;
}
const renamed = { ...item, name };
items.set(itemId, renamed);
return ok({
data: { id: renamed.id, name: renamed.name },
summary: `Renamed inventory item to ${renamed.name}`,
refs: [{ type: "inventory.item", id: renamed.id, title: renamed.name, icon: "ti ti-package" }],
links: [
{ rel: "edit", href: `/app/inventory/items/${renamed.id}/edit` },
],
});
},
},
},
});Import the declaration where the application starts:
src/config.ts
import { defineApp } from "@k2b/cloud";
import { Hono } from "hono";
import { inventoryCapabilities } from "./capabilities";
const app = defineApp({
id: "inventory",
name: "Inventory",
description: "Track inventory items.",
icon: "ti ti-package",
baseUrl: "http://app-inventory:3000",
routes: ["/app/inventory"],
});
const router = new Hono().get("/app/inventory", (c) =>
c.html("<h1>Inventory</h1>"),
);
export default await app.start({
capabilities: inventoryCapabilities,
fetch: router.fetch,
});app.start() compiles the declaration before registration. The application
service still owns durable reads and writes, permission checks, and audit
records. Retry safety is not an application concern: Cloud claims every
idempotency: "required" Action for you.
Localize catalog presentation
The declaration's English titles, descriptions, search-tag copy, and Zod field
descriptions are its complete base presentation. Add locale-specific overlays
under presentation when the application ships another language:
export const inventoryCapabilities = defineCapabilities({
protocolVersion: 2,
presentation: {
baseLocale: "en",
translations: {
de: {
types: {
item: { title: "Inventarartikel", description: "Ein Artikel im Inventarkatalog." },
},
queries: {
"item.read": {
title: "Inventarartikel lesen",
description: "Liest einen sichtbaren Inventarartikel anhand seiner stabilen ID.",
input: { id: "Stabile Inventarartikel-ID." },
},
},
actions: {
"item.rename": {
title: "Inventarartikel umbenennen",
description: "Benennt einen bearbeitbaren Inventarartikel um.",
input: { itemId: "Stabile Inventarartikel-ID.", name: "Neuer Artikelname." },
},
},
},
},
},
// types, queries, and actions...
});Translation maps use stable local IDs, stable search-tag tokens, and dotted
schema field paths such as filters.tags[]. Startup rejects unknown IDs and
paths. A locale may override only the human presentation it owns; operation
IDs, tag tokens and aliases, schemas, schema hashes, safety flags, and result
data remain unchanged. Catalog requests resolve exact locale, ancestor, and
base fallback (de-CH → de → en) and return final localized strings, so
consumers never import another application's message keys.
People see this presentation in the Assistant's tool rows, approval cards,
and remembered approvals; in Studio app approvals; in search; and in the
capability catalog. Each surface resolves it for the reader's request locale,
together with the localized app name from the app's own presentation. An
entry without a translation shows its base text, so a missing locale never
hides a capability. Translate at least the title and description of every
Type, Query, Action, Command, and search tag.
Cloud's Assistant and chat presentation surfaces word every app through its own presentation and never treat one app differently by its ID, name, or tool name, so built-in and third-party apps read the same.
The model always receives the base presentation, and so do the tools the MCP server lists. Tool names, titles, descriptions, and schema field descriptions are prompt text for a model: they stay identical for every reader, while the model still answers in the reader's language.
Action reviews and provider-authored summaries or errors are runtime output,
not registry metadata. Resolve those inside the handler from
context.locale, including every review message and detail label;
preserve their stable codes and structured values.
Word Actions for people
An Action can say in its own words what it is about to do and what it did.
The Assistant's approval card, its receipts and step rows, and the approval
text of cld assistant actions show these sentences, in the reader's
language; the approval text adds the Action's live review message below the
sentence. They are optional: an Action without them reads as its title with
up to two labelled fields, such as "Send email · Recipients: jana@example.com".
presentation.sentences is deprecated and will be removed with the next major
release. Use per-call review forms
for new Actions.
Declare the base-locale sentences under presentation.sentences, keyed by
Action local ID, and each translation under the Action's entry in
translations. A third-party app uses exactly the fields Mail uses:
export const inventoryCapabilities = defineCapabilities({
protocolVersion: 2,
presentation: {
baseLocale: "en",
sentences: {
"item.transfer": {
approval: "Transfer {input.quantity} × {input.itemName} to {input.recipient}",
done: "Transferred {input.quantity} × {input.itemName} to {input.recipient} on {data.transferredAt}",
rejected: "Did not transfer {input.itemName}",
notRun: "{input.itemName} not transferred",
},
},
translations: {
de: {
actions: {
"item.transfer": {
title: "Artikel übergeben",
description: "Übergibt einen Bestand an eine Person.",
sentences: {
approval: "{input.quantity} × {input.itemName} an {input.recipient} übergeben",
done: "{input.quantity} × {input.itemName} am {data.transferredAt} an {input.recipient} übergeben",
rejected: "{input.itemName} nicht übergeben",
notRun: "{input.itemName} nicht übergeben",
},
},
},
},
},
},
actions: {
"item.transfer": {
title: "Transfer item",
description: "Transfers stock to a person.",
input: z
.object({
itemName: z.string().max(200).describe("Item name."),
quantity: z.number().int().positive().describe("Quantity."),
recipient: z
.object({ name: z.string().max(200).describe("Display name."), email: z.string().max(320).describe("Email address.") })
.strict()
.describe("Recipient."),
})
.strict(),
data: z.object({ transferredAt: z.iso.datetime({ offset: true }) }).strict(),
// destructive, openWorld, idempotency, review, run...
},
},
});| Sentence | Shown | May read |
|---|---|---|
approval |
Card title, running receipt, step row while the call runs or waits, approval text | {input.*} |
done |
Receipt and step row after the Action succeeded | {input.*}, {data.*} |
rejected |
Receipt and step row after the person rejected the call | {input.*} |
notRun |
Receipt when the turn ended before the call ran | {input.*} |
Each {…} reference names a field by its dotted schema path, like the schema
presentation maps: {input.to} or {input.to[].name} for the call input,
{data.id} for the Action's result data. Cloud inserts each value as plain
one-line text without control characters, line breaks, or invisible format
characters such as direction marks and joiners, and never reads it as a
template. A value longer than 60 characters keeps its start and its end, so a
path keeps its file name and an address its domain. Lists show three entries,
such as "jana@example.com, max@example.com, and 2 more"; an entry without a
value to show still counts toward "more". Only fields whose schema declares
format: "date" or format: "date-time" read as dates: instants in the
reader's time zone, calendar dates as the same day everywhere. Any other text
stays exactly as the call carries it, even when it looks like a date, and so
does a date field's value that is no real day, such as 2026-02-30. Numbers
use the reader's locale, and booleans read "yes" or "no". An object reads as
its email, address, or path when it has one, else as its displayName,
name, title, or label: a name in the call input is not verified, so it
never stands in for the value the Action acts on.
When a referenced field has no value to show, such as an empty list or an optional
field, or the sentence would be longer than 240 characters, it falls back to
the generic wording instead of showing a gap or losing its last words. A
missing outcome sentence uses the generic state words around the approval
sentence, such as "Rejected: Send email to jana@example.com". A done
sentence takes the place of the Action's runtime summary in the receipt;
leave done out when the summary says more about the call, as Mail does for
sending, where the summary names the subject and whether the email was queued
or scheduled.
The generic sentence takes labels from the first clause of the localized field
descriptions, up to 40 characters, and skips identifiers, revisions, flags,
and long text; start descriptions with a short noun phrase, such as
"Task title; required unless the template has one.".
app.start() rejects a sentence that names a field the schemas do not
declare, reads {data.*} outside done, uses other braces, has more than six
placeholders or 200 characters, or belongs to a Query or an unknown Action.
The most specific locale that words an Action supplies its whole set of
sentences, so one receipt never mixes languages; translate every sentence you
declare. Cloud derives the wording from the live registry for every app alike
and saves it with the turn like the title, so history keeps its words after
an app update. Older Cloud readers ignore sentences and keep the generic
wording.
When an Action waits for approval, the model may pass an optional
approvalReason beside the app's input. Cloud shows it under its own label,
such as "Why: Jana asked for the offer.", removes it before review,
authorization, and execution, and never lets it replace the app's sentence.
An Action whose input declares its own approvalReason field keeps it.
An app that asks people to approve a capability call with the chat's approval
card, as the Assistant does for code, gets the same wording from
getCapabilityActionWording(appId, localId, locale) in
@k2b/cloud/capabilities/server and passes its sentences and fields into
the block's capability presentation. It returns null for an unknown app or
Action, which then reads as its title.
Understand Types, Queries, and Actions
Types name resources
A Type gives an addressable resource a stable identity such as item. Cloud
qualifies local IDs with the application ID:
item -> inventory.item
item.read -> inventory.item.read
item.rename -> inventory.item.renameApplications declare only the local part. Cloud derives the qualified ID from the registered application ID and uses it as the stable operation identity for Skills, Assistant discovery, loaded-tool state, CLI, and transport metadata. Provider-safe function names are generated later and are not capability IDs.
An application that publishes Capabilities uses a lowercase kebab-case ID
matching [a-z][a-z0-9-]* (maximum 80 characters). This keeps qualified IDs,
CLI commands, and MCP tool names aligned.
Types connect operation targets, result references, Universal Search results, and client presentation. Declaring a Type does not create CRUD operations.
A Type may name one canonical reader Query:
types: {
item: {
title: "Inventory item",
description: "One item in the inventory catalog.",
reader: "item.read",
},
}reader is the Query's local ID inside the same app. Its presence tells a
consumer that a CloudResourceRef of this Type can be read programmatically.
The referenced Query is the only read implementation; do not publish a second
item.get or a Project-specific reader for the same operation. Omit reader
only when the resource has neither a page nor a useful bounded machine
representation.
When an application lets a user copy or paste this identity, use Cloud's versioned resource clipboard format instead of embedding an ID in app-specific JSON or inferring it from text. See Copy and paste Cloud resources.
Resource links
The owning application returns a root-relative semantic link with rel: "open"
for each resource it exposes. Its canonical reader returns that destination
in top-level links. Choose a page the current caller may open: a reader's
Mail draft opens a read view, while a writer's draft opens the composer. A
resource without its own page can open its containing page, such as a comment's
conversation. Never build another application's page route.
Cloud supplies a permanent entry through cloudResourceHref(ref) from
@k2b/cloud/contracts: /open/<type>/~<encoded-id>. The required ~ prefix keeps
IDs such as . and .. intact during URL normalization; the helper supports
the full 512-character resource ID limit. For example,
{ type: "mail.draft", id: "AbC123" } produces /open/mail.draft/~AbC123.
Core invokes the Type's live reader under the signed-in user's authority and
redirects to its first top-level open link. Signed-out users return to the
permalink after signing in. Missing access, unavailable apps and missing
readers or destinations show the same 404 page. Links without ~ also return 404.
These fields serve different boundaries:
| Field | Author and meaning |
|---|---|
Provider links[].rel: "open" with href |
The owning app's root-relative, permission-aware destination |
Consumer refs[].href |
Assistant adds the canonical /open link for refs with live readers; MCP makes it absolute using the installation's app.url |
CLI permalink |
An absolute /open URL on the selected profile's server, added by cloudResourceUrl(ctx, ref) or withCloudResourcePermalink(ctx, type, resource) |
Providers keep the strict ref shape and root-relative semantic links. MCP's
advertised output schema accepts its added refs[].href; that consumer field
is optional and does not become a provider field. Assistant's
read_cloud_resource and MCP's cloud__resource__read accept a ref with its
href unchanged. A ref whose ID is not well-formed Unicode gets no href or
permalink. Generic cld capabilities read, query and action add permalinks for refs with live readers and make
returned semantic links absolute, including links inside resource lists. They
read the catalog for this only when a result names refs; if the catalog is
unavailable, the command warns and still prints the successful result.
Assistant CLI JSON, JSONL, terminal and ACP output make supplied Cloud-relative
Markdown links absolute. Models use supplied links and never construct URLs.
A permalink identifies a resource; it grants no access and contains only its Type and ID. A CLI must omit a canonical link if its reader requires access the caller lacks. Published Grids app commands add no permalinks: projections can be accessible without Base access, but canonical readers require it. See CLI modules.
Test resource links
Give every Type with a page a reader. In the provider's tests, call each reader
for a caller of every access level that may read the resource. Check its
top-level destination with assertResourceOpenLink(result) from
@k2b/cloud/capabilities/testing.
A reader must return a page that caller may open: Mail drafts open the composer
for writers and the read view for readers. A denied read returns no link.
Actions that remove a resource or the caller's access to it do not name it in
refs.
Older owning-app readers that return only edit remain usable: Core accepts a
validated edit destination during rollout when no open is supplied. Readers
without any destination still work through capability calls but cannot open a
permalink. Update owning apps together with Core; see the
rollout note.
Queries read data
Queries do not mutate application state. Use them for bounded read, list, filter, or search operations. Filtering, sorting, pagination, and authorization stay in the application service.
Declare openWorld on every Query. Use true when it may interact with an
open world of external entities, even if it remains read-only. A web search is
open-world; a lookup limited to the app's own permission-scoped database is
closed-world.
Queries may opt into Universal Search. An app may publish multiple focused search Queries when it owns distinct resource kinds. Cloud caps merged results per app, so registering more focused Queries does not give an app a larger share of the global result set.
A canonical reader is an ordinary Query named by its Type. Its input has one
required resource field named id. It may add optional fields for bounded
pagination or content windows, but no other required field. Every
provider-owned CloudResourceRef for that Type must use an id the reader can
resolve directly. The reader performs the same current authorization as every
other Query.
Use the resource's stable, app-owned public ID for CloudResourceRef.id, not
an internal database primary key. The app's canonical reader accepts that value
unchanged and resolves it at the application boundary. See
Public resource identifiers when
choosing between an existing domain identifier and a compact generated ID.
Consumers resolve a reader from the current live manifest: find the owning app
from the qualified ref.type, find the matching Type, then invoke the Query in
its optional reader field with { id: ref.id }. They do not derive a Query
name, persist one beside the reference, or fall back to semantic capability
search.
Design operations that machines can compose
Capability metadata is part of the machine contract. A person may infer that two bare IDs belong to different resources from the surrounding UI; a generic client or agent must not have to guess.
- Start titles with the concrete verb and resource, such as Search mail or Read conversation. Give overlapping operations distinct scopes instead of repeating generic words such as “get” or “update.”
- Make the description state the useful boundary: one known resource, one mailbox, all accessible mailboxes, or an advanced structured search. When one operation is the normal starting point and another is specialized, say so directly.
- Describe every opaque input by resource type and provenance. For example,
write “Exact mailbox ID returned by List mailboxes,” not merely “Mailbox
ID.” Do not advertise invented sentinel values such as
defaultunless the schema and implementation actually accept them. - Return qualified
CloudResourceRefvalues for addressable results. A client can pass that ref unchanged to the Type's current canonical reader. It must never guess a reader name or pass an ID from one resource Type to a reader for another Type. - Keep identity next to each independently usable list result. Prefer
CloudResourceView[]when its fixed projection is sufficient; otherwise use clearly typed domain fields and semantic links. Do not make callers correlate parallel arrays or infer resource type from a bareid.
A common read flow should therefore remain short and typed:
discover focused search -> search -> typed resource ref -> canonical readerUse Universal Search as the normal cross-application or cross-scope search when its bounded resource-view contract fits. Publish a separate app-specific search only for materially different semantics such as structured filters, exhaustive traversal, or a required domain scope. Its title, description, and input descriptions must make that distinction visible during discovery.
Design results around the next decision
Choose fields by the task they support, not by the database row they come from. A mailbox selector may need a name, typed reference, unread count, and action-needed count. Connection diagnostics and creation timestamps are useful for administration, but need not appear in every selection result. Define what counts include and whether they depend on the current actor.
Keep selection results compact. Return enough information to choose a resource and perform the next operation without reading every candidate separately. Load full content or specialist details only when the task needs them. Add a separate browse or content operation only when it provides a useful boundary; do not create one mechanically for every Type or replace its canonical reader. Never present a preview as complete content.
Before changing a published result, inspect real consumers, including other applications, workflows, CLI clients, and Assistant skills. A field that seems irrelevant to an agent may still support a user-facing feature. Follow additive evolution: do not remove guaranteed fields from an existing operation to make its output smaller.
Check complete task paths
Start with representative user tasks and trace the operations needed to finish them. Check both discovery and direct access to a known resource. For example:
- An unknown mailbox needs selection before reading its conversations; a known mailbox ID should not require listing all mailboxes again.
- A reply needs the relevant conversation, a draft, and the applicable send approval. Return the identifiers needed for that handoff directly.
- Editing a note needs the intended content and a consistent revision, not a read of every note in its notebook.
Avoid redundant lookups, but retain resource authorization, necessary current-state reads, and approval gates. Keep domain rules in the owning service so HTTP, capability, and UI callers retain the same behavior.
Assistant skills should explain these short task paths and route to specialist references only when needed. Do not copy the complete operation inventory or schemas into a second manual. Keep capability descriptions, skill instructions, and tested workflows consistent.
Preserve completeness within bounded results
Pagination and content windows must distinguish a complete result from a bounded part of one:
- Budget the serialized JSON envelope, including metadata, refs, and links,
against the contract limits. Account for UTF-8 and
JSON escaping, not just character count or the size of
data. - Follow
page.hasMoreandpage.nextCursor, not the number of returned items. A byte-limited page can be short; a filtered source page can even be empty while more source data remains. Document whether pagination advances through source records or emitted results. Never skip records omitted only because the output budget was reached. - Keep cursors scoped to the query and require forward progress. Consumers should detect repeated cursors or cycles, not assume a fixed page count. If a traversal stops at a work budget, report it as incomplete rather than claiming all results were read. A failed read is not an empty result.
- Bound internal scans and fan-out independently of the returned item count. Permission filtering or recurrence expansion can require much more work than the visible page suggests.
- Mark partial content explicitly. A whole-document replacement needs complete
source content from one consistent revision or hash. Do not assemble windows
from different revisions or overwrite from a preview. For partial updates,
preserve omitted fields server-side and define what explicit
nulland empty collections mean. Reject revision conflicts rather than overwriting blindly.
Review the provider and its consumers together
Before publishing or changing an operation, verify:
- Every retained field supports selection, interpretation, the next action, or an existing consumer contract.
- A representative result can feed the next operation's actual input schema, without guessing IDs, resource types, or hidden defaults.
- Real consumer projections still accept the result and preserve user-facing behavior; use the manifest evolution check for published contracts as well.
- Focused tests cover the affected permission boundary, pagination progress, result limits including refs and escaped multibyte content, and partial-read or revision-conflict behavior where applicable.
Actions change state
Actions declare the mutation's objective behavior:
| Field | Meaning |
|---|---|
destructive |
true when the effect is irreversible or externally visible; false for reversible in-Cloud state changes |
openWorld |
true when the Action may interact with an open world of external entities; false when its interaction domain is closed |
idempotency |
Retry contract: none or required |
approval |
Optional Cloud client policy: omitted asks each time; "rememberable" offers scoped approval; "none" allows autonomous closed-world, non-destructive execution |
review |
Optional read-only description of the concrete effect for human review |
Query and Action kinds project to the MCP readOnlyHint; destructive and
openWorld project to the matching MCP hints. openWorld is independent of
mutation: a read-only web search is still open-world.
destructive means irreversible or externally visible. Deleting a record,
sending a message, publishing, sharing outside Cloud, and overwriting content
without an undo path are destructive. A reversible state change the same user
can set back — marking read, marking a favorite, editing tags, changing a status,
or reassigning an owner — is not destructive. Choose the label from the
consequence for the user, not from the SQL statement.
Cloud enforces the mechanical part of that rule when it compiles a manifest, so a violating app fails to start rather than shipping a misleading catalog:
- every
destructive: trueAction must declarereview()andidempotency: "required"; - every
openWorld: trueAction must declarereview()andidempotency: "required", and must not useapproval: "rememberable"; approval: "rememberable"must not be combined withdestructive: true.
An Action that creates a record should also require idempotency. That one is guidance rather than a compile check, because Cloud cannot tell a create from an update.
Queries are always retry-safe. An Action with idempotency: "required" is
retry-safe because Cloud enforces it (see
Retry an Action safely); none means callers must
not send a key and must not retry after an ambiguous transport failure.
Do not rely on MCP's conservative defaults. Declare these fields explicitly so the live Cloud catalog remains deterministic. MCP defines annotations as untrusted hints and does not prescribe an approval policy. Cloud clients may use trusted app metadata for confirmation, warning, retry, or untrusted-content treatment, but that behavior belongs to the client. See the official MCP ToolAnnotations schema and AI tools and approvals.
approval is deliberately optional. Without it, AI Core
asks for every Action call. approval: "rememberable" lets a supporting client
offer an explicit Approve for this chat or Always approve choice after
showing the Action review. That review must return an opaque, app-owned
approvalScope. A remembered choice matches the current actor, qualified
Action, and exact scope, and a chat choice also the chat; for example, Mail
uses one scope per mailbox. Choose the smallest stable domain in which repeated
calls have the same understandable consequence. A review may omit
approvalScope for arguments whose consequence should be confirmed every time,
for example a Files copy into another storage base or into a publicly shared
folder, a Spaces task with assignees, or a Mail move to Trash.
Cloud rejects this policy on openWorld Actions, on destructive Actions, and
on Actions without a review. It does not weaken app-side authorization, input validation, audit,
or concurrency checks, all of which still run for every invocation.
Routines check their mandate first, then call the review before every Action
whose approval is not "none", including background Code Mode; they run it
only when the review returns an approvalScope. Omit the scope for consequences
that need confirmation every time: routines will refuse them, and a review
can only narrow the mandate.
People decide Action approvals in the chat, an app, or Cloud's approval lists.
Use remembered approval only for bounded, repeatable changes where future calls of the same Action are an understandable extension of the user's choice, such as editing a draft, marking a conversation, or updating a task. Do not use it for deletion, external communication, permission changes, financial or legal commitments, or other effects whose target and consequence should be confirmed every time.
None of the metadata replaces application-side authorization. The owning app must authorize both an optional review and the eventual Action against current state.
Record security-sensitive mutations with Audit events.
Retry an Action safely
Idempotency is a platform mechanism. Applications write no claim code, no
receipt table, and no replay branch: they implement the Action once and Cloud
guarantees that one Idempotency-Key produces at most one effect.
The caller sends a stable Idempotency-Key header with every
idempotency: "required" Action, and reuses the exact same key and input to
retry it. Before forwarding, the dispatcher claims
(app, capability, acting principal, key) together with a hash of the canonical
input, and resolves that claim once the outcome is known:
| Situation | Result |
|---|---|
| First call | Forwarded once; a definitive success is stored |
| Retry of a stored success | The stored status and body are replayed without forwarding |
| Same key, different input | 409 IDEMPOTENCY_CONFLICT |
| A concurrent attempt is still running | 409 IDEMPOTENCY_IN_PROGRESS |
| An earlier attempt lost its answer | 409 IDEMPOTENCY_UNCERTAIN |
A failure that proves nothing happened — a 4xx from the app, or a dispatcher
rejection before forwarding — releases the key, so a corrected retry runs
normally. A timeout, transport failure, or unusable response freezes the key as
uncertain; the outcome is genuinely unknown, so Cloud never re-forwards it and
the caller must read the current state with a Query before deciding.
Stored responses larger than 256 KiB are not retained; replaying one returns
IDEMPOTENCY_UNCERTAIN with a "result not retained" message. Claims expire 24
hours after they are created, pruned by the same maintenance that prunes
execution history. Reuse a key only to retry the same call.
Describe an Action before it runs
Use one test: if a client chooses to ask before execution, can a person identify
the target, change, and consequence from the parsed Action arguments alone? If
not, add review.
A review is normally useful when:
- opaque IDs need current names, labels, or values;
- an update needs a before-and-after comparison;
- external communication, publication, permission changes, or destructive work needs a concrete consequence;
- a bulk Action needs a bounded count and representative targets;
- large or encoded input such as a document, attachment, or calendar payload needs a readable summary.
Cloud's built-in providers add a review to every destructive or openWorld
Action so a client can present concrete targets and consequences before asking
for approval. Third-party apps may opt into reviews independently, but should
follow the same rule when their Actions need human approval. Do not add reviews
to closed-world, exclusively additive Actions merely to repeat the title or
serialize the same arguments differently; that creates confirmation fatigue
without adding useful context.
An Action can explicitly declare approval: "none" for autonomous execution.
The compiler accepts this only for non-destructive, closed-world Actions. AI
execution then skips review and confirmation; normal authorization, validation,
idempotency, and delegated mandate restrictions still apply. Omitting approval
keeps the default confirmation behavior. Use rememberable when users should
choose whether to approve future operations in the same scope.
Legacy Actions without a policy return this deprecated type:
type CapabilityActionReview = {
message: string;
details?: Array<{
label: string;
value: string;
display?: "inline" | "block";
format?: "date" | "date-time" | "markdown";
}>;
links?: CapabilitySemanticLink[];
approvalScope?: string;
};message states the consequence. details lists the concrete values a person
should check. Omit display, or use inline, for concise fields. Use block
for bounded long-form plain text such as a proposed message body. links
reuses the existing root-relative, same-origin semantic links so the person can
inspect or edit the resource in its owning app.
approvalScope is omitted for one-time approvals. When the Action declares
approval: "rememberable", it is what a person can remember; a review without
it asks for that call every time. It is an opaque identifier interpreted by
the owning app, not a permission grant or a replacement for current access
checks in review and run.
The owning app chooses date semantics without pre-formatting for one locale:
use format: "date" with an exact YYYY-MM-DD calendar date or an RFC 3339
instant whose time should be hidden. Use format: "date-time" when both date
and time matter. Clients render those values in the viewer's locale and convert
instants to the viewer's timezone. Omit format for ordinary text, including
relative descriptions such as an undo window.
Block review details may use format: "markdown" for readable emphasis and
paragraphs inside the standard approval UI. Set display: "block" and escape
dynamic text before composing Markdown. Plain text remains the default.
Images are not loaded in approval details.
The shape is intentionally fixed. Reviews have no app-defined schema, title,
icon, severity, arbitrary JSON, HTML, refs, pagination, or executable
controls. display is only a layout hint, and format selects date formatting
or block Markdown; neither changes the value's trust. Clients derive
the title and app presentation from the live manifest and registry in the
reader's locale, and derive
warning treatment from openWorld and destructive. Treat every review value
as untrusted content; render plain text escaped and
Markdown through the shared safe renderer and keep semantic links as links rather than
flattening them into text.
Cloud bounds a review to a 1,000-character message, 20 details with a
120-character label and 10,000-character value, and 10 semantic links. For
larger content, return a useful bounded description and an open or edit
link to the complete resource.
The callback receives the Action's parsed input and normal
CapabilityExecutionContext. It must only read, validate, and authorize; it
must not mutate state, perform an external effect, manufacture approval, or
change the Action input. Return normal capability errors when the target is no
longer available or reviewable.
A successful review is presentation, not permission or proof of user intent.
The Action still revalidates its input, authorization, version or revision,
and domain invariants in run. If reviewed state changes before execution,
fail the Action rather than applying a different effect.
Cloud reviews an approved call again directly before it runs and compares the result with the review the person approved; see Action approvals. Return the same review for the same input and unchanged state: no current time, counters, or random values. Otherwise every approval of the Action asks again. The repeated review protects only what it shows, so an Action that refers to changeable state, such as a draft, also carries the expected revision in its input.
Describe each call with policy and typed slots
New applications can declare policy and return CapabilityActionCallReview.
The old approval, CapabilityActionReview, and
presentation.sentences contracts are deprecated and will be removed with the
next major release. Existing applications keep working in this release.
Upgrade Core and every application that calls Actions (at least Assistant
and Capabilities) before an application returns new reviews or receipts. An
older Core can discover the Action through its derived approval, but older
Core and caller versions reject these new result shapes.
Choose the widest reach of a silent Action:
| Policy | Meaning |
|---|---|
{ silent: "you" } |
Only the person sees the result |
{ silent: "draft" } |
A private draft that takes effect through a later Action that asks |
{ silent: "shared" } |
Other people can see the result, including content later chats or routines read |
{ silent: "you", routines: false } |
A silent Action that routines must never receive as a grant |
{ ask: reason } |
Ask for every call, with a Cloud-owned reason line |
The reason codes are sends, deletes, cannot_undo, involves_people,
changes_access, keeps_working, sensitive, and app_asks. The app chooses
the code; Cloud supplies its English or German text. An unknown language uses
English frame words while retaining that review's locale for Intl formatting.
In this release, silent behaves exactly like approval: "rememberable":
the first call asks, and the person may remember its scope. It does not yet
mean automatic execution. ask behaves like omitted approval and asks every
time. A silent review with ask cannot be remembered, even when it has scope.
Declaration rules apply at app.start():
- Declare either
policyorapproval, never both. A policy requiresreview. destructive: trueoropenWorld: truerequires an asking policy, a review, andidempotency: "required".- Asking with
sendsordeletesrequiresidempotency: "required"even when both effect flags are false. routinesmay only be the literalfalse, and only on a silent policy.- Unknown policy fields, reasons, and reaches fail startup.
A policy review contains locale, sentence, and slots. Its five required
forms are action, running, done, confirm, and decline. Optional
rejected, notRun, and short forms override Cloud's defaults. The action
form omits the question mark; Cloud supplies it. Keep the forms in the app's
localized message catalog and return the locale actually used for all forms.
Slots are typed values:
| Kind | Fields and bounds |
|---|---|
object |
title (1–200), optional resource ref, icon, and gone: true after deletion |
place |
title (1–200), optional ref and icon |
person |
people (1–20); each has a name or address, optional UUID userId; optional total from the listed count to 100,000 |
time |
start, optional end, format: "date" or "date-time"; real calendar dates or RFC 3339 instants with offset, end not before start |
count |
Integer value (0–1,000,000,000) and localized noun forms; other required |
text |
value (1–200) rendered as ordinary text |
Person names have at most 200 characters, as long as a mail display name, and addresses 320. Icons use
ti ti-<name>. Each noun form has 1–40 characters. gone is valid only on an
object. A chip shows at most 40 characters, cut in the middle to preserve file
extensions. People show up to three names plus a count of the rest. Plain text
includes addresses and quotes object and place titles. Counts use the review's
plural rules. Calendar days never shift with time zones; instants use the
reader's explicit time zone. Plain text is bounded to 400 characters.
actionReview is available from @k2b/cloud/capabilities and its /server
entry point. It builds and validates the review without performing I/O. Literal
forms enforce exactly the slot names in sentence.action: a missing slot,
an extra slot, or another form introducing a new name is a type error. A
catalog typed as plain string falls back to a string-keyed slot record.
import { actionReview } from "@k2b/cloud/capabilities";
const forms = {
locale: "en",
sentence: {
action: "Transfer {amount} {item} to {to}",
running: "Transferring {amount} {item} to {to}",
done: "Transferred {amount} {item} to {to}",
confirm: "Transfer to {to}",
decline: "Keep item",
},
} as const;
const review = actionReview(forms, {
amount: { kind: "count", value: 2, noun: { one: "unit of", other: "units of" } },
item: { kind: "object", title: "Dock" },
to: { kind: "person", people: [{ name: "Lea Berg", address: "lea@example.com" }] },
});Optional review fields describe this call:
| Field | Purpose |
|---|---|
preview |
The thing being changed, in one of the seven shapes below |
consequences |
Up to six { text, slots?, mark? } sentences; mark a consequence added or removed |
ask |
Ask for this call with a reason, on either policy; overrides the declared reason |
reach |
Narrow a silent policy's reach, in order you < draft < shared; invalid on an asking policy |
effects |
Integer 1–1,000 for the number of changes in a batch; callers otherwise assume one |
check |
{ query, input }: a same-app Query that returns CapabilityActionCheckSchema, with valid Query input |
scope |
Stable app-owned domain (1–200 characters) for remembered approval; grants no permission |
links |
Up to ten semantic links to inspect the reviewed revision |
After an unknown Assistant Action outcome, Core runs the review's check Query
once: done shows its receipt, not_done shows not run, and pending keeps the
unknown line. A failed check also stays unknown; Check again repeats only
the Query, never the Action, and without check, no repeat button is offered.
The preview is one of:
| Kind | Content and bounds |
|---|---|
message |
from?, to, cc?, bcc?, more?, subject, body, format, attachments?, moreAttachments?, sendAt?, warnings?, truncated?; up to 50 per recipient class and 50 attachments, at least one recipient; counts at most 100,000; subject at most 998, body 10,000, attachment name 255; up to five warnings of 300 characters |
event |
title, start, optional end, allDay, valid IANA timeZone, place, location, recurrence, attendees, description; title and place fields at most 200, 50 attendees, description 2,000 |
task |
title, optional place, due, assignees, status, checklist, tags, description; 20 assignees and checklist rows of 200, ten tags of 60, status 60, description 2,000 |
page |
title, optional place, body, format, truncated, changes; body at most 10,000, 20 added/removed changes of 2,000 |
file |
name, optional place, path, byte size, mediaType, to; name at most 255, path and destination 1,024, media type 120 |
rule |
when as a sentence, does (at most 4,000), up to 12 consequence sentences in may, seesChat |
fields |
1–12 rows with label (at most 60), value, optional complete before value (each at most 2,000), optional date or date-time format |
Page and task titles and places are bounded to 200 characters. Message and page
format is plain or markdown. Markdown uses Cloud's safe renderer without
loading images. before promises the complete old value; use a revision check
in the Action input so the review names exactly what the Action replaces.
A sends message must be complete: show the effective from, every recipient
class and attachment, scheduled sendAt, and the app's own safety warnings.
Any positive more.to, more.cc, more.bcc, or moreAttachments requires an
open link to the complete reviewed revision, on every message preview,
including receipts. Every truncated preview also requires an open link.
Cloud rejects a message preview without from when the declared policy or the
review's own ask is sends; the application is responsible
for truthfully supplying its schedule and warnings. The from requirement
applies only to reviews; receipts may omit it.
Both the application and Core use the same validation function. Invalid reviews
or receipts follow the existing INVALID_APP_RESPONSE path; a dispatched
Action without retry safety retains the unknown-outcome behavior. Validation
also checks:
- Placeholders use
{name}matching^[a-z][A-Za-z0-9]{0,31}$; other braces are invalid. Every{name}names a slot;action,running, anddoneeach name every slot. - At most six slots and three chip occurrences in
action. Templates have at most 200 characters;shorthas at most 60. Each button names at most one slot and renders to at most 40 characters. Button object titles and the name of a single person are cut to 24 characters; a group of people renders as a count. - Sentence forms, slot strings, consequences and single-line preview fields reject controls, invisible format characters and line separators. Multiline preview fields allow newline and tab, and reject the other unsafe characters.
- The review locale is canonical BCP 47 and belongs to the app's presentation locales or falls back to one through its locale ancestors.
- The serialized review is at most 32 KiB of UTF-8 JSON, including all preview fields and slots.
Use assertCapabilityReview(review, context?) and
checkActionReviewMessages({ locales, forms, slots }) from
@k2b/cloud/capabilities/testing in one test per locale. The first throws with
the issue list; optional context also checks the declaration, locale set, and
check Query. The catalog check reports missing required forms, sentence-rule
violations, and questions longer than 80 visible characters, counting chip
labels and the question mark. The 80-character bound is a catalog check, not a
runtime rejection rule. Keep fixtures deterministic with fixed dates.
capabilityReviewRememberScope(review, approval) and
capabilityReviewView(review, { timeZone }) are transitional helpers for
surfaces that still read the legacy shape, available from
@k2b/cloud/capabilities and @k2b/cloud/capabilities/server.
The former resolves a rememberable scope and the latter supplies legacy
message, detail, and link fields; both will be removed with the next major.
Receipts and outcome checks
An Action result may return receipt: { text, slots?, preview? }, reusing the
same slot, sentence, and preview rules. Every receipt slot must appear in its
text; its slots belong to the receipt itself. Receipt completeness uses the
result's top-level links. A Query result must not contain receipt in its
envelope. Query data may use CapabilityActionCheckSchema, returning
{ outcome: "done" | "not_done" | "pending", receipt? }.
The five CapabilityActionOutcome states are running, done, rejected,
not_run, and unknown. A done result uses its receipt when supplied, otherwise
the review's done form. Rejected and not-run outcomes use the corresponding
forms or Cloud's fallback; an unknown outcome uses Cloud's uncertainty frame.
A queued message's receipt says “queued” or “scheduled”, never “sent” before
that is known. The check contract lets a later consumer settle an unknown
outcome; declaring it does not itself run a reconciliation or retry.
Mixed versions and routine grants
Manifests publish policies in an optional top-level policies map keyed by
Action local ID, with sorted keys. Action entries carry derived approval:
silent becomes rememberable, ask omits approval, and both advertise
review: true. A manifest without policies keeps its earlier hash. Older
readers ignore the new top-level map and keep discovering every Action.
This release drops an Action whose policy is unreadable, disagrees with its
derived approval or review flag, or declares silent destructive/open-world
work. A policy for an unknown Action is dropped. Producers remain strict.
Consumers use capabilityActionPolicy(manifest, localId) from
@k2b/cloud/contracts to resolve the policy.
Routine grants accept only silent policy Actions without routines: false,
independently of their derived approval. Each unattended call still runs its
review. A review with ask, or a missing or failed review, prevents
execution with NEEDS_APPROVAL. A cancelled routine stops like any other
cancelled routine step. Silent reviews need no scope in a routine.
Legacy Actions retain their existing routine rules.
Migrate an application
- Upgrade Core and the applications that call Actions (at least Assistant and
Capabilities) first, then rebuild the app with this release of
@k2b/cloud. - Replace
approvalwithpolicy, choosing the widest reach or the reason the Action always asks. Keep the existing effect flags and retry safety. - Move catalog sentences into the review forms for every supported locale.
Return typed slots and, where useful, a preview; delete the migrated
presentation.sentencesentries. - Replace
approvalScopewithscope. On silent Actions, use per-callaskwhere confirmation is always needed andreachwhere the call is narrower. - Add a receipt or use the review's
doneform. Seteffectsfor batch work and add a check Query for sending or deleting when the outcome can be read. - Test each locale with the helpers above. The manifest evolution check accepts the one-time move from rememberable approval to silent or asking policy, and from omitted approval to asking policy.
For the item.transfer Action, the review callback changes from:
approval: "rememberable",
review: async (input) => ok({
message: `Transfer ${input.quantity} × ${input.itemName}`,
approvalScope: "inventory",
}),To an asking policy using the forms shown above (the rest of the Action stays in its declaration):
policy: { ask: "involves_people" },
review: async (input) => ok(actionReview(forms, {
amount: { kind: "count", value: input.quantity, noun: { one: "unit of", other: "units of" } },
item: { kind: "object", title: input.itemName },
to: { kind: "person", people: [{ name: input.recipient }] },
})),For a multilingual app, resolve forms once from the app's catalog using
context.locale. The German action form would be
"{amount} {item} an {to} übergeben", with matching running, done, confirm,
and decline forms.
Write valid contracts
Cloud validates the declaration at startup:
protocolVersionis currently2;- the declaration and every Type, Query, Action, Command, Universal Search
block, search tag, and stream in it use only fields that your
@k2b/cloudrelease defines, so a field from a newer release fails startup instead of being ignored; - local IDs start with a lower-case letter and may contain
.,_, or-; - one local ID may occur only once across Types, Queries, Actions, and Commands;
- inputs are closed
z.object(...).strict()schemas; - every meaningful input field has a concise
.describe(...)string; - input and data schemas must project to JSON Schema;
- every Query and Action declares
openWorld, and every Action also declaresdestructiveandidempotency; - every Type
readernames an existing Query in the same declaration; - a reader Query has a required string
idfield and no other required input fields, and is assigned to at most one Type; idempotencyKeyis reserved for transports and cannot be an Action field;- Action reviews use the fixed platform schema and are advertised as
review: trueonly when the callback exists; approval: "rememberable"appears only on non-destructive, closed-world Actions with a review;- every
destructiveoropenWorldAction declaresreview()andidempotency: "required"; - every provider-owned Type used by
refsor Universal Search is declared; - an app may declare at most 200 Types, 200 Queries, and 200 Actions;
- the deterministic live manifest may not exceed 256 KiB.
Every invocation transport caps the complete JSON request and result at 256 KiB. The paginated discovery catalog has a separate 2 MiB page limit because one valid manifest may itself approach 256 KiB. Provider endpoints validate and serialize invocation results within the 256 KiB bound before returning them. Keep individual field limits comfortably below that envelope; large files, exports, and document bodies belong in app-owned upload or download APIs referenced by a capability.
Each operation publishes input and data JSON Schema plus a stable schema hash.
Tuples are allowed in input schemas: the model sees a bounded array of their
item schemas, and the app's own schema validates the exact tuple.
The result envelope is one fixed Core contract and is not repeated in every
manifest entry. Refresh the live catalog after SCHEMA_MISMATCH.
Keep contracts at the owning boundary
The provider owns the canonical Zod input and data schemas in its
src/capabilities.ts declaration. Core stores their projected JSON Schemas in
the live registry and validates both the caller input and every successful app
response before returning it. Core does not compile built-in app DTOs into a
central schema package.
A consuming app calls the public capability client and keeps only the small
DTO projection its own UI or service needs. Do not import another app's
capability-contracts, service files, or private source paths. This applies to
built-in and third-party apps equally: installation and registration make a
provider discoverable, while the live manifest supplies the runtime contract.
Additive consumer projections should accept unknown extra fields so a provider
can extend a result without breaking older consumers.
This boundary keeps deployment independent:
provider Zod declaration -> live manifest JSON Schema -> Core dispatcher
-> public consumer clientThe provider still parses and authorizes inside run. Core's validation is an
additional transport invariant, not a replacement for app-side checks.
When several providers can serve the same consumer, the shared schemas belong
in @k2b/cloud/contracts instead of either app. capabilityContractIssues
checks a published operation against such a contract, including the stream
direction a contract names. There are two ways to choose a provider: the
consumer selects it through its settings, as Mail does for the
contact directory, or providers declare
themselves in their manifest, as with fileProvider for
file providers.
Evolve published local IDs additively
Treat an app ID plus local operation ID as a stable public contract. The same ID may add new Types, operations, optional input object fields, result object fields, Universal Search tags, a Type reader, or an Action review. It must not change kind, remove fields or operations, add required input, weaken a previously guaranteed result field, change safety or idempotency semantics, or remove or replace an advertised reader, review, or search token. Publish a new local ID for those changes and migrate callers deliberately.
Keep a previous manifest fixture and check it in provider tests:
import {
assertCapabilityManifestEvolution,
compileCapabilityManifest,
} from "@k2b/cloud/capabilities/testing";
const current = compileCapabilityManifest("inventory", inventoryCapabilities);
assertCapabilityManifestEvolution(previousManifestFixture, current);Titles and descriptions may improve without changing the local ID. Consumers should still project only the fields they need with permissive runtime schemas so additive result fields remain compatible.
Read manifests from other Cloud releases
Applications and Core update independently, so a manifest or catalog page can
come from a newer Cloud release than the code that reads it. Every reader in
@k2b/cloud follows one rule: CapabilityManifestSchema,
CapabilityCatalogSchema, listCapabilityCatalog(), Core's registry, and the
capabilities CLI plugin.
- Readers ignore top-level fields they do not know: in the manifest, in each
catalog app, and in the catalog page and its
pageobject. The app's other capabilities stay available. - A Type, Query, Action, or Command with a field or value the reader does not know is left out, not stripped. The unknown part may change how the entry runs or who may use it, so reading the rest could widen access. Core returns 404 for an operation it left out.
- Core also leaves out a Query, Action, or Command it cannot use as described, for example because it cannot read its JSON Schema. It logs the left-out entries once per registered manifest with the app ID and their local IDs.
- Entries that depend on a left-out entry follow it. A Universal Search Query scoped to a left-out Type is left out, and a Type whose reader was left out has no reader.
- A
fileProviderwith a field the reader does not know is left out as a whole, and Core logs it with the left-out entries. Whether its operations exist and match the contract is checked byfileProviderIssues; see File providers. - Core ignores presentation fields it does not know and skips translations of left-out entries; the remaining translations still apply.
- A manifest with a different
protocolVersionis not read at all. Core also rejects the whole manifest when its app ID, unique local IDs, ormanifestHashdo not check out.
Producers stay strict. app.start() and compileCapabilityManifest() reject
every declaration field their release does not define, at every level, so an
application never registers a field it believes is in effect while its
@k2b/cloud ignores it.
Because older readers ignore them, new top-level manifest fields may only add something, such as a provider declaration. A change that restricts how an existing Type, Query, Action, or Command may run belongs inside that entry, where an older reader leaves the entry out instead of running it with fewer restrictions.
Core checks manifestHash against the manifest exactly as the app sent it,
including the fields Core ignores, and keeps that hash. It therefore still
matches the hash in the app's registry entry, and a manifest without newer
fields has the same hash as before.
Return structured results
Optional table previews
CapabilityResult<T> accepts optional presentation metadata:
type TablePresentation = {
kind: "table";
rowsPath: string[];
columns: Array<{ path: string[]; label: string; format?: "text" | "number" | "date" | "datetime" }>;
rowLinksPath?: string[];
};Metadata describes existing data, not a second result. rowsPath selects an array
relative to data; [] selects data itself. Column paths and rowLinksPath are
relative to each row. Segments are literal own JSON property names, not dotted
expressions, templates or executable code. Missing cells display as absent.
Row links use the existing semantic-link contract.
Assistant opens capability navigation links in a new browser tab so users keep
their conversation. This is client behavior, not a provider target setting.
return ok({
data: [{ name: "Cable", stock: "42.0000" }],
presentation: {
kind: "table", rowsPath: [],
columns: [{ path: ["name"], label: "Item" }, { path: ["stock"], label: "Stock", format: "number" }],
},
});For Grids-shaped data, use rowsPath: ["rows"], column paths such as
["values", column.key], and rowLinksPath: ["links"]. Derive columns from the
authorized result; never reread hidden fields to improve a preview. Exact decimal
strings stay intact; format hints do not authorize lossy numeric conversion.
Metadata is bounded to 100 columns, 16 path segments, and 500 characters per segment or label. It counts toward the existing result byte budget. Providers validate the supported shape. Generic clients can ignore presentation entirely; the typed client ignores unknown or invalid presentation while still validating canonical data. An unknown future kind is not a data failure.
Assistant renders up to 100 returned rows with scrolling and a notice when the
preview is capped. The full result remains in tool details. Existing page
metadata indicates additional server pages; do not duplicate pagination or invent
totals in presentation. No endpoint, widget, HTML, callback or mutation authority
is added.
Result envelope
Every successful operation returns data. Add only the navigation and identity
metadata the caller can use:
type CapabilityResult<T> = {
data: T;
presentation?: TablePresentation;
summary?: string;
refs?: Array<{
type: string;
id: string;
title?: string;
preview?: string;
icon?: string;
}>;
page?:
| { hasMore: true; nextCursor: string }
| { hasMore: false };
links?: Array<{
rel: "open" | "edit" | "status" | "preview" | "download";
href: string;
title?: string;
}>;
};Every result reference contains the stable identity fields type and id.
When the provider already knows a useful current label, it may add title, a
short plain-text preview, and an icon. These presentation fields are an
optional snapshot for generic clients; they do not participate in identity,
authorization, deduplication, or canonical-reader resolution. A bare
{ type, id } reference remains valid.
Prefer the name a person sees in the owning app: a mailbox name, note title,
contact name, or mail subject. Use preview only for concise secondary context
that helps distinguish similar resources. Use the Type's declared icon unless
the individual resource has a more specific stable icon. Do not copy IDs into
presentation fields, infer labels from unstructured content, or return stale
cached presentation when current authorized state is already available.
Use the optional summary for one concise, provider-authored description of a
successful result. Describe the outcome the user cares about, the readable
target, and any important resulting state. Prefer names and public labels from
the validated result or authorized domain state; do not expose internal IDs or
implementation steps.
For an Action, be specific about one changed field, and group several changes
into one readable result. For a successful single-resource Query, name what was
read, the resource kind, and its current readable name or label. Add current
state only when it changes how the user understands the result. Write the
complete result line, such as Read message “Quarterly report”., rather than
returning only Quarterly report.
Do not promote a domain object's content summary into the top-level summary;
the top-level text describes the operation result. For list and search Queries,
omit summary when the structured results already communicate what matters.
Use it only when a bounded count or scope is itself the useful result.
Good summaries:
Added #customer to Reiner Schmiedt.Completed “Launch plan”.Read message “Quarterly report”.Read mailbox “Support” with 3 unread conversations.
Bad summaries:
Updated tags successfully.— it hides the affected person and actual tag.Contact mutation completed.— it describes an implementation step instead of the user's outcome.Loaded item.— it does not identify the resource.Read message abc123.— it exposes an internal identifier instead of a readable target.
The summary is trimmed, limited to 500 characters, persisted with the result, and rendered as escaped plain text. Derive it from the operation's validated result instead of asking an agent to describe what supposedly happened. Omit it when the operation title and structured data already say everything useful.
Provider-owned refs use qualified declared Types. Foreign qualified refs are
opaque cross-app identities and need not be redeclared by the provider. An app
may enrich a foreign ref only when it has an authoritative user-facing label;
otherwise it returns the bare identity. Provider links are root-relative
same-origin Cloud paths. Readers return the resource's top-level open link
for Resource links; links on other results remain optional
hints and do not require UI.
When hasMore is true, nextCursor is required; on the final page it must be
absent. Treat cursors as opaque values.
When a provider-owned Type advertises a reader, every returned ref.id for
that Type must be accepted by that reader. For a foreign ref, the foreign
Type's owning app defines whether and how it can be read. A ref never carries
or caches a reader name; consumers resolve it from the current owning app
manifest.
Keep result metadata non-overlapping. For one primary resource, return one
optionally presented top-level ref and its navigation in top-level links.
Do not repeat the same identity in a second resource array. For several
independently navigable presentation results, use CloudResourceView[] as
data so each title, ref, and link stays together. A rich domain list that must
retain app-specific fields may instead add optional semantic links directly
to each item. This keeps navigation next to the item without replacing the
domain result or making clients correlate parallel top-level arrays.
All result links are optional hints. Omit links when there is no stable,
useful Cloud destination, and omit the field instead of returning an empty
array. Clients must not require links for an operation to succeed. Do not invent
a generic app link for a resource that has no directly addressable UI, and do
not add another result field that duplicates both identity and navigation.
Domain-only operations may return data without links.
Failures use the normal structured service-error shape:
{
"code": "FORBIDDEN",
"message": "Write access is required",
"details": {}
}For VALIDATION_FAILED, Core includes bounded details.issues entries with a
field path and message. Generic AI clients surface those entries so an agent
can correct one argument instead of repeating the unchanged call. Issue
details never include the rejected input value. Capability authors should
still make semantic requirements and ID provenance clear in field
descriptions; validation feedback is recovery, not primary documentation.
Framework errors include VALIDATION_FAILED, SCHEMA_MISMATCH,
IDEMPOTENCY_KEY_REQUIRED, IDEMPOTENCY_KEY_NOT_ALLOWED,
IDEMPOTENCY_CONFLICT, IDEMPOTENCY_IN_PROGRESS, IDEMPOTENCY_UNCERTAIN,
APP_UNAVAILABLE, CAPABILITY_NOT_FOUND,
DEADLINE_EXCEEDED, ACTION_OUTCOME_UNKNOWN, REQUEST_CANCELLED,
INVALID_APP_RESPONSE, and RESPONSE_TOO_LARGE. Applications may return their
own domain error codes. Provider failures accept the explicit HTTP statuses
400, 401, 403, 404, 409, 429, 499, 500, 502, 503, and
504; other statuses fail closed as an invalid provider response.
Cloud resolves the human message of framework-owned failures from the
caller's request locale without changing the code, status, details, or retry
semantics. Provider-owned failures remain the application's responsibility and
must already contain their final localized message. Follow
Product language and tone for
specific, recoverable error wording.
DEADLINE_EXCEEDED is retry-safe for Queries and required-idempotency Actions.
ACTION_OUTCOME_UNKNOWN means a non-idempotent
Action may already have taken effect and must not be retried automatically.
INVALID_APP_RESPONSE means the provider returned data outside its registered
contract; callers must not retry the same request unchanged. The provider logs
the validation path while the public error omits returned values. Once a
non-idempotent Action has been dispatched, a lost, unreadable, oversized, or
schema-invalid response is reported as ACTION_OUTCOME_UNKNOWN; clients must
not offer an automatic retry.
Invoke capabilities
Core reads the live capability registry and dispatches every generic client through the same path:
GET /api/capabilities/v1/catalog?limit=10&cursor=<appId>
POST /api/capabilities/v1/queries/<appId>/<localId>
POST /api/capabilities/v1/actions/<appId>/<localId>
POST /api/capabilities/v1/actions/<appId>/<localId>/reviewOAuth callers need read or admin for the catalog, Queries, and Action
reviews, and write or admin for Actions. Sessions and API keys keep their
existing application authorization behavior. Scopes only cap the operation
kind; the owning app still enforces its domain permissions.
The public catalog contains only revalidated live manifests plus current app name, icon, and description. Core derives the internal endpoint from the live app registry; providers cannot publish a dispatch URL or trusted app metadata inside the Capability record. Raw registry records are internal and are not a consumer API.
The POST body contains one input field:
{ "input": { "id": "k3P9xQ" } }The optional review route accepts the same body and is available only when the
Action manifest advertises review: true. It performs no mutation and needs no
Idempotency-Key.
Send Idempotency-Key only when invoking an Action whose manifest requires it.
Core pins the registered schema and replaces the source credential with a
30-second invocation JWT bound to the target app, operation, mode, and schema
hash. The target verifies that token locally, reloads the current principal,
reconstructs the normal actor and access subject, validates the input, and
authorizes the resource. Cookies, API keys, and OAuth tokens never reach the
target app. Reviewing an Action never authorizes its later invocation.
A valid invocation for an outdated provider schema returns HTTP 409
SCHEMA_MISMATCH, not a login failure. Invalid JWTs or unavailable live authority
still return HTTP 401. Core includes x-request-id only when it is 1–200 visible
ASCII characters; invalid optional tracing metadata is dropped rather than
preventing a capability, search, or widget call.
Core also forwards the caller's resolved locale as invocation metadata in the
internal x-cloud-locale header, next to authorization and tracing. Query,
Action, and review handlers receive it as context.locale in their
CapabilityExecutionContext without declaring it in an input schema; without
a caller preference it falls back to the operator's app.locale default. Use
it only for human-facing formatting or optional message catalogs — stable
error codes and result data must not depend on it, and callers never need to
interpret provider message codes. See
Locale and time for the resolution
contract and Internationalize an application
for the message and error boundary.
Two more invocation-metadata fields reach every Query, Action, and review
handler on the same CapabilityExecutionContext:
| Field | Meaning |
|---|---|
context.requestId |
Correlation id of the originating Cloud request. The dispatcher reuses a valid incoming x-request-id and generates one otherwise, then forwards it unchanged to the app and stores it on the execution record |
context.origin |
Cloud surface that invoked the capability: assistant, mcp, http, or app |
Write context.requestId into the app's own audit rows. That is the join
between an app's domain trail and the platform execution record, and it works
without either side sharing payloads. Use context.origin for logging or
diagnostics only — never for authorization, because permission decisions belong
to the actor and access subject.
Browser and client islands use the same-origin public client:
import { invokeCapabilityWithDataSchema } from "@k2b/cloud/capabilities";
import { z } from "zod";
const itemSchema = z.object({ id: z.string().regex(/^[A-Za-z0-9]{6}$/), name: z.string() }).passthrough();
const result = await invokeCapabilityWithDataSchema(
{
appId: "inventory",
capabilityId: "item.read",
kind: "query",
input: { id: itemId },
},
itemSchema,
);
if (!result.ok) throw new Error(result.error.message);Only Core-owned code may pass verified authority for in-process dispatch.
It invokes Core's permission, review, idempotency, signing, and audit machinery;
this is not a credential bridge for independently deployed application hosts.
Never construct authority from JSON, tool arguments, workers, or browser state,
and never mix it with credentials, a mandate or transport.
Framework-owned execution hosts can supply a server-only transport(request)
to the capability client. It handles Queries, Actions, reviews, and binary
continuations through Core. Managed Assistant execution uses a short-lived,
Core-issued callback bound to its conversation and foreground turn. Core checks
the current account, conversation ownership and active turn on every callback.
The host refreshes this credential through Core's polling requests; it never
passes it to user code or persists it with tool arguments. Closed, cancelled or
background turns cannot use this callback. Ordinary application callers use
request credentials as shown below; background jobs use revocable mandates.
This is a Core-only foreground-turn authority exchange: after verifying the
callback and live turn, Core resumes dispatch under that user's authority.
It does not create a session cookie or allow ordinary app invocation tokens to
mint other invocations. Core rejects a requested cancellation immediately on
subsequent callbacks. Stream continuations also recheck the conversation's
current allowedTools; the provider remains responsible for current resource
permissions.
Server-side app code uses the Core-backed adapter. Pass only credentials and
trace data from the current request. The adapter sends them to Core's private
origin, where Core resolves the authority and dispatches a target-bound
invocation. Configure CLOUD_CORE_INTERNAL_ORIGIN to avoid a public
Gateway/ingress round trip:
import { invokeCapabilityWithDataSchema } from "@k2b/cloud/capabilities/server";
const result = await invokeCapabilityWithDataSchema(
{
appId: "inventory",
capabilityId: "item.read",
kind: "query",
input: { id: itemId },
},
itemSchema,
{
cookie: request.headers.get("cookie"),
authorization: request.headers.get("authorization"),
requestId: request.headers.get("x-request-id"),
signal: request.signal,
},
);Both clients return { ok: true, data } or { ok: false, error }; ordinary
network and protocol failures do not require exception handling. The untyped
invokeCapability() variant returns unknown; use a small permissive runtime
data schema at every typed consumer boundary. Use
reviewCapabilityAction() from the same entry point for an advertised Action
review. App tests may compile a declaration without importing Cloud internals:
import { compileCapabilityManifest } from "@k2b/cloud/capabilities/testing";A durable worker uses the same server helper with an app workload credential and its persisted mandate ID/revision. The helper sends both only to Core; Core validates the current mandate, signs and dispatches the exact capability, and returns the bounded result. It never returns the invocation JWT to the worker. See Background authority mandates for the complete lifecycle and example.
The generic CLI uses the same dispatcher:
cld capabilities catalog
cld capabilities read inventory.item k3P9xQ
cld capabilities query inventory item.read \
--input '{"id":"k3P9xQ"}'
cld capabilities action inventory item.rename \
--input '{"itemId":"k3P9xQ","name":"Dock"}'The Capabilities playground at /app/capabilities lists the live catalog,
renders schema-driven inputs, invokes operations, and builds matching cURL
requests. It is a discovery and debugging surface, not the app's normal UI.
Cloud MCP
The authenticated /api/mcp/v1 endpoint projects the live catalog as MCP
tools:
cloud__resource__read
inventory__query__item.read
inventory__action__item.renameQueries become read-only tools. Query and Action openWorld values become
openWorldHint; Action metadata also becomes destructive and idempotent hints.
A generic client passes any returned typed ref unchanged to
cloud__resource__read; Cloud resolves the Type's current canonical reader
from the live manifest instead of requiring the client to discover or guess
the Query name.
A required idempotency key is a separate idempotencyKey tool argument. MCP
uses the same Core dispatcher and has no broader authorization or approval
contract.
Cloud capability MCP exposes live application operations. Fibel MCP exposes read-only developer documentation. They are separate endpoints with separate purposes.
Execution history
Every capability invocation is recorded once, by the dispatcher, in the
platform-owned capabilities.executions table. The assistant, MCP, the
Capabilities workspace app, and app-to-app mandate calls all produce the same
row, so operators read one history instead of one per surface. An Action review
is a read-only preview and is not an execution, so it is not recorded.
A row holds correlation and shape, never payloads: request_id, origin,
app_id, the qualified capability, kind, the declared destructive flag,
the acting principal and the access subject when they differ, status,
error_code, the input and output shape metadata, an optional
idempotency_key, whether the row was replayed from an idempotency claim,
and start, end, and duration.
| Status | Meaning |
|---|---|
succeeded |
The app returned a valid result |
invalid_input |
Input failed schema, idempotency, or envelope validation |
denied |
The caller was not authorized for this capability |
rejected |
A user declined the Action approval, so it never ran |
timed_out |
The app did not answer within the capability deadline |
failed |
Any other failure, including app errors and cancellation |
Origins are assistant, mcp, http, and app. Retention is bounded: Cloud
prunes execution rows older than 90 days.
Operators read this history at
Observability. Applications never write to
this table; they record their own domain effects with
Audit events and correlate them through
context.requestId.
Open an interactive flow with Commands
A Query reads, an Action changes domain state, and a Command opens an
application-owned interface. Declare Commands beside Queries and Actions. Their
local IDs share the same namespace, so use distinct IDs such as task.compose
for a form and task.create for a mutation.
const ComposeInput = z.object({
spaceId: z.string().optional().describe("Optional destination Space ID."),
}).strict();
const capabilities = defineCapabilities({
protocolVersion: 2,
commands: {
"task.compose": {
title: "New task",
description: "Create a task in Spaces.",
icon: "ti ti-checkbox",
keywords: ["todo"],
input: ComposeInput,
path: "/app/spaces",
},
},
});The path must belong to the application's live registered routes. Resolution
validates the declared input and returns a URL based on the app.url setting
(APP_URL), never the caller's browser origin. It does not call an app handler,
create a record, or authorize a later mutation. The destination page and its
normal services still check access. Commands are not mutating AI tools.
Resolve a link from another application or an external client:
import { resolveCommand } from "@k2b/cloud/capabilities";
const result = await resolveCommand("spaces.task.compose", { spaceId: "AbCd12" }, {}, {
baseUrl: "https://cloud.example/api",
headers: { authorization: `Bearer ${token}` },
});
if (result.ok) console.log(result.data.href);The HTTP equivalent is POST /api/capabilities/v1/commands/:appId/:localId
with { "input": {} }, returning { "href": "https://…" }. It requires an
authenticated caller with read scope. Invalid input returns 400, missing
Commands 404, and unavailable or mismatched route owners 503.
For an in-browser interaction, use openCommand:
import { openCommand } from "@k2b/cloud/browser/commands";
await openCommand("spaces.task.compose", {
source: { type: "mail.conversation", id: "AbCd12" },
});Register a handler in the island that owns the form. Use the same input schema as the declaration and dispose registrations when their owner unmounts:
import { consumeCommandLink, registerCommandHandler } from "@k2b/cloud/browser/commands";
import { onCleanup, onMount } from "solid-js";
onMount(() => {
onCleanup(registerCommandHandler("spaces.task.compose", ComposeInput,
(input) => openTaskForm(input),
(input) => !input.spaceId || input.spaceId === currentSpaceId(),
));
void consumeCommandLink();
});Pass an AbortSignal as the fourth argument when the request may outlive its
reason, for example openCommand(id, input, { returnTo }, signal) from a view
the person can leave. openCommand resolves true after a mounted handler
accepted the request and its promise resolved, or navigation started. It
resolves false when nothing opened because the signal aborted or its code
could not load; Cloud shows a reload notice when code cannot load.
The signal covers loading the code and resolving the link; an accepting mounted
handler takes over at once and runs its own interaction without receiving the signal.
openCommand offers the request to mounted handlers synchronously. Exactly one
accepting handler owns it; with no owner Cloud resolves the link and navigates.
If resolution fails, it throws an error with the resolver's message and the
capability error ({ code, message, status }) in cause.
Overlapping owners produce an error before either runs; use accepts to keep ownership unambiguous.
A rejected handler surfaces an error and never triggers fallback navigation.
Document events connect independent islands without a shared Solid context.
consumeCommandLink consumes the URL parameters before handing control to the
form, so reload and Back do not reopen a consumed interaction. It reports
invalid or unavailable entries and clears their parameters as well, so reloading
does not repeat the error. It never silently invokes a different Command.
An unhandled handler rejection is displayed once with its message. A handler
that presents its own inline error and retry should handle that failure itself.
Keep URL input to small public references and options. Links are limited to 8 KiB; do not put message bodies, calendar files, tokens, or secrets in them. The receiving app reads referenced resources with the current user's authority and shows the source visibly before submission.
An optional { returnTo: "/app/contacts?contact=AbCd12" } may be supplied as the
third argument. Only internal root-relative paths are accepted, and a path
whose dot segments resolve to //host is rejected. The destination app
decides when returning makes sense, normally after completion or explicit
cancellation. This is a return link, not a webhook or cross-page callback.
Omit it when users should stay with their newly created content.
Localize titles, descriptions, and input field descriptions under
presentation.translations.<locale>.commands, keyed by local Command ID.
Follow the palette's action wording guidance
so users can distinguish actions by scope and understand what each one opens.
Where Commands appear
The global palette and the composer's slash menu discover Commands that accept
an empty input ({}), from apps in the person's navigation. The slash menu
lists them in its last section, Open in apps, and leaves out its own app's
Commands. Navigation controls discoverability; the target page still checks
access.
Write a title with a verb and an object, such as New task. The description
is one sentence about what opens. Include German and English search words in
keywords; translate titles, descriptions, and keywords in
presentation.translations.<locale>.commands.
Before opening an app, the composer saves the draft, including references and
attachments. A failed save opens nothing. It supplies returnTo for the saved
chat, but the target app decides whether and when to use it and may ignore it.
Calendar Command entry points
Mail and Spaces expose two context-dependent Commands. Neither accepts an empty input, so they do not crowd the global action catalog.
| Command | Input | Interface |
|---|---|---|
mail.draft.calendar |
{ mailboxId, draftId } |
Attach an event to this existing Mail draft |
spaces.event.invite |
{ itemId, method?: "request" | "cancel" } |
Prepare an invitation, update or cancellation for this event |
IDs are public resource IDs. Opening a Command checks access and opens the
existing form; it does not send mail. Mail keeps the same draft, recipients and
attachments and uses its existing edit lease and revision checks. Spaces owns
the event and invitation sequence; Mail owns the resulting draft and sending.
Use returnTo only when there is a useful originating view to return to.
The Mail composer also offers its calendar action for an unsaved draft. It saves through the existing composer flow before opening the inline event form. A save or lease failure leaves the user in the composer. Incoming invitation import, RSVP draft preparation and linking a conversation to an existing Spaces item remain local context actions with their existing selectors and confirmation steps.
Binary streams
A Query or Action can optionally return one stream alongside its ordinary
data, references and links. Use this for binary content that does not fit the
256 KiB JSON result budget. The same contract supports a file download, an
invoice PDF, audio or an archive import. Resource Types do not prescribe a
format, and consumers must discover the operation's input schema.
Declare stream: { direction, maxBytes, ...handlers } on the operation. A read
Query implements read(descriptor, context): Promise<Response>. A write Action
must require idempotency and implement:
write(descriptor, body, context): consume theReadableStream<Uint8Array>completely, validate it, then publish and return a normal capability result.status(descriptor, context): return{state:"open"},{state:"aborted"}or{state:"completed", result}with the same result schema as the Action.abort(descriptor, context): discard an unfinished transfer. Preserve an already completed result; the framework reads status after aborting.
The operation's run returns a descriptor with id, direction, mediaType,
size (exact byte count), expiresAt (ISO timestamp), and optional name.
The provider's ID identifies an authorized source revision or a durable upload
reservation. Core replaces it with an encrypted, caller-bound reference. The
manifest publishes direction and maximum bytes and includes them in the schema
hash. Expiry must be in the future and at most 24 hours away.
Handlers receive the original descriptor and a freshly authenticated execution context. Check current resource permissions in every handler. Keep source identity/revision and destination/conflict policy in the reservation, not in caller-supplied transfer parameters. Serialize concurrent writes to the same reservation and reconcile retries from its durable receipt. A write response must not promise success before publication. Do not emit another stream from a write receipt.
Consumers use transferCapabilityStream(ref, "read" | "write" | "status" | "abort") from @k2b/cloud/capabilities; the server counterpart accepts the same
CapabilityCaller as ordinary invocation. Read returns binary bytes; the other
verbs return JSON. Writes supply body without encoding it as JSON or base64.
The HTTP endpoint is POST /api/capabilities/v1/streams/{verb} with the opaque
reference in x-cloud-stream-id. Never put it in a URL or log it.
Transfers preserve backpressure, propagate cancellation and enforce the exact
byte count even without Content-Length. Each request has a five-minute
transport deadline. No write is retried automatically: after a lost response,
inspect status before deciding whether to resume or prepare a new operation.
Expiry also limits status and abort; providers must reclaim abandoned
reservations independently. Keep receipts for their domain recovery period.
References require the same caller and credential scope, an unchanged live operation schema and current provider access. Possessing a reference alone cannot invoke a different operation. This initial stream contract supports interactive and service-account authority; mandate-backed background invocations of streaming operations fail before execution. Existing operations without streams retain their contracts.
Assistant code mode exposes cloud.capabilities.streams.read/write/status/abort.
read returns a File, and write accepts a Blob, string, ArrayBuffer or
Uint8Array. It uses only references returned by that run's approved
cloud.capabilities.run calls. The existing runtime budget applies: 50 MiB per
payload and 250 MiB across transfers, with at most 64 references per run.
HTTP and CLI clients may use the provider's larger advertised limit.
Core binds code-mode stream references to their conversation and foreground
turn. They cannot be replayed through the ordinary HTTP endpoint or a different
turn, even by the same user. The host stops canceled turns and aborts their
network transfers. Files (filesv2) checks cancellation again before committing an
upload. Cancellation is not rollback: a write already committed remains
committed, and a cancellation racing that commit can still complete.
Managed hosts carry bytes over authenticated loopback HTTP with backpressure,
without serializing whole binary bodies through process IPC. Admission is
limited to eight simultaneous host requests per Assistant process; excess
requests return operation_busy before consuming the request body. The final
File returned to code still occupies memory within the runtime budget.
The turn-bound grant format replaces the previous alpha format outright. Previously issued references are invalid after this upgrade; obtain new ones.
Provider IDs are limited to 2 KiB of UTF-8; use a stored reference for long paths. Core's sealed reference is bounded to 8 KiB. Treat provider IDs as untrusted selectors even on an authenticated invocation and authorize them against the actor. Replaying an idempotent Action returns its original grant and does not extend its lifetime. After expiry, use the app's recovery API or prepare a new Action with the current conflict policy.
Stream read/write attempts have separate execution rows linked by the original request ID. They record direction, outcome and byte count without recording the content or stream reference. A read completes only when its body has been consumed; canceling it records an interrupted transfer.