Security model

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:

PermissionOperations
tablesList tables, read table definitions, query rows, add rows
workflowsRun a workflow
relaysAssign a Relay task
files: readRead file details and content
files: writeThe above, plus create files
model: readRead the semantic model, and query rows through it
chat: askOpen Chat with a prompt; the person decides whether to send it
chat: answerThe above, plus get Chat's answer to a prompt, from a read-only turn
ai.modelsCall those chat models
ai.mediaGenerate those media kinds
knowledgeSearch those knowledge bases
integrationsRun 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', so fetch, XMLHttpRequest and 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:

  1. Publish a draft version, which makes it installable.
  2. 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

ActionMinimum role
Open and use installed appsread
List apps, read app details, upload a draft versionwrite
Publish or retire a versionadmin
Install, uninstall, change the version, pins, budget or statusadmin
Read an install's spend and audit trailadmin
List, set or delete secretsadmin

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.