Platform Tooling
Run your whole tool layer on Protobox: batch execution for agent turns, per-workspace catalog curation, per-user tool attribution, scoped keys, and browser tokens.
You run a SaaS product whose users create tools, connect apps, and talk to agents — and you want Protobox to be the engine behind all of it. This page covers the surface built for that shape of customer: your backend is the principal (one API key = one workspace), each of your users is an actor, and every capability below keeps their data partitioned by that actor.
Coming from the agent side? Framework adapters covers handing tools to a model; this page covers running the platform around it.
Execute a whole model turn in one round-trip
When a model emits several tool calls in one turn, send them as one batch instead of N
serial requests. Items fail independently — a bad call lands in its own slot and its
siblings still run — and results keep input order, each carrying actionId so you can
pair them back to the model's tool_use blocks.
const protobox = new ProtoboxSDK({ apiKey: process.env.PROTOBOX_API_KEY! });
const results = await protobox.tools.executeBatch(
[
{ name: 'GITHUB_CREATE_ISSUE', arguments: { title: 'Ship it' } },
{ name: 'GMAIL_SEND', arguments: { to: 'team@acme.dev', subject: 'Shipped' } },
],
{ entityId: 'user_123' }, // the actor whose credentials execute every item
);
for (const result of results) {
console.log(result.actionId, result.success ? 'ok' : result.error);
}Curate which apps your users see
Your workspace decides which catalog connectors exist for your product. Disabling a
connector keeps it listed (so your admin surface can re-enable it) but flags it
enabled: false and refuses its actions at execute time with CONNECTOR_DISABLED —
before any upstream call.
await protobox.connectors.disable('salesforce'); // curated off, not deleted
const { items } = await protobox.connectors.list();
items.find((c) => c.slug === 'salesforce')?.enabled; // → false
await protobox.connectors.enable('salesforce'); // back in the catalogRemember which user created a tool
When your users author tools in your UI, register each one with the creating actor.
The connector reads back ownerActorId, and the list filters by it — so "your tools"
views and scoped deletion need no bookkeeping on your side.
await protobox.connectors.registerOpenApi({
slug: 'acme-crm',
spec: openApiSpec,
actorId: 'user_123', // who created it
});
const mine = await protobox.connectors.list({ actorId: 'user_123' });
// only user_123's connectors — the catalog and other users' tools are excludedScope a key to a job
Scoping is opt-in: a key carrying any connectors:* verb — or a
connectorAllowlist — is scoped, and the platform then requires the matching verb per
route family, so an agent-runtime key can execute but never register. The allowlist pins
a key to specific connectors (CONNECTOR_NOT_ALLOWED on anything else). Keys minted
before these verbs existed — including tools:read/tools:execute-only keys — behave
exactly as before.
// POST /api/v1/api-keys (with your admin credentials)
{
"name": "agent-runtime",
"permissions": ["tools:read", "tools:execute", "connectors:read"],
"connectorAllowlist": ["github", "gmail"]
}| Verb | Grants |
|---|---|
tools:read | List/search actions, read executions |
tools:execute | Execute actions (single + batch) |
connectors:read | Read the connector catalog |
connectors:write | Register, sync, unregister, enable/disable connectors and toolsets |
Let the browser read directly
List-the-catalog and show-the-logs reads don't need to proxy through your backend. Mint
a short-lived (≤15 min), read-only token bound to one actor and hand it to your
frontend; it calls Protobox directly with the X-Gateway-Token header. Writes are
refused, and execution-log reads are pinned to the bound actor — asking for another
user's history is a 403.
// Your backend (private API key):
// POST /api/v1/actor-tokens { "userId": "user_123", "ttlSeconds": 300 }
// → { token, expiresAt, scopes: ["tools:read", "connectors:read"] }
// Your frontend:
const res = await fetch('https://platform.protobox.ai/api/v1/tools/integrations', {
headers: { 'X-Gateway-Token': token },
});Revocation is by expiry only — that is why the TTL is capped. Mint per page-load, not per session.
Hear about changes instead of polling
Register one webhook endpoint and receive signed events: connection.activated /
connection.revoked / connection.expired when users connect apps,
execution.completed / execution.failed for run outcomes, and
connector.registered when a connector lands in your workspace (your cue to invalidate
any cached tool definitions). Verify every delivery:
import { verifyWebhook } from '@protoboxai/sdk';
app.post('/webhooks/protobox', (req, res) => {
const event = verifyWebhook(req.rawBody, req.headers['x-protobox-signature'], {
secret: process.env.PROTOBOX_WEBHOOK_SECRET!, // returned once by webhooks.register
});
if (event.type === 'connector.registered') invalidateToolCache(event.workspaceId);
res.sendStatus(200);
});Python: the execute-core
Python agent runtimes (voice bots, workers) get the same execute contract from the
protobox package — stdlib-only, so a container adopts it without new dependencies.
Full parity follows; today it covers list, search, execute, and batch.
from protobox import Protobox
protobox = Protobox(api_key=os.environ["PROTOBOX_API_KEY"])
user = protobox.entity("user_123") # in-band actor scoping, same as TypeScript
tools = user.tools.list_tools(integration_id="github")
result = user.tools.execute(name=tools[0]["name"], arguments={"title": "Hi"})
results = user.tools.execute_batch([
{"name": "GITHUB_CREATE_ISSUE", "arguments": {"title": "Hi"}},
{"name": "GMAIL_SEND", "arguments": {"to": "a@b.c"}},
])Errors are typed (AuthenticationError, RateLimitError, …) in both languages, and
neither SDK retries — your runtime owns backoff.