How AgenticOS Tools Actually Run

How AgenticOS Tools Actually Run

An agent net can do a lot with its built in transitions, but sooner or later it needs to reach outside itself: call a weather API, render a PDF in a container, run a maintenance script, invoke a reusable sub workflow. AgenticOS answers these the same way, through one durable Tool Catalog. Four capability flags map to four kinds of tool (d docker, h http, t tool nets, s scripts), and the catalog now comes in two scopes: a global one and a local one per model. This is how the catalog is built, what really happens when a tool fires, and why a tool can be global or private to one model.


The Problem: Tools Were Ad Hoc

A Petri net in AgenticOS is made of places (durable token containers) and transitions (the things that fire). The engine ships a fixed set of transition kinds: pass, map, HTTP, LLM, agent, command, link. That covers a great deal, but a real workflow keeps wanting capabilities that live outside the net: a running service to call, an image to start, a script to execute on a worker.

Before the catalog, each of those was handled by hand and none of them were durable. An external API meant someone wrote an HTTP transition inscription by hand and copied the URL around. A container tool meant an agent knew a magic image name. And a script tool was the worst offender: on our safe team forum automation we literally used docker cp to push a .cjs file into the executor container and then ran node /opt/that-file.cjs from a command transition. It worked, but the script lived nowhere the platform could see. It was invisible to the catalog, unversioned, unaudited, and it vanished the moment the container was recreated.

The question the catalog answers is simple to state and was surprisingly hard to answer before: what callable tools exist, and how do I run each one?


The Idea: One Catalog, Three Bindings

The Tool Catalog is a durable, event sourced registry. Every entry is a token in the p-tool-catalog place of the always available default model, so it survives restarts, is queryable, and is visible to every agent that can reach it. Large contract material (a full OpenAPI document, a script body) does not sit inline in the token: it lives in the blob store, referenced by an immutable URN.

The most important design choice is that every entry separates two concerns. The contract says what can be called (the operations, the input and output shape, the capabilities). The binding says how it actually runs. That split is what lets one uniform catalog describe three very different kinds of tool without pretending they are the same underneath.

One catalog, three bindings, and a flag for each kind of toolTool Catalogglobal (default) + local per modelcontractwhat to callbindinghow to runDocker binding (flag d)image in the local registryvalidated, digest pinnedmaster starts the containerstatus: approved, always globalre-push a tag → re-importHTTP binding (flag h)external base URL + OpenAPIno container is startedwrapped into HTTP transitionsstatus: registered, always globalnot container validatedScript binding (flag s)artifact in the blob storepinned by sha256executor runs node, sh, python3approved, global OR local to a modelverified at both endsThe fourth flag, t, governs tool nets: reusable sub workflows composed from the other tools.Catalog search and get are granted by any of d, h, t, s, and return only the kinds your flags reveal.

What a Catalog Entry Looks Like

Every entry, whatever its binding, carries the same top level fields: an id, a name, a version, a description, a set of capabilities, a status, the contract, and the binding. Entries upsert by id, so a tool has exactly one current definition. Re-registering a tool replaces the old entry instead of piling up duplicates.

Here is a script entry as it sits in the catalog. Notice that the actual script content is not here: the binding points at a blob store URN and records the sha256 the content must hash to.

{
  "id": "forum-sentinel",
  "name": "Forum sentinel",
  "version": "1.0.0",
  "status": "approved",
  "capabilities": ["forum", "safe-team"],
  "contract": {
    "type": "script-io",
    "input": "argv + env; JSON on stdin when args.input is set; stdout is the result"
  },
  "binding": {
    "type": "script",
    "runtime": "node",
    "scriptUrn": "urn:agenticos:blob:tool-catalog/scripts/df6ff312...af6d1.cjs",
    "sha256": "df6ff312a2c0955727f8735dcc93686f684fb8fc3eac8a87288c87f4596af6d1",
    "filename": "forum-sentinel.cjs",
    "sizeBytes": 14252
  }
}

The word approved is doing real work. For a script it does not mean a human read and blessed the code. It means the catalog has pinned a sha256, and that pin is enforced cryptographically every time the tool runs. More on that below.


The Three Bindings, One at a Time

Docker: run a container the platform trusts

A docker tool is a container image that has already been pushed to the bundled local registry. A coding agent connected over MCP builds it on its own machine and pushes it. The important boundary is that the master never builds an image. It only imports one that already exists.

