Skip to main content

AstralVessel

A lightweight, open agent server and the spec behind it. AstralVessel is a server for agents that sits between an application and its model provider: it takes questions over A2A 1.0, uses any OpenAI-compatible model, runs each agent's tools in its own sealed sandbox, reaches the outside world only through an egress door, and answers in Markdown with optional AstralPrimitives so Astral's clients can render the result.

It is built on the same design as Astral's harness and sandbox, as a separate implementation an application can use without running a full Astral instance. It is not code taken from AstralDeep.

Documentation: https://astraldeep.github.io/AstralVessel/

Diagram: on the left, a full Astral instance with its web, Windows, Android and Apple clients, accounts and sign-in, an agent catalog of many agents, and a highlighted harness and sandbox. On the right, one app such as a team's web app, using AstralVessel for its agents: an agent folder (vessel.toml, prompt.md, tools/) plus a harness, sandbox and egress. A solid arrow from left to right reads "used on its own"; a dashed arrow back reads "could slot back in later".

Why it exists

Astral is a centralized platform: one deployment serves many users, many agents, and Astral's own web, Windows, Android and Apple clients. Some applications need only a small piece of that: a few agents answering questions or doing work inside one app, without Astral's accounts, agent catalog or client apps.

Without AstralVessel, such an app tends to borrow its agent loop from a coding assistant run headless (OpenCode is the usual choice) and surround it with hand-built scaffolding:

  • a sandboxed container
  • an egress proxy
  • an HTTP client for the assistant's private API
  • a translator for its event stream
  • session cleanup
  • configuration that switches off the coding features the agent must not have

Each app rebuilds that glue, it breaks when the assistant changes its private API, and it still runs a coding tool to do something else.

AstralVessel replaces the borrowed assistant and the scaffolding with one component that is:

  • Lighter. It is a Python package and a few containers. It has no coding-assistant features to switch off and no private API that shifts between releases.
  • Open. Every interface is a published standard: A2A for conversations, the OpenAI-compatible API for models, and AstralPrimitives for rich output. A conformance suite checks any agent server that claims to follow this spec, and the code is under Apache 2.0 like the rest of Astral.
  • Astral-ready. It speaks the same protocols as AstralDeep, so AstralDeep can call an AstralVessel agent the way it calls any external A2A agent, and an agent built for AstralVessel could move into Astral without being rewritten.

AstralVessel's spec covers an agent server as seen from outside: A2A in, events and transcripts out, sandboxes behind it. AstralDeep's own harness contract is a different layer, the interface its orchestrator uses for its internal model loop; AstralVessel does not implement or replace it.

How it works

An agent is a folder: a manifest (vessel.toml), a briefing (prompt.md), and optional declared tools, each a small TOML file. A deployment file (deployment.toml) lists the agents one vessel serves, the sandbox and services each one gets, and which applications may call which agents. The agent declares what it needs; the deployment file is the only place access is granted.

