@fayz-ai/agent-runtime
v0.3.0
Published
Headless agent runtime — turns a synced AgentContract into a server-executable tool catalog, and runs its reads without a browser
Readme
@fayz-ai/agent-runtime
Headless agent runtime. Turns an app's synced AgentContract into a tool catalog a
server can execute, and runs the read tools without a browser.
Status: preview — incubating, API may move.
npm install @fayz-ai/agent-runtimeWhat it is for
The in-app assistant, an MCP client and WhatsApp are three heads on one agent. The head differs in who talks to the user and whether there is a UI; the middle — catalog, authorization, execution — has to be one thing, on the server. This package is that middle, minus the transport.
It holds no credentials and no HTTP. The host injects a tenant-bound transport.
Usage
import { buildServerCatalog, createEntityReadExecutor } from '@fayz-ai/agent-runtime'
const catalog = buildServerCatalog({
contract, // AppManifest.agent, synced from the app
grants, // AgentTool rows — EMPTY MEANS NO RESTRICTION (see below)
})
const executor = createEntityReadExecutor({
catalog,
transport, // must already be scoped to one tenant
actor: { tenantId, userId, timeZone: 'America/Sao_Paulo' },
})
if (executor.handles(call.name)) {
const result = await executor.execute(call.name, call.arguments)
}Ship catalog.tools to the model. catalog.clientToolNames are the ones a surface
with a UI still runs itself; catalog.dropped says why anything was left out — a
capability that disappears quietly is how a model ends up promising what it cannot do.
Two rules worth knowing before you change anything
An empty grant list means "no restriction", not "nothing". AgentTool rows are
keyed by table and were empty in every project when this was written. A naive
intersection would empty the catalog and take the in-app assistant down with it.
Tightening to deny-by-default is a deliberate, announced decision — not a default.
A write is server-plane only once the server can confirm it. Until the
pendingAction round-trip exists, persist tools stay client-plane where the surface
still renders the confirmation card. allowServerWrites flips that, and must not be
flipped before confirmation lands.
The chokepoint
createEntityReadExecutor is the one place a server-plane read is composed, and it
holds four invariants:
- An entity resolves only from the synced contract — a table or column name never reaches a query because the caller said so.
- A tenant-scoped entity with no tenant is refused, not queried anyway.
- Filter, sort and aggregate columns must be declared by the entity.
- The tenant predicate belongs to the transport, never to this module. Two sources of truth for tenant isolation is one too many.