Import is a validation flow, not a copy. The master refuses any reference that is not in the local registry, then it starts a temporary validation container, fetches and parses the OpenAPI document the image advertises, stores that spec in the blob store, records the registry digest, writes an approved entry, and tears the container back down. From then on the master can start that tool on demand. If you re-push the same tag with different content, the recorded digest no longer matches, and the tool has to be re-imported (and thereby re-validated) before it will run again.

HTTP: describe a service that already exists elsewhere

Not every capability should be pulled into a local container. An external REST service is registered with just a base URL and an OpenAPI document (supplied inline or fetched from a URL). No container is started, so the entry is stored with status registered rather than approved: it was never container validated, and the catalog is honest about that. Agents read the stored contract and wrap the operations they need into ordinary HTTP transitions. This is the right home for third party APIs that should never be copied into your registry.

Script: the pattern that replaces docker cp

A script tool is an executable artifact (node, sh, bash, or python3) that runs on the executor. You register it once and the content goes into the blob store, pinned by its sha256. This is the binding that retires the old docker cp workaround: the script now lives somewhere durable and content addressed, it is versioned and auditable in the catalog, and content that does not match the registered digest is refused end to end. The rest of this article follows a script tool from a fire down to the process that runs it, because that path is where the interesting engineering lives.


How a Script Tool Actually Fires

A command transition invokes a script tool by reference, not by content. Its token says which tool, and how to call it, but carries none of the code:

{
  "executor": "script",
  "command": "invoke",
  "args": {
    "toolId": "forum-sentinel",
    "argv": [],
    "env": { "DISCOURSE_URL": "https://forum.example.com" },
    "timeoutMs": 110000
  }
}

What happens next is a two party dance between the master and the executor, and the design goal behind it is worth stating up front. Executors poll for work over an egress only connection, and in a gateway deployment their token is scope limited to the polling protocol. They cannot reach the blob store directly. So the platform must get the script to the executor without the executor fetching anything itself.

A script tool from fire to resultcommandexecutor: scripttoolId onlymaster (at FIRE time)1. resolve toolIdfind the catalog entry2. verify blob vs pinsha256 must match3. inline into tokencontent + sha256 + runtimeexecutor4. re-verify sha256refuse on mismatch5. materialize to cachecontent addressed, /workspace6. run the runtimenode, sh, python3pollresult token (large output offloaded to blob URN)digest checked twice: once before it ships, once before it runs

The master side: resolve, verify, inline

When the transition is ready to fire and its tokens are reserved, the master looks at the bound command token, sees executor: script, and resolves the toolId against the catalog, checking the firing model’s own local catalog before the global one. It reads the blob the entry points at, checks that the content still hashes to the pinned sha256, and then inlines the content into the token it is about to ship: the base64 body, the sha256, the runtime, and the filename all get merged into the args. Only fires pay this cost. Tokens waiting for a slot do not.

If resolution fails for any reason, the master ships the token untouched rather than blocking the fire. The executor then fails that one command with a precise error, which is a far better outcome than a stuck transition.

The executor side: re-verify, cache, run

The executor receives a self contained token. It decodes the content, hashes it, and refuses to run anything that does not match the declared sha256. Then it writes the script into a content addressed cache under the persistent /workspace volume, keyed by the digest, so identical content is written once and survives container recreation. It runs the script with the declared runtime, closes stdin (so an interactive capable tool cannot hang forever), captures the output, and returns a result in the same shape a plain shell command would.

Even the cache is defended. If a cached file at the digest location has been altered on disk, its bytes will not hash to the digest, and the executor rewrites it from the verified content before running. There is no path by which a script that does not match the pin gets executed.

That double check is the whole security story, and it is why the blob store does not need to be a fortress. The blob store holds the content, but the catalog token (which lives in the event sourced node, behind the gateway) holds the digest. Tampering with the stored blob does not let an attacker run a different script: the hash will not match, at the master before it ships and again at the executor before it runs. Content addressing plus a pin, verified twice, is the same guarantee you would otherwise reach for a secret or a signature, achieved with a hash the platform already computes.


Four Flags, One Per Kind of Tool

AgenticOS agents carry capability flags, and there is now one flag per kind of tool. It reads like a file permission string: rwxhludcts. The last four letters are the tool kinds:

  • d (docker): discover and start container tools, and import images into the catalog.
  • h (http): call external services and register them in the catalog. An HTTP binding is just an external service, so the flag that lets you call one also lets you describe one.
  • t (tool nets): discover and invoke tool nets, the reusable sub workflows that compose the other tools into a single callable unit.
  • s (scripts): register executable script artifacts. Running one is authoring an ordinary command transition, so that stays with write access.

