berthos-sdk lets you write a resident app in Python. It uses the same berth.yml and the same export protocol as the TypeScript @berthos/sdk, so a Python app's exports look identical to an agent, and it can publish and subscribe on the context bus alongside TypeScript apps.
pip install berthos-sdk # imported as berth_sdk; Python 3.11+A Python resident app runs inside a Berth sandbox, like a TypeScript one. An agent calls its exports as tools, through the CLI (berth dev, berth test, berth mcp, berth os up) or Computer.boot(). Inside the sandbox it can exchange events with other apps over the context bus, call and be called by sibling apps with app:invoke:, and be gated by a governing app.
Your app runs as its own Python process in the sandbox, started under agent-init as python3 -m berth_sdk.runtime instead of the Node runtime (entrypoint.sh). It talks to the host over stdio, to sibling apps over Unix sockets, and to the context bus daemon over a Unix socket.
Declare the runtime in berth.yml:
name: my-app
version: 0.1.0
runtime: pythonThen run it like any other app: berth dev, berth test, berth mcp, berth os up, or Computer.boot(). A Python app can share a sandbox with TypeScript apps; each is started with its own runtime, and they talk over the same context bus. It can call and be called with app:invoke:, and is gated by a governs: true app in the same sandbox (or be that app), exactly as a TypeScript app is.
Every sandbox image carries the SDK at /opt/berth/sdk-python, and already has pydantic, pyyaml and protobuf. When berth dev bind-mounts a clone of the repo, the app's process imports the repo's own packages/sdk-python instead, so SDK edits need no rebuild; the capability policy is always compiled by the image's copy. Your capabilities are compiled into the same kernel policy a TypeScript app gets. Put your own dependencies in on_install (pip install -r requirements.txt).
berth test checks your exports against berth.yml and calls each one with a stub input built from its Pydantic model, as it does for a TypeScript app. If the app has a tests/ directory it then runs python3 -m pytest -q tests in the image. pytest isn't in the base image, so install it through on_install.
Exports are served as line-delimited JSON on stdio, and also on a Unix socket when BERTH_RPC_SOCKET is set. Next to that socket, the runtime binds one socket per sibling allowed to call the app, at peers/<caller>/rpc.sock; which socket a call arrives on is how the app, and its governor, know who called:
{"id": "1", "export": "greet", "input": {"name": "Ada"}}
{"id": "1", "result": {"message": "Hello, Ada!"}}
{"id": "2", "error": "no such export \"nope\""}Inside the process, the runtime (berth_sdk/runtime.py) loads the manifest, imports your entry file, checks your exports, runs your hooks and then serves your exports. These are its steps, in order.
- Load and validate
berth.yml. - Import the entry file and find
app. - Check the app's exports against
berth.yml'sexports:, and stop with an error if they differ. - Run
on_installhooks. - Connect to the context bus.
- Run
on_agent_readyhooks with anAppContext. - Log
[berth:runtime] "<name>" readyand serve exports.
berth.yml:
name: hello-world-py
version: 0.1.0
capabilities: []
exports:
- name: greet
input: { name: string }
output: { message: string }src/app.py:
from berth_sdk import define_app
from pydantic import BaseModel
class GreetInput(BaseModel):
name: str
class GreetOutput(BaseModel):
message: str
def greet(inp: GreetInput) -> GreetOutput:
return GreetOutput(message=f"Hello, {inp.name}!")
def setup(app):
app.export("greet", greet, input_model=GreetInput, output_model=GreetOutput)
app = define_app(setup)The runtime loads src/app.py and looks for a module-level variable named app. A full example is apps/hello-world-py.
Calls setup(app) and returns the app. Assign the result to a module-level app.
Registers an export. name must match an entry in berth.yml's exports:, and registering the same name twice raises ValueError.
- With
input_model(a pydanticBaseModelsubclass), the handler receives a validated model instance. Without it, the handler receives the raw input (usually adict, orNone). - With
output_model, a return value that isn't already an instance is validated into one. A pydantic model is sent back as itsmodel_dump(); anything else is sent as returned. - A raised exception is sent back to the caller as an error; it doesn't stop the app.
Registers fn() to run once at startup, before on_agent_ready, inside the sandboxed process. For build-time setup, use berth.yml's on_install.
Registers fn(ctx) to run once at startup, before exports are served. ctx is an AppContext:
| Attribute | Type |
|---|---|
ctx.manifest |
BerthManifest |
ctx.context_bus |
context bus client; see the context bus reference |
Hooks are plain functions, not async. A handler only receives its input, so keep ctx.context_bus in a module-level variable if an export needs it.
| Name | What it does |
|---|---|
load_manifest(path) -> BerthManifest |
Reads and validates a berth.yml |
BerthManifest, ExportSpec |
Pydantic models for the manifest and one export |
parse_capability(s) |
Splits namespace:action:scope; raises ValueError if malformed |
matches_capability(granted, requested) -> bool |
Namespace and action match exactly; scope matches with * globs |
BerthManifest validates name, version, description, capabilities, exports, on_install and on_agent_ready. Other fields (secrets, expose, governs, governance, resources) are accepted but not checked; use @berthos/manifest-schema for full validation.
| Variable | Default |
|---|---|
BERTH_MANIFEST_PATH |
./berth.yml |
BERTH_APP_ENTRY |
./src/app.py |
BERTH_RPC_SOCKET |
unset (stdio only) |
BERTH_CONTEXT_BUS_SOCKET |
/tmp/berth-context-bus.sock |
- No HTTP RPC bridge or TCP listener. A Python app is reached over stdio and its Unix sockets only.
- No semantic filesystem client,
requestCapabilityorconfigureEgressProxy. Reading and writing files under/contextworks as ordinary file I/O. To use the egress proxy, point your HTTP client at$BERTH_EGRESS_PROXY_URL.