Environment Work
Claim leased Session activations and verify bounded healthchecks for self-hosted workers.
Environment Work is Mango's activation and lease protocol for self_hosted
Environments. It is not a second Agent runtime. A worker claims a Session,
listens to the existing Session
event stream, executes agent_toolset_20260401 tools in customer-hosted
infrastructure, and posts results through the existing user.tool_result
or user.custom_tool_result event.
Mango creates Session Work when a self-hosted Session has runnable input. The Work insert, public event admission, Session projection update, and Temporal outbox wakeup commit in one PostgreSQL transaction. Further runnable input is coalesced while the Session has a live Work item; input received after Stop creates a new activation.
Worker flow
Poll -> Ack -> Heartbeat(NO_HEARTBEAT) -> Heartbeat(previous timestamp) -> Stop
queued starting active stopping/stopped
permanent bad input -> Fail -> stoppedPolltentatively claims the oldest available item and returns a fresh Worksecret. It is a URL-safe base64 JSON payload containing the claim'ssessions_token. A stale unacknowledged claim may be reclaimed; every reclaim rotates the token. The token becomes usable after Ack; the tentative Poll response alone does not authorize Session execution. The optionalworker_idquery parameter contributes to queue statistics and operational correlation; it is not a credential.Ackremoves the item from the queue and changes it fromqueuedtostarting. It uses the polling client's Environment credential and has no request body. Repeating Ack after a successful transition is idempotent, so a lost success response can be retried.- The first heartbeat uses
expected_last_heartbeat=NO_HEARTBEAT. Every later heartbeat echoes the exact timestamp returned by the previous response.desired_ttl_seconds, when supplied, must be from 1 through 300. Healthy workers renew continuously; the five-minute cap bounds stale-owner access. Heartbeat, Fail, and the worker's final Stop authenticate withsessions_token. A stale timestamp or expired current lease returns412; reclaim rotates the credential, so an old owner is rejected at authentication. Failis the terminal path for a permanent immutable-input error. It marks the Work stopped and atomically terminates the Session and all of its Threads withsession_input_failed_error. A transient API, object-store, or network error does not call Fail or Stop: the worker retries five times, then leaves the lease to expire so queue reclaim can retry the same activation.- Graceful Stop changes active Work to
stopping; the next heartbeat tells the worker to cancel. A stopping token expires no later than its current TTL even if no poller performs the eventual state cleanup. Forced Stop immediately recordsstopped. A Workspace API key retains operator authority to stop Work without possessing its Session credential.
The API exposes Create, Get, Update, List, Ack, Heartbeat, Result, Fail, Poll, Stats, and Stop beneath:
/v1/environments/{environment_id}/workStop returns 204 No Content, and an empty Poll returns an empty JSON object.
Get, List, metadata Update, and Ack responses redact the Work secret as null;
only Poll returns the raw payload. The Go WorkPoller preserves the polled value
in memory when it returns the acknowledged item.
Check an Environment's execution path
With a Workspace API key, create a healthcheck before sending a real Session:
curl -sS -X POST "$MANGO_BASE_URL/v1/environments/$MANGO_ENVIRONMENT_ID/work" \
-H "Authorization: Bearer $MANGO_API_KEY" -H 'Content-Type: application/json' \
-d '{"data":{"type":"healthcheck"}}'The 201 response has data: {"type":"healthcheck"}, state: "queued",
result: null, and expires_at fixed at 120 seconds after creation. Each POST
creates a new check. The existing supervisor claims and Acks it with its
Environment key; the Docker worker heartbeats with the per-Work token, then
runs a fixed Bash process and verifies a small workspace file's contents. It
uses an ephemeral workspace, permits no caller-provided command, and needs no
Agent, Session, event stream, or model endpoint.
Retrieve GET /v1/environments/{environment_id}/work/{work_id} with the
operator's Workspace key. A terminal check has state: "stopped" and
result: {"status":"succeeded","message":"Sandbox process and workspace check passed"}
or a failed, timed_out, or cancelled result. List returns the same durable
result. The message contains at most 1024 characters. Session Work retains
data: {"type":"session","id":"sesn_..."} and has null expiry/result.
Execution is limited to 10 seconds; the Docker launcher bounds the container
attempt to 30 seconds and removes its container and tmpfs workspace. Docker
reconciliation and cleanup have separate 15-second request bounds; after
cancellation no Work secret is delivered and Stop uses zero container grace.
An unavailable Docker daemon can prevent confirmed removal; the launcher reports
that failure. A lost
worker can be reclaimed with the same Work ID and a rotated token before the
absolute deadline. Get, List, Poll, Ack, Heartbeat, Result, Stop, and Stats
reconcile overdue checks to timed_out; without requests there is no promise
that a background timer updates the stored state at the deadline. Stop records
cancelled unless the check has already expired or finished.
The worker submits POST .../work/{work_id}/result with status (succeeded
or failed) and message. Only its current active lease may complete the
check. Completion atomically commits the immutable result and stops Work.
Identical result retries are accepted for 30 seconds; a conflicting retry gets
409. During this short terminal window the token authorizes only result
retry, never reads, heartbeat, Poll, or Session access. Workspace and Environment
keys cannot fabricate a worker result. A stale or reclaimed token is rejected.
All native SDKs expose creation, retrieval, pagination, and completion under
environments.work (Environments.Work in Go). The Go WorkPoller dispatches
both data variants and rejects unknown ones. The Go EnvironmentWorker calls
an explicit provider-owned Healthcheck callback with a 10-second context;
missing callbacks report failure. The first-party Docker worker configures the
fixed sandbox probe. Python and TypeScript provide typed HTTP clients for the
same contract and do not ship execution helpers.
A successful result establishes the configured execution path at that moment; it does not test model credentials, every Session input, provider services, or arbitrary host diagnostics. See the design and failure invariants.
Session inputs and state
The Work and Session event APIs provide the worker protocol. The Go SDK ships a
provider-neutral WorkPoller for poll, Ack, drain, and reclaim, plus a
single-Session SessionToolRunner for stream/history recovery, confirmation
gates, local dispatch, and result submission. EnvironmentWorker composes both
with conditional heartbeat, lease-loss cancellation, the scoped Work-secret
handoff, and final forced Stop. Run owns Poll through Stop in one trusted
process; HandleItem runs only an already-acknowledged item and can read its
narrow identity from MANGO_WORK_ID, MANGO_ENVIRONMENT_ID,
MANGO_WORK_TYPE, and MANGO_SESSION_ID (Session Work only). Its Work secret must be supplied through a protected
launcher transport whenever untrusted subprocesses share the sandbox.
These SDK lifecycle helpers do not choose or create a sandbox. The composed Go worker prepares immutable custom Skills and attached Memory Stores; File and Git inputs remain outside this slice. Mango's first-party Docker launcher composes them with container and workspace-volume lifecycle; other launchers still own that boundary. See the staged self-hosted worker design.
Web Search and Web Fetch stay on the configured model endpoint. The worker
handles the six shell/file tools and any custom tools registered by its
operator; it never needs a Web executor or the model credential. Native Web
responses remain in the durable model transcript, and only the existing text
and usage projections are public. Web tools require always_allow and an
endpoint that supports the native Web declarations. Disable them otherwise.
First-party Docker worker
Build and run the preview reference worker from the repository root:
docker build -f deployments/self-hosted/docker/Dockerfile \
-t mango-self-hosted-worker:local .
MANGO_ENVIRONMENT_KEY=replace-with-an-environment-key \
MANGO_ENVIRONMENT_ID=env_replace_me \
MANGO_BASE_URL=http://localhost:8080 \
MANGO_DOCKER_BASE_URL=http://host.docker.internal:8080 \
go run ./cmd/mango-worker dockerThe supervisor uses the Environment key only for Poll and Ack. It creates a
hardened container for each acknowledged Work item. Resource IDs and the
sandbox-visible Mango URL are non-secret environment values; the opaque Work
secret crosses a one-shot attached stdin stream and is absent from container
environment and command metadata. Before reading it, the item runner becomes a
non-dumpable Linux process so same-UID Bash children cannot inspect its /proc
environment, memory, or descriptors. The item
process performs the first heartbeat before executing tools, continuously
renews the lease, reconciles Session events, posts tool results, and force-Stops
ordinary exits. It runs the Go SDK's six core local tools in /workspace and
scrubs Mango credentials from the shell environment. Bash is a persistent PTY
session within one Work container: working-directory and environment changes
survive later calls, restart creates a fresh shell, and timeout_ms overrides
the shell-call timeout without extending the runner-wide tool deadline. Timeout,
cancellation, shell termination, or
corrupt completion framing closes the old shell before another call can run.
Containers are removed after each activation. A Docker named volume derived from the Session ID is retained, so later Work for the same Session resumes the same workspace; shell process state deliberately does not survive that container boundary. Before dispatch, the worker prepares the frozen custom Skill pins and attached Memory Stores described below. It does not yet prepare File/Git resources or Session outputs, and it is not a hardened hostile multi-tenant boundary. See the Docker worker deployment notes.
The file tools are confined to /workspace plus the exact Memory Store roots
attached to the Session. write and edit reject read-only roots. Bash itself
is intentionally not path-confined within the container, so read-only access is
a file-tool policy rather than a filesystem guarantee; the Docker boundary and
its mounts, credentials, user, capabilities, resources, and network policy
remain the security boundary.
Workers must honor evaluated_permission independently of execution location.
An ask call waits for a persisted allow confirmation; a deny must never run.
After allow, execute and submit user.tool_result for the original call.
An approval alone does not clear the Session's pending-action barrier. On
reconnect, the Go runner reads confirmations and results as well as tool uses,
copies the owning Thread ID for child-call results, and exposes the original
event ID so a tool can make its external side effects idempotent. See
external tool approvals.
Before it starts the tool runner, the Go EnvironmentWorker fetches the frozen
Session snapshot with the scoped item credential and downloads every primary
and roster Agent custom Skill pin. Primary Skills are expanded below
<workdir>/skills/<name>; external roster Agents receive stable isolated roots
below <workdir>/skills/.agents/. One fresh tree is staged directly below the
canonical Workdir and atomically replaces the prior Session tree, so paths that
an earlier tool replaced with symlinks are never traversed. Downloads verify
the frozen compressed length and SHA-256 digest, reject unsafe or non-regular
archive members, and share Session-wide limits of 500 unique scope-pins,
500 MiB compressed and expanded bytes, and 10,000 files. The tree is removed
when Work ends. Permanent validation failures durably terminate the Session;
temporary retrieval failures remain reclaimable.
The Agent loop activates a Skill from Mango's own immutable archive, so it does
not require inbound access to the worker filesystem. Supporting files and
scripts are read or executed from the independently materialized worker copy.
Model-visible self-hosted paths start with skills/ and are relative to the
worker's configured workdir; the control plane never assumes that a future
provider mounts it at /workspace. See Skills.
The same frozen Session snapshot contains its Memory Store attachments. Before
constructing the toolset, the worker downloads each Store to its absolute
/mnt/memory/... path and passes those exact writable/read-only roots to the
tool factory. Writable Stores reconcile by path after tool calls at a default
15-second cadence: remote changes win conflicts, local changes use the existing
Memory create/update API and SHA-256 preconditions, and deletions require a
second observation before they may reach the server. A clean end performs one
final full sync; every end also receives a separately bounded, push-only flush
that cannot pull or delete. A Store marker prevents an altered directory from
being uploaded or removed. See Memory.
Security boundary
The supervisor uses an Environment-scoped API key to Poll and Ack. Poll additionally
issues an unpredictable per-claim credential payload; only the SHA-256 digest
of its sessions_token is stored. That token is limited to the claimed Work's
Heartbeat, Fail, and Stop, the claimed Session's read/event execution routes, and the
immutable Skill inputs plus the Memory Stores attached to that Session.
Memory mutations are limited to read_write attachments and are fenced against
the live Work lease in the same database transaction as the change. On the event write
route it may submit only user.tool_result and user.custom_tool_result, not
ordinary user messages, interrupts, approvals, or system.message. It becomes
invalid when the Work stops, its lease expires, or it is reclaimed. An existing
Session event stream rechecks that ownership once per second and closes after
invalidation. A Workspace key retains full operator access and must never enter
an untrusted Session sandbox. A sandbox runner necessarily
receives its per-Work token, but tool subprocesses must not inherit that token
or be able to inspect it through their parent process. An allowlisted child
environment is necessary but not sufficient when untrusted code shares a Linux
process identity with the trusted runner.
See capabilities and limits for the current support boundary.
Supervisor key lifecycle
Issue a key on the Mango operator host, where MANGO_DATABASE_URL is configured:
mango api-key create -workspace wrkspc_default -environment env_replace_me -label docker-pool
mango api-key list -workspace wrkspc_default
mango api-key revoke -id key_replace_meCreation prints api_key once. Supply that value as MANGO_ENVIRONMENT_KEY to
mango-worker docker; it does not read MANGO_API_KEY. The key can only Poll,
Ack, and read Stats on its bound Environment. Stats exposes aggregate queue
counts and the oldest queued timestamp for supervisor diagnostics. Work
Get/List/Update, Environment management, Session APIs, Files, and Vaults return
403 permission_error. Invalid and revoked keys return 401 authentication_error.
First-party SDKs use their existing bearer configuration (APIKey, api_key,
or apiKey) with the Environment key; their Environments.Work methods retain
the same request and response shapes.
Rotate by issuing a replacement key, deploying it to the supervisor, and revoking the old key. A replacement can Ack a pending claim in the same Environment. Poll and Ack check the key inside their database transaction, including every empty long-poll retry. Revocation waits for transactions already holding the key lock; after revocation commits, the old key cannot commit another claim or Ack. Already-Acked Work keeps its per-Work token and finishes under its existing lease. Unacknowledged claims remain reclaimable after the normal reclaim delay. To stop active execution immediately, an operator separately stops the Work.