An app holds no authority of its own. It acts as the person using it, narrowed to what its manifest names, from a sandboxed frame that cannot reach the network. This page explains each layer.
The app acts as the person using it
Every SDK call is made as an app principal: the signed-in person, the app, and the installed version, together. The workspace authorizes each call again as that person, at the moment of the call:
- their current workspace role and permission group,
- their access to each table, workflow, Relay and file,
- row security and hidden columns on every table.
It then narrows the result to the manifest. Effective access is what the person can reach and what the manifest names, never more. Naming a table in the manifest gives the app nothing the person could not already read, and a person with access to a table the manifest does not name cannot reach it through the app.
Each manifest permission opens a fixed set of operations, defined in Agent Serve's code. A manifest names resources and access levels, never operations, so it cannot ask for anything outside this list:
| Permission | Operations |
|---|---|
tables | List tables, read table definitions, query rows, add rows |
workflows | Run a workflow |
relays | Assign a Relay task |
files: read | Read file details and content |
files: write | The above, plus create files |
model: read | Read the semantic model, and query rows through it |
chat: ask | Open Chat with a prompt; the person decides whether to send it |
chat: answer | The above, plus get Chat's answer to a prompt, from a read-only turn |
ai.models | Call those chat models |
ai.media | Generate those media kinds |
knowledge | Search those knowledge bases |
integrations | Run those integration actions with the person's connected account |
A semantic model query reaches the tables behind the model for that one call. Each of those tables is still authorized as the person, so the model reaches no row they could not read.
Model and media calls are held to the person's permission group (allowed providers, denied models, the media switch) and the deployment's blocked providers, as well as to ai.models. A Chat answer runs as a read-only turn: Chat is told the person may only read, is offered no integration tools, and any tool that would change something is refused rather than run.
An integration action runs on the server with the person's own connected account; credentials never reach the frame. When the person has several accounts for the provider, the app names one with account. An action that changes anything (sending, creating, updating, deleting, paying) stops for the person's approval in the workspace page first, the same way Chat asks before it changes something. The app cannot skip that step: only the page around the frame can mark a call approved, and it does so only after the person clicks Run it. If they decline, the call fails with declined. Each approved action is recorded in the audit log.
The sandboxed frame
An app's pages, panels and Chat cards run in an iframe with sandbox="allow-scripts". It always has an opaque origin, wherever it is shown:
- No cookies or browser storage, and no same-origin access to Agent Serve.
- No network. The content security policy sets
connect-src 'none', sofetch,XMLHttpRequestand WebSockets are refused. The only way out is the SDK, through messages to the page around the frame. - Only its own files. Scripts, styles, images, fonts and media load only from the app's own bundle. Inline scripts are refused, so ship scripts as files; inline styles are allowed. Forms, nested frames, workers and plugins are refused.
- No device access. Camera, microphone, location, payment and USB are turned off.
The page around the frame answers messages only from that frame's own window, checks every message against the SDK protocol, and limits each frame to 20 calls a second. Calls it does not answer itself go to the workspace, which checks them again.
Bundle files are served under a signed, short-lived frame token instead of a cookie. The token binds the person, the workspace, the install and the version that were authorized when the app was opened, and it expires after one hour.
A bundle may contain only static files: HTML, JavaScript, CSS, JSON, source maps, text, Markdown, YAML, CSV, images (SVG, PNG, JPEG, GIF, WebP, AVIF, ICO), fonts (WOFF, WOFF2, TTF, OTF) and media (MP3, MP4, WebM). An upload with any other file type is refused. See archive limits.
What an admin approves
An app reaches a workspace only after an admin acts twice:
- Publish a draft version, which makes it installable.
- Install a published version in the workspace.
The install screen lists the version's permissions: each table, workflow and Relay by name, and whether it may read the model, open Chat, or read or write files. Installing also creates the tables, rules and Relays the app declares under requires that the workspace does not already have, using the installing admin's own access and plan limits.
Changing the installed version — upgrading, or rolling back — is an admin action too, and a new version can ask for different permissions. An admin can also turn an install off, which makes every SDK call fail with conflict, or uninstall it.
A version that any workspace still runs cannot be retired. An app is managed from the workspace that first uploaded it.
Roles
| Action | Minimum role |
|---|---|
| Open and use installed apps | read |
| List apps, read app details, upload a draft version | write |
| Publish or retire a version | admin |
| Install, uninstall, change the version, pins, budget or status | admin |
| Read an install's spend and audit trail | admin |
| List, set or delete secrets | admin |
Permission groups can narrow this further: a group can hide the Apps module, which blocks using and building apps, or turn off building and managing apps on its own. Opening an app and making SDK calls work only from a signed-in browser session, never from an API key.
Secrets
Admins can store per-install secrets with agentserve app secrets set or on the Manage page.
- Secrets are write-only. A value is encrypted when it is stored, and nothing returns it: listing shows names and when each was last changed.
- Names use capital letters, digits and underscores and start with a letter, up to 64 characters (
STRIPE_KEY). A value is at most 16 KB. - Setting and deleting a secret is recorded in the audit trail.
Secrets never reach the browser bundle or the app's frame, and the SDK has no method to read them. They reach only the app's code tools, as environment variables of the tool's own process on the server. A secret value in a tool's error is replaced with [secret], and a run whose output contains one fails with its output withheld.
Budgets and usage
Paid work an app starts is attributed to its install in the usage ledger, alongside the person it was done for. That includes work that runs later, such as a queued workflow run or a Relay task the app assigned.
An install's monthly budget is the one an admin set on its Manage page, or else the manifest's budget.monthlyUsd. Without either, spend is not capped. Before the app runs a workflow, assigns a Relay task, runs a code tool, calls a model, generates media, asks Chat for an answer, searches a knowledge base or runs an integration action, the workspace compares the spend since the start of the calendar month (UTC) with the budget. Once spend reaches it, those calls fail with budget_exhausted until the month ends or an admin raises the budget. Reads, row writes, file calls and opening Chat do not check the budget.
Model, media, Chat, knowledge and integration calls also have a per-install ceiling each minute, separate from the bridge's per-second limit, so one busy app cannot run up a large bill before its budget notices. Over it, calls fail with rate_limited.
A Chat answer is billed once, as a Chat turn, and its cost also counts against the app's budget.
The check happens before each call, so one call that starts below the budget can finish above it.
agentserve app usage and the Manage page show this month's spend against the budget, by source.
Audit trail
Every change to an app is in the workspace audit trail: uploading, publishing and retiring versions, installing, updating, rolling back and uninstalling, setting and deleting secrets, and the rules an install creates.
Every write an app makes — adding rows, running or queuing a workflow, assigning a Relay task, creating a file — is recorded as an app write naming the app, the version and the method, in addition to the record the underlying action keeps. Every code tool run is recorded too, with the tool, who called it and from where, the status and the duration, but never its input, output or secrets. agentserve app logs and the Manage page show an install's trail.
Because the app acts as the person using it, those records name that person. Usage is billed the same way as their own work, and also counted against the app's budget.