The split is deliberate. Earlier, all of this hid behind one docker flag, which meant granting the ability to register a script also granted the ability to spawn containers. Now d means containers and s means scripts, and neither implies the other. Catalog search and get are granted by any of the four flags, and they return only the kinds you hold: a pure script registrar sees scripts, a tool net consumer sees tool nets. The one thing no flag grants is building a container image through the master. Builds stay on the coding agent’s own machine. The master validates, catalogs, and runs, but it never compiles.


Global and Local: Where a Tool Lives

Not every tool belongs to everyone. A docker image and an external HTTP service are inherently shared: there is one registry, one docker host, and an external API exists whether or not any particular model is loaded. So docker and http bindings live in one global catalog, the default model’s p-tool-catalog.

Scripts are different. A maintenance script that reaches into one workflow’s places and lanes is meaningless to any other model. Forcing those into the global catalog pollutes every model’s namespace. So each model now gets its own local catalog, created the first time it registers something, and script registration defaults to the caller’s own model. You pass model: "default" only when you actually want a script to be global.

Lookup is local first. When a search runs, or when a script fires, the engine checks the model’s own catalog before the global one, and a local entry with the same id shadows the global one. That gives you an override: a model can ship its own variant of a shared script without disturbing anyone else. Results are labeled scope: local or scope: global, and a global entry that a local id is hiding is marked so you can see the shadowing. A search never has a side effect on another model: reading a model’s catalog never creates anything in it.

# search my model's catalog first, then the global one
GET /api/tool-catalog?type=script&model=my-team

# register a script private to my model (the default)
TOOL_CATALOG_REGISTER_SCRIPT { "id": "nightly-digest", "name": "Nightly digest", "runtime": "node", "content": "..." }

# register a script everyone shares
TOOL_CATALOG_REGISTER_SCRIPT { "id": "shared-util", "name": "Shared util", "runtime": "sh", "content": "...", "model": "default" }

Real Behavior, Not a Sketch

We migrated a live workload onto this. Our safe team forum automation had twenty one Node scripts that used to be copied into the executor with docker cp and invoked as node /opt/name.cjs. Moving them onto script bindings gave us some concrete properties:

  • All twenty one scripts registered in the catalog, each pinned by sha256, each one durable token per tool, and all local to the safe team model rather than cluttering the global namespace every other model sees.
  • Every command transition rewritten from a raw node /opt/... string to a clean toolId reference.
  • Local first resolution proven live: a script registered globally with one body and into a model’s local catalog with another fires the local one, and search shows the local entry shadowing the global.
  • Content survives container recreation. We restarted the blob store and confirmed all blobs came back, and a script fetched back with its hash matching the pin.
  • Large command output does not bloat the token: anything over the inline threshold is offloaded to the blob store and the token carries a preview plus a URN.
  • Tampering is a non event. A blob changed underneath the catalog fails the hash check at the master and never ships.

Where This Leads

  • Crystallization. A capability that starts as ad hoc reasoning becomes a cataloged tool, then a reusable one that any net can call. The catalog is where that hardening lands.
  • Portability. Because the content is content addressed, the same tool can be re-registered on a fresh install and land at the same URN the catalog already points at, so the entries keep working.
  • Uniform discovery. Docker, HTTP, and script all answer the same questions (name, version, capabilities, contract, status), so an agent browses one catalog rather than three mechanisms.

Try It Yourself

A coding agent builds a tool image on its own Docker host, pushes it to the bundled registry, and asks AgenticOS to validate and catalog it:

docker build -t localhost:5001/agenticos-tool-example:1.0.0 ./example
docker push localhost:5001/agenticos-tool-example:1.0.0

# then, from an agent that holds the D flag:
TOOL_CATALOG_IMPORT_IMAGE { "image": "localhost:5001/agenticos-tool-example:1.0.0", "id": "example" }

Registering a script is one call, and from then on any command transition can invoke it by id:

TOOL_CATALOG_REGISTER_SCRIPT {
  "id": "digest",
  "name": "Repo digest",
  "runtime": "node",
  "content": "console.log(JSON.stringify({ ok: true }));"
}

// invoke from a command transition:
{ "executor": "script", "command": "invoke", "args": { "toolId": "digest" } }

Search what is registered, and inspect a contract before you wire anything:

GET /api/tool-catalog?type=script
GET /api/tool-catalog/forum-sentinel

Built on AgenticOS: a Petri net workflow platform where the net topology is the architecture. The Tool Catalog lives in the master, its contracts in the blob store, and its script bindings run on the same egress only executors that already fire every command transition.

Related reading: Executors and Deployment Architecture, and Building a Self Healing QA Net.

Leave a Reply

Your email address will not be published. Required fields are marked *