Mango

Environment Work

Claim leased Session activations and verify bounded healthchecks for self-hosted workers.

Edit on GitHub

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 -> stopped
  • Poll tentatively claims the oldest available item and returns a fresh Work secret. It is a URL-safe base64 JSON payload containing the claim's sessions_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 optional worker_id query parameter contributes to queue statistics and operational correlation; it is not a credential.
  • Ack removes the item from the queue and changes it from queued to starting. 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 with sessions_token. A stale timestamp or expired current lease returns 412; reclaim rotates the credential, so an old owner is rejected at authentication.
  • Fail is the terminal path for a permanent immutable-input error. It marks the Work stopped and atomically terminates the Session and all of its Threads with session_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 records stopped. 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}/work

Stop 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 docker

The 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_me

Creation 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.

On this page