Obelisk 0.41: Safe Deployments for Agents
2026-08-09Obelisk 0.41 draws a firm line between what an operator controls and what a deployment can do. That
line is what makes it safe to let an agent author, submit, and activate deployments: the operator
sets secrets and outbound HTTP policy once in server.toml, and no deployment can exceed it, no
matter who wrote it. Our proof of concept
workflow-agent, where the workflow is the agent
editing and pushing deployments, runs entirely inside these boundaries.
The operator owns server.toml
A deployment describes an application: its components, their allowed hosts, the secrets they
reference by name. server.toml describes the trust the operator is willing to extend to any
deployment on that server. In 0.41 the two are enforced together. Runtime access is the
intersection of the operator policy and the deployment's own component policy, so a deployment
can only narrow what the operator granted, never widen it.
That is exactly the property you want when the author is an agent. You can hand it the deployment and let it iterate, because the blast radius is fixed by a file the agent does not get to rewrite.
Secrets by name, never by value
Secrets are now operator-owned names declared in a [secrets] table:
# server.toml
[secrets]
LLM_API_KEY = { env = "LLM_API_KEY" }
The value is resolved from its source once, before the async runtime even starts, and the source environment variable is then wiped from the process. Deployments reference the secret by name only:
[[outbound_http.allowed_host]]
pattern = "${LLM_BASE_URL:-http://127.0.0.1:9190}"
methods = ["POST"]
secrets = ["LLM_API_KEY"]
replace_in = ["headers"]
The component receives an opaque placeholder. The runtime substitutes the real value only into
outgoing requests to approved hosts, at the network edge, in the locations replace_in selects. The
WASM or JavaScript guest never sees the plaintext and cannot exfiltrate it.
Deployment-side interpolation of secret values is gone entirely.
Outbound HTTP, gated on both sides
Component-originated outbound HTTP now requires a matching [[outbound_http.allowed_host]] entry in
both server.toml and the deployment. An omitted server allowlist denies all
component-originated HTTP by default. On top of the host pattern and methods, an optional
request_url_regex pins the exact routes a component may call:
[[outbound_http.allowed_host]]
pattern = "${LLM_BASE_URL:-http://127.0.0.1:9190}"
methods = ["POST"]
request_url_regex = "^POST http[s]?://[^/]+(/[^/]+)*/v1/(messages|chat/completions|responses)$"
secrets = ["LLM_API_KEY"]
replace_in = ["headers"]
An agent editing this deployment can point the component at the LLM endpoint the operator approved, with the key the operator provided, and can reach nothing else.
Offline verify and repair
obelisk deployment verify compiles and checks a local deployment with no running server and no
database. With --fix it repairs content digests, updates the exec activity allowlist, and
scaffolds missing secret declarations. This is the loop an agent runs before it ever touches a
server: author the deployment, verify it offline, let --fix bring it into shape, then submit. For
single-party setups, obelisk generate server-config --trusted emits a minimal configuration that
allows all exec activities and component-originated HTTP without registering secrets.
Seen in workflow-agent
The workflow-agent proof of concept is an Obelisk app
whose workflow is an agent. It mounts the active deployment as a virtual filesystem, edits it with a shell,
verifies it, and can submit and activate the result or open a pull request. Its server.toml grants
exactly three destinations, each with a named secret injected at the edge: the LLM endpoint, the
Obelisk API, and the GitHub API. The agent has full run of the deployment, and still cannot read a
token or reach an unlisted host. That is 0.41's boundary doing its job.
The agent loop and its shell used to run on an embedded JavaScript engine. We rewrote both as a
native Rust workflow, porting just-bash to Rust so the whole session (chat history, virtual
filesystem, the model-facing bash tool) executes as compiled WASM. The payoff is latency: a turn
that replays a cold workflow from its execution log lands in tens of milliseconds, and once the
workflow is warm in memory the hot path is single-digit milliseconds. Durable execution here costs
almost nothing.
Upgrading
This release is breaking for both operators and component authors. Move
[activity_exec.secrets] env_vars to activity_exec.secrets = ["NAME"], flatten nested
allowed_host.secrets.env_vars to secrets = ["NAME"] with replace_in = [...], declare each name
in [secrets], and copy your existing deployment allowlist entries into server.toml as a starting
point before narrowing them. Backtrace persistence is now on demand rather than global, and execution
failure JSON consistently uses execution_failed.
For the full model and every before/after snippet, see the updated Configuration and CLI docs.