oz0-plugin.yaml

Data, evidence and mail

Look up the fields for 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.

oz0-plugin.yaml
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 _meta under oz0/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 as anthropic.
    • amount_micros: 0 to 10^13 micro-units of the currency.
    • currency: an ISO 4217 code such as USD.
    • key: at most 200 characters from A-Z a-z 0-9 . _ : / @ + = -, unique per row. Repeating the key changes the row instead of adding one.

    Optional fields are account (the provider's workspace or project, shown in no view), team (an OZ team's name), model (or cost line), input_tokens and output_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 declares outside-spend and 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.
server.py
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.

oz0-plugin.yaml
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 _meta under oz0/inventory: tenant, at (when the edge read them) and agents, each with name, flow (true for a flow), plugin, version (its plugin's installed version), description, owner_team, owner_person (user:<name>), state (active or retired), retired_reason and retired_at, risk_class, purpose, and in_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 declares inventory and 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 source inventory.
server.py
@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 tool oz.evidence. Unlike the other kinds it is granted for named target plugins, by a person who holds content.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), then oz0 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 source results.
    See Evidence for agents.
oz0-plugin.yaml
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.

oz0-plugin.yaml
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.

See results drawn as cards.

Back to oz0-plugin.yaml.

Copyright © 2026