Data, evidence and mail
For: Plugin authors looking up manifest fields.
Look up the fields for data, evidence and mail.
provides
A record of the core that the plugin's tools fill beyond their own calls. Each kind is a permission: an install of a version that declares one the tenant has not granted is refused until a tenant admin accepts it with oz0 plugin install ... --accept-provides <kind> (audited plugin.provides.granted). The grant stays across versions, oz0 plugin list shows it, oz0 plugin revoke-provides <plugin> <kind> takes it back, and removing the plugin drops it. oz0 dev grants the working copy's in its dev tenant.
provides: [outside-spend]
The one kind there is:
outside-spend: spend that belongs to no job, such as a model provider's admin cost report, shown as outside spend. A tool returns it in its MCP result's_metaunderoz0/outside-spend, as a list of at most 500 rows.
Each row contains:day: a UTC day,YYYY-MM-DD, from the last three years up to today.provider: a short lowercase name such asanthropic.amount_micros: 0 to 10^13 micro-units of the currency.currency: an ISO 4217 code such asUSD.key: at most 200 characters fromA-Z a-z 0-9 . _ : / @ + = -, unique per row. Repeating the key changes the row instead of adding one.
Optional fields areaccount(the provider's workspace or project, shown in no view),team(an OZ team's name),model(or cost line),input_tokensandoutput_tokens.
The runtime takes the rows out of the result, so they never reach the model, the job's output or its history, and the edge records them only for a plugin whose installed version declaresoutside-spendand whose tenant granted it. The rows never count toward a job, a budget or a quota, and at most 5,000 rows are kept per plugin and day.
The tool's text gets one line saying how many rows were added, changed and already there; a refusal makes the call an error.
row = {
"day": "2026-10-08",
"provider": "anthropic",
"account": "wrkspc_01",
"team": "research",
"model": "claude-opus-5",
"amount_micros": 2500000,
"currency": "USD",
"key": "anthropic/wrkspc_01/2026-10-08/opus",
}
result = CallToolResult(content=[TextContent(type="text", text="1 day read")], _meta={"oz0/outside-spend": [row]})
Nothing in a row may name a person: sum people's figures to teams in the plugin, before the rows leave it.
reads
A record of the core that the plugin's functions read beyond their own calls. Each kind is a permission, granted as provides is: an install of a version that declares one the tenant has not granted is refused until a tenant admin accepts it with oz0 plugin install ... --accept-reads <kind> (audited plugin.reads.granted).
The grant stays across versions, oz0 plugin list shows it, oz0 plugin revoke-reads <plugin> <kind> takes it back at once (audited plugin.reads.revoked), and removing the plugin drops it. oz0 dev grants the working copy's in its dev tenant. Nothing a plugin reads this way can be written back.
reads: [inventory]
The kinds:
inventory: the tenant's agents and flows as the inventory records them. Each call of one of the plugin's own functions gets them in its MCP request's_metaunderoz0/inventory:tenant,at(when the edge read them) andagents, each withname,flow(true for a flow),plugin,version(its plugin's installed version),description,owner_team,owner_person(user:<name>),state(activeorretired),retired_reasonandretired_at,risk_class,purpose, andin_catalog(false once its plugin was removed and the inventory still keeps it).
Nothing else is in it: no models, regions, kinds of data, eval results, jobs or members. The runtime asks the edge for it before the call, with its own token, and the edge answers only for a plugin the calling node runs, whose installed version declaresinventoryand whose tenant granted it; the inventory goes to the call alone, never to the model, the job's output or its history.
A call that gets none is not made, and its caller hears why. The edge audits a plugin's reads once per ten minutes (inventory.read, with how many reads the ten minutes before had). The plugin's routines and flows may also fire on the inventory's changes: the built-in event sourceinventory.
@server.tool()
def sync(name: str, ctx: Context) -> str:
inventory = ctx.request_context.meta["oz0/inventory"]
agent = next((a for a in inventory["agents"] if a["name"] == name), None)
...
results: what the jobs of other plugins of the tenant came to (their findings, failing and kept eval cases, refused approvals with their reasons, error groups, cost per run and scorecards), which the plugin's agents read with the built-in tooloz.evidence. Unlike the other kinds it is granted for named target plugins, by a person who holdscontent.read, never a key:oz0 plugin install ... --accept-reads results --reads-target <plugin>(repeatable; the install screen asks which when none is named, and none means it reads nothing), thenoz0 plugin reads grant|revoke <plugin> results --target <plugin>.
A target added is a governance change, with a reason and four eyes when the tenant has them on; each grant and revocation is audited with its target. The edge answers every read with the permissions the granting person holds at that moment, and the plugin's routines and flows may fire on the built-in event sourceresults.
See Evidence for agents.
reads: [results]
mail
The plugin's functions send mail through the edge. A permission like reads: an install of a version that declares it is refused until a tenant admin accepts it with oz0 plugin install ... --accept-reads mail, and oz0 plugin revoke-reads <plugin> mail takes it back at once.
mail:
connection: email-graph # optional
connections:
- name: email-graph
tools: [mytool]
# ...
scopes:
- functions: [mytool.mail_token]
Each call of a function of the plugin's own (local) tools gets, in its _meta under oz0/mail, the address of a listener of the node's runtime and a ticket good for that call only. The function posts the message there as JSON (op check, send, prepare or sent, and message with to, cc, subject, text, html and attachments); the runtime adds the plugin, the tool, the function, the job, the call's idempotency key and binding, and lets only the bytes of the files the call was given be attached.
The edge checks the recipients against the tenant's mail domains, adds the AI label and Auto-Submitted, refuses a send from a function whose listing is read-only, and either sends the message through the operator's relay (send) or hands it back rendered as MIME (prepare), for a mail service the plugin sends it through itself, followed by sent with the receipt prepare gave.
Every message sent is audited as mail.sent.
connection names one of the plugin's connections whose token the edge mints with a message prepare rendered, and nowhere else: its scope names <tool>.mail_token, a function no server may list, so no call fetches the token before it starts and a tenant that sends through the relay needs no such connection. ai.oz0/email is the plugin that uses it.
Card keywords in schemas
Output schemas (an agent's or a flow's output_schema, and the outputSchema a tool's MCP server lists) may carry two keywords that shape how a matching result is drawn as a card, and input schemas the first:
x-oz0-order: a list of property names, shown first: a table's columns, key-values' keys, and the fields of an edit form or a typed question. Lint warns about a name that is not a property.x-oz0-diff:{before: <property>, after: <property>}at an object schema's root: a result holding both is drawn as a diff (lines for two texts, keys for two objects). Lint refuses one that does not name two different properties of that schema.