Packs
What a B3OS pack contains, how a declarative HTTP action is written with CEL templates, and how packs are hashed, signed, published and verified.
A pack is a versioned bundle with a b3pack.json manifest at its root. The manifest names the pack, declares the hosts it may reach and the credentials it may use, and describes each action and trigger.
A small pack
This pack has one action, which calls the public Open-Meteo geocoding API and needs no credential:
json{ "pack": "io.b3os/open-meteo", "version": "1.0.0", "title": "Open-Meteo", "description": "Place search and current weather from the Open-Meteo API.", "publisher": { "id": "io.b3os", "name": "B3OS" }, "license": "MIT", "engine": "^1.0", "egress": [{ "host": "geocoding-api.open-meteo.com" }], "actions": [ { "key": "search-places", "title": "Search places", "description": "Find places by name. Returns coordinates and time zone for each match, best match first.", "inputSchema": { "type": "object", "additionalProperties": false, "required": ["name"], "properties": { "name": { "type": "string", "minLength": 2, "maxLength": 100 } } }, "outputSchema": { "type": "object", "additionalProperties": false, "required": ["places"], "properties": { "places": { "type": "array" } } }, "effects": { "class": "read" }, "impl": { "kind": "http", "request": { "method": "GET", "url": "https://geocoding-api.open-meteo.com/v1/search", "query": { "name": "${{ input.name }}", "format": "json" } }, "response": { "map": { "places": "${{ has(response.body.results) ? response.body.results : [] }}" } } } } ]}
Its action is called as io.b3os/open-meteo/search-places@1.0.0, and its MCP tool name is open-meteo__search-places.
Manifest fields
| Field | Meaning |
|---|---|
pack, version | <namespace>/<name> and a semantic version without build metadata. The namespace is the publisher's reverse-DNS name. |
title, description, license, publisher | What the pack is and who publishes it. Verification status lives in the registry, never in the manifest. |
engine | The pack runtime it needs, as a caret range; the v1 runtime is 1.0.0. |
egress | Every host any action or trigger may reach. Actions may narrow it. A new host in a later version is a major change. |
connections | The credentials the pack can use, by key, each with a type (api_key, bearer, basic, oauth2) and the hosts it is injected for |
actions | Up to 256 actions: key, title, description, inputSchema, outputSchema, effects and impl |
triggers | Up to 64 webhook, poll or schedule triggers |
signature | Written by the publish tool: the content hash and the publisher's Ed25519 signature |
How a declarative action runs
- The input is validated against
inputSchema, and schema defaults fill in absent fields. - The request template is evaluated. Expressions are CEL inside
${{ }}, withinputand the connections' non-secretparamsin scope. Text spliced into the URL is percent-encoded, so an input can never add a path segment or a query. - The final URL's host must match the action's egress list.
- The egress proxy adds the credential for each connection the action requires, only on requests to that connection's hosts:
api_keywhere the pack'sinjectsays,bearerandoauth2asAuthorization: Bearer,basicasAuthorization: Basic. Templates never see secrets and cannot set the injected header. - A response counts as success on a 2xx status unless
response.successsays otherwise.response.mapbuilds the output, which must matchoutputSchema; the default map is the response body. On failure, the first matchingerrors[]entry names the pack's error, reported in the problem'spack_error.
Packs whose logic needs code are written in JavaScript and run in a separate runtime with no credentials, reaching the network only through the egress proxy. In the beta those are first-party packs only.
Signing and verification
Every pack version has a content hash over all of its files (the manifest serialized with RFC 8785, without its signature) and an Ed25519 signature by the publisher's registered key over b3pack-sig-v1, the pack, the version and the hash. Before loading a version, a node recomputes the hash, compares it with the registry's record, and checks the signature. A pack that fails is not loaded, and calls to it fail with pack_unavailable.
The registry
| Call | Does |
|---|---|
GET /v1/actions/packs | Packs the node serves, with their latest version and action and trigger counts |
GET /v1/actions/packs/{namespace}/{name} | A pack version with its actions, triggers, connections and egress hosts |
GET /v1/actions/packs/{namespace}/{name}/versions/{version}/manifest | The exact b3pack.json, as published and signed |
GET /v1/actions/registry/packs | The registry at beta-api.b3os.org, with every published version |
POST /v1/actions/registry/packs | Publish a version as a gzip tarball (actions:write) |
To see what a pack does before an agent calls it, read its manifest:
bashcurl -s https://beta-api.b3os.org/v1/actions/packs/io.b3os/open-meteo \ -H "Authorization: Bearer $B3OS_API_KEY"curl -s https://beta-api.b3os.org/v1/actions/packs/io.b3os/open-meteo/versions/1.0.0/manifest \ -H "Authorization: Bearer $B3OS_API_KEY"
tsimport { B3OS } from "@b3os/sdk";const b3os = new B3OS();const pack = await b3os.actions.getPack("io.b3os", "open-meteo");console.log(pack.version, pack.actions.map((a) => a.key));const manifest = await b3os.actions.getPackManifest("io.b3os", "open-meteo", pack.version);console.log(manifest.egress);
pythonfrom b3os import B3OSb3os = B3OS()pack = b3os.actions.get_pack("io.b3os", "open-meteo")print(pack["version"], [a["key"] for a in pack["actions"]])manifest = b3os.actions.get_pack_manifest("io.b3os", "open-meteo", pack["version"])print(manifest["egress"])
actions_describe gives an agent one action's schemas, effect class and connections. Whole packs and manifests are read from code.
Versions are immutable once published. In the beta only first-party B3OS packs (io.b3os) can be published; public publishing comes later, with signed provenance and a review of each version's new capabilities.