Diagram of who holds what. The app holds its key and its users' conversation history, names each user with X-Vessel-User, and is also on an outside network for its users' browsers. It calls the vessel over A2A 1.0. The vessel serves every agent and runs the agent loop; it holds only runs in flight, never conversation history or the model key. It calls each agent's sandbox over /run (HTTP); there is one sandbox per agent, which runs the tools on read-only data and never has network, secrets, history, or a route to the vessel's API. The vessel reaches the egress over the OpenAI-compatible API, which is also the web proxy for webfetch and websearch. The egress holds the model key, reaches only public addresses on ports 80 and 443, and calls the model over HTTPS: any OpenAI-compatible model the operator configures. Everything except the egress's outbound link and the app's browser-facing side sits on internal Docker networks with no route out.

  • The app authenticates with its own key, which the deployment limits to certain agents, and names each end user with an X-Vessel-User header. An orchestrator with an identity provider (such as AstralDeep's Keycloak) can instead send each user's signed token: the vessel checks its issuer, audience, expiry, authorized party and signature against the issuers the deployment trusts. Every task is bound to its owner: the app and user, or the issuer and the token's subject. The app owns conversation history: it sends the history with each turn and stores the messages that come back. It may attach images for agents whose model accepts them.
  • The vessel serves every agent in the deployment, each with its own A2A address and agent card. It runs the agent loop (ask the model, run the tool it asks for, return the result, repeat), streams every step as A2A events, and streams the turn's messages as a transcript in OpenAI chat format. It keeps only runs in flight, and enforces step limits, deadlines and cancellation. Agent briefings are fixed at setup: history from the app may never carry a system message.
  • Each agent's sandbox is a separate container that runs its file, shell and command tools. It has no network route, no secrets, no history, a read-only root, and its data mounted read-only. Inside it, every tool call runs under Landlock and a seccomp filter. The calls of one turn share a scratch directory (the turn's workspace, $TMPDIR), so a tool can save an intermediate result for the next call; it is deleted when the turn ends. Disk is limited per turn and per user (turn_disk and user_disk in the agent's [agents.sandbox], by default a quarter and half of scratch): no file may outgrow the turn's limit, each file or folder also counts 1 KB, files deleted but still open count until closed, and a turn that goes over either limit has its calls stopped and its workspace emptied. The sandbox checks usage twice a second, so a fast writer can briefly overshoot; scratch stays the hard ceiling. A call cannot start a new session or process group (setsid, setpgid), so everything it starts ends with it, and it may hold at most 4096 open files. A call cannot read another turn's files, see other processes, signal them, make socket files, use shared memory or message queues, or open network sockets. Users can share an agent without seeing each other. An agent that needs extra software ships a Dockerfile built on the AstralVessel image.
  • Service tools call the application itself: the vessel sends the model's arguments to an endpoint the deployment grants, with credentials the deployment holds, so the sandbox never needs network access or a secret. The application learns who asked through X-Vessel-Owner (the app and its user, or the issuer and subject), X-Vessel-Agent and X-Vessel-Tool headers.
  • The egress is the only way out. It adds the model key, so the harness never holds it, and forwards model calls only for the vessel, which holds a token derived at deploy time (so an agent's model api_key stays env:VESSEL_MODEL_KEY, the default). Its web proxy reaches only globally routable addresses on ports 80 and 443.
  • Web search runs on a SearXNG container the deployment starts beside the egress: no account or key, several engines at once so one engine's CAPTCHA or rate limit leaves the others answering, and all of its traffic through the egress. Only the vessel can reach it. A failed or refused search reaches the model as an error, never as "no results". [search] provider = "duckduckgo" in deployment.toml scrapes DuckDuckGo instead, with no extra container, but DuckDuckGo challenges repeated queries.

Nothing in the running system controls Docker. astralvessel deploy turns deployment.toml into a Compose file, the operator starts it, and the vessel serves an agent only once its sandbox reports that it is confined and was started from the current version of its sandbox profile, its manifest and its tools (a profile fingerprint set at deploy time, so an edit that was not redeployed is caught). Deploy labels the vessel with the deployment file and those fingerprints, so up -d recreates it whenever they change. Prompt files are read on every turn and need no redeploy. Applications join the vessel's front network to reach it, and to receive its service calls.

What it protects, and what it trusts

AstralVessel assumes the model, and everything the model reads (web pages, data, tool output), may be hostile: a prompt injection can make the agent try anything its tools allow. Against that it keeps:

  • the model key in the egress alone, and app keys as hashes in the vessel;
  • conversation history with the application, never in the vessel or a sandbox;
  • data read-only, tool calls with no network (a sandbox's only link is to the vessel), and each call out of other calls' files and processes;
  • each user's tasks bound to their owner, and each app limited to the agents it was granted;
  • agent-driven web traffic to public addresses on ports 80 and 443 only (the model endpoint is the operator's choice).

It trusts the operator and the host's kernel and Docker, the application to name its users honestly (or the token issuers the deployment lists), and the model provider and search engines with what is sent to them. Out of scope: kernel or container escapes, and what the agent says in its answer. An agent can pass anything it may read to its user, and with web tools it can send it to any public site, so give web tools only to agents whose data may leave.

Calling an agent

Each agent has an A2A endpoint at /agents/<id> and a public card at /agents/<id>/.well-known/agent-card.json. Two ways to ask:

  • A conversation turn: a text message, with the history as an application/vnd.astralvessel.transcript+json part. A caller that lists that media type in acceptedOutputModes gets the turn's messages streamed back to store; the bundled client does.
  • A structured skill call, the shape AstralDeep uses for external agents: a data part {"method": "tools/call", "name": "<id>", "arguments": {"question": "..."}}, usually with a non-streaming SendMessage. The answer comes back as the task's answer artifact, with any AstralPrimitives components under _ui_components, which AstralDeep renders.

A token's tools:* scopes, or an X-Vessel-Scopes header from an app, narrow the tools an agent may use on that call. They can only remove tools, never add them.

# deployment.toml: trust an identity provider's tokens for some agents
[[issuers]]
id = "astral"
issuer = "https://iam.example.edu/realms/Astral"
audience = "research-vessel"
jwks_url = "https://iam.example.edu/realms/Astral/protocol/openid-connect/certs"
authorized_parties = ["astral-orchestrator"]
agents = ["records-agent"]

Only asymmetric signatures are accepted (RSA, ECDSA, EdDSA); shared-secret and unsigned tokens are refused. Keys are fetched through the egress and refreshed when a token names a new one.

Declaring tools

# tools/species.toml: a command run in the sandbox, given the arguments as JSON on stdin
name = "species"
description = "List every species in the guide as JSON."
command = ["python3", "{package}/tools/species.py"]
scope = "tools:read"

# tools/lookup.toml: a service tool, answered by the application over HTTP
name = "lookup"
description = "Look a record up."
service = "records"
path = "/agent-tools/lookup"
# deployment.toml: the operator grants the service and its credentials
[egress]
upstream_url = "env:VESSEL_UPSTREAM_MODEL_URL"   # required: any OpenAI-compatible endpoint

[[agents]]
id = "records-agent"
package = "agents/records"
[agents.services.records]
url = "http://app:8000"
headers = { "X-Internal-Token" = "env:RECORDS_TOKEN" }

Each tool has one of Astral's six permission scopes, and an agent's card carries the strongest of its tools' scopes as a scope: tag Astral reads.

Files from tools

An agent that lists file types in its manifest ([output] files = ["png", "svg", "csv", "pdf"], any of these) can return files. A sandbox call writes them into $VESSEL_OUTBOX. After the call, the sandbox takes every entry out of the outbox and keeps regular files with a listed extension, a plain name and valid content, up to 4 MB each and 4 per call. A turn returns at most 8 files and 16 MB. The model is told which files were attached and why any others were refused.

Each file streams to the caller as an A2A artifact named file (raw bytes, mediaType, filename, and the tool's call_id in its metadata). Callers that list acceptedOutputModes get only the types they list. The bundled client yields them as file events. Like the transcript, files go out on the live stream only, and the app stores what it wants to keep. An SVG can carry script, so show it as an <img> or a download, never inline.

What is here

Path What
astralvessel/ The harness: agent loop, model adapter, tools, confinement, sandbox server, service client, A2A server, deployment loader, Compose generator, client
astralvessel/history.py An application-side conversation store, for apps that want one
egress/ The egress container: model passthrough that holds the key, and the guarded web proxy
examples/field-guide/ An example agent over a small sample field guide, with a declared command tool
examples/field-guide-reader/ A second, read-only example agent over the same data, with no shell or web access
deploy/ The example deployment.toml, .env.example, and verify-isolation.sh
conformance/run.py Conformance checks: what any agent server claiming to follow the spec must pass
stress/load.py A load test: many concurrent users, long histories, cancellations and flooding tool output
tests/ Unit and service tests with a scripted model

Install

pip install astralvessel                 # or: uv tool install astralvessel
astralvessel deploy deployment.toml      # writes compose.generated.yml for your deployment file

An installed copy deploys the images released with it, ghcr.io/astraldeep/astralvessel and ghcr.io/astraldeep/astralvessel-egress (amd64 and arm64); a checkout builds them from source. Both need Docker with Compose.

Run the example

cp deploy/.env.example deploy/.env      # fill in the model endpoint and key, app key and its sha256, sandbox token
cd deploy
astralvessel deploy deployment.toml      # (installed as in Develop) writes compose.generated.yml and sandbox tokens
docker compose -f compose.generated.yml --env-file .env up -d --build
./verify-isolation.sh                    # 43 isolation probes from inside the containers
set -a; . ./.env; set +a
for agent in field-guide field-guide-reader; do
  docker run --rm --network astralvessel_front -v "$PWD/../conformance:/conformance:ro" astralvessel:local \
    python /conformance/run.py http://172.31.247.10:8700 "$VESSEL_APP_KEY" $agent
done                                     # 20 conformance checks per agent

Sandboxes need a Linux kernel with Landlock ABI 6 or newer (6.12+); without it they refuse to start.

To run a second deployment on the same host, give it its own project and front network in [vessel] (for example project = "second", front_subnet = "172.31.248.0/24", front_address = "172.31.248.10"). Keep each deployment in its own folder, since its generated secrets sit beside its Compose file. The project name prefixes its containers, networks, volumes and locally built images; the conformance run above then uses --network <project>_front and that deployment's front_address.

Develop

uv venv -p 3.11 && uv pip install -e . pytest pytest-asyncio ruff
uv pip install astralprims                                       # optional: validates primitives
.venv/bin/python -m pytest -q
.venv/bin/python stress/load.py --users 50 --turns 3 --history 1000 --cancels 20

The vessel refuses to start without sandboxes unless VESSEL_DEV_UNSANDBOXED=1, which runs tools in its own process, unconfined. That mode is for tests only: never point a model at it.

Contributing

  • Work on a branch named for the change and open a pull request against main. CI (lint, tests on Python 3.11 and 3.12, and isolation probes on the running example) must pass, and a maintainer reviews before it merges.
  • Before pushing, run ruff check . and .venv/bin/python -m pytest -q. Add a test with each fix or feature.
  • AstralVessel stays application-neutral: no code, examples, prompts or docs about a particular application that uses it. An application's own agents live in that application's repo.
  • Never commit secrets, .env files, or generated Compose output (deploy/compose.generated.yml, deploy/.generated/).
  • CI also starts the example deployment and runs verify-isolation.sh against it. The conformance checks need a real model, so a change to the agent loop, the A2A interface, the sandbox or the web tools also needs them run by hand (the steps in "Run the example"), all passing. Say in the pull request that they passed.
  • Releases: bump version (X.Y.Z) in pyproject.toml on main, run the Release workflow by hand as a dry run, then push the tag v<version> on main. It publishes the images, waits for a maintainer's approval of the pypi environment, checks the images are public, then publishes the package.
  • The documentation lives in docs/ (built with MkDocs). Update it, and this README where it applies, in the same pull request when behavior changes.

Status

Version 0.5.0, November 2026, the first release: pip install astralvessel and images at ghcr.io/astraldeep/astralvessel. Done:

  • one vessel serving many agents, each with its own sandbox, card and A2A address
  • deployment.toml, per-app keys, and Compose generated by astralvessel deploy
  • per-call confinement inside each sandbox (Landlock and seccomp)
  • a workspace per turn that its tool calls share, deleted when the turn ends
  • files from tools (PNG, SVG, CSV, PDF), checked twice and streamed as A2A artifacts
  • application-owned history, with transcripts streamed in OpenAI chat format
  • service tools, image input, and per-agent sandbox images
  • verified-token callers (issuer allowlist, audience, owners keyed by issuer and subject), per-call scope narrowing, and structured skill calls in the shape AstralDeep sends
  • fetched web content fenced and labeled as untrusted before the model reads it
  • web search through a private SearXNG container, with failures reported as errors
  • a task store that survives restarts, owner binding, cancellation, and AstralPrimitives output
  • unit and service tests, isolation probes, conformance checks, a load test, and an independent security review
  • CI on every pull request: lint, tests, image builds, and isolation probes on a running deployment
  • releases from a version tag: multi-architecture images and the PyPI package

Load test, in one process on one core with a fake model (the load generator shares that core): 50 concurrent users, three turns each, every turn sending a 1,000-message history, running a confined shell command and streaming 1,000 pieces of text, plus 20 cancellations mid-turn. No failures, a median turn of about 6 seconds, peak memory about 380 MB, and no scratch left behind.

Not yet:

  • jobs: submitting without holding a stream open, completion callbacks, structured results
  • pausing a turn for approval or a policy check before a tool runs
  • usage and audit export
  • an admin API and a launcher that starts sandboxes at runtime
  • writable mounts, stronger isolation tiers (gVisor, microVMs), and MCP tool servers

For now AstralVessel is an independent project. Whether and how it joins AstralDeep is an open question for the Astral maintainers.

Licensed under Apache 2.0, like the rest of Astral.

Metadata

Release files for astralvessel 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for astralvessel 0.5.0
File Size Uploaded Provenance Actions
astralvessel-0.5.0.tar.gz 265.2 kB With provenance Details for astralvessel-0.5.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for astralvessel 0.5.0
File Interpreter ABI Platform Actions
astralvessel-0.5.0-py3-none-any.whl Python 3 none any Details for astralvessel-0.5.0-py3-none-any.whl

Total release size: 344.7 kB

Release files / astralvessel-0.5.0.tar.gz

Download URL astralvessel-0.5.0.tar.gz
Size 265.2 kB
Tags Source
SHA-256 checksum
How to use checksums
652c6060e1c4b6619f43626d606771226b683138d0096dfaf5267672febee174
BLAKE2b-256 checksum
How to use checksums
4401f43fa94ad030f6d68a1dc685ecf4c9cb98a70c41f46fe80dddf8ad053433
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Release files / astralvessel-0.5.0-py3-none-any.whl

Download URL astralvessel-0.5.0-py3-none-any.whl
Size 79.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f1e87802c74959f9a09d89c91ddb3d1ac1ecaeab91a4fb958a19c73cdadbc84f
BLAKE2b-256 checksum
How to use checksums
dc8fb2a00623a8338a18a0d8a5cdcea75a1ce2adedd65e474ae25ff1a10c8e3b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Security for astralvessel 0.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for astralvessel-0.5.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page