aFFido Quickstart

experimental pi-durable 1.3.0 node 24 python 3.13 self-hosted

Self-hosted AI agents

Agents you can
entrust.

Affido gives every user a sandboxed agent that starts with no powers. You grant capabilities one at a time, and the trace keeps every prompt, tool call and tool result.

Sessions live on volumes. Stop the container, change the model, or resume a conversation from six days ago. The work is still there.

$ docker compose up -d --build

Docker Compose · Node 24 · Python 3.13 · no published agent ports

The keepwatch the decision graph ▸
1container per user
2per-user volumes
0agent ports published
100%cited text hashed
01

Guarantees

An Affido agent has no npm, no npx and no corepack. It cannot install a new capability from inside the sandbox. Powers arrive as skills from the marketplace, installed file by file, and each file is checked against the manifest hash.

  1. 01

    Closed toolbox

    The runtime image deletes npm, npx and corepack. Extending the toolbox at runtime fails with command not found.

  2. 02

    One skill at a time

    A prompt starting /skill_name resolves to a server-built hint: read the installed SKILL.md first, or an honest not-installed note naming what is installed.

  3. 03

    Installs survive

    Skills live on the per-user volume. Pause, stop, eviction and reprovision do not remove them. Only a full reset does.

02

Receipts

With citations: "required", a reply only passes if every citation is a span the agent found in the corpus. The server records the document, version, line, quote and a sha256 of the quoted text. The model never writes those fields.

The gate rejects a reply with an uncited line, an undeclared citation id, or a quote that does not match the stored text. GET /corpus/citations?runId= re-checks every quote on every read, so a citation cannot outlive the document it points at.

Citation receipt verified
document
Contratto di manutenzione
file
contratto.md
version
1
line
4
quote
Il contratto si rinnova tacitamente il 1 marzo 2027.
text sha256
29ef9a73...f2617896

Example from e2e-test/citations.sh. Status: attribution_verified.

Gate rejection rejected
claimed quote
Il contratto si rinnova tacitamente il 1 marzo 2026.
stored text
Il contratto si rinnova tacitamente il 1 marzo 2027.
reason
quote_mismatch

A rejected quote writes nothing. The reply does not get a citation id.

03

Durability

State lives in two per-user volumes: SQLite at SESSION_DB and the workspace at WORKSPACE. Process memory holds nothing that must survive a stop.

SESSION_DB=/data/session.db
WORKSPACE=/workspace
  • idle pausethe harness closes after IDLE_MS with no requests
  • activatethe next request reopens storage and resumes an unfinished run
  • evictionthe manager stops a paused sandbox after STOP_AFTER_S
  • resumethe next request starts the same container and volumes
  • idempotentthe same requestId returns the admitted submission, never a second run
  • honest probesGET /health and GET /status never open storage

The e2e suite restarts a container and re-verifies its citations against the stored text. e2e-test/citations.sh asserts the same line and quote after the restart.

04

Under the hood

Caddy is the only published endpoint. A request under /u/<user_id>/... triggers a forward_auth subrequest to the manager. The manager provisions the network, two volumes and one container, or starts the paused one, then returns the endpoint in X-Sandbox-Endpoint. Caddy rewrites the path and dials the sandbox.

A client-supplied endpoint header never reaches the dial.

clientsession cookie
caddy :18080forward_auth
sandbox-managerprovision or start
agentno host ports
The services in the control plane
servicerole
caddythe only published endpoint; per-user routes /u/<user_id>/*
oltppostgres: the sandbox registry and the web client database
sandbox-managerpython 3.13; owns provisioning, resolve and eviction through the docker socket
pi-durable-agentone per user, created on demand, never a compose service
05

Quickstart

The control plane is three compose services. The agent image is built locally. The first request under /u/dev-user/ cold-provisions that user's sandbox.

quickstart

$ docker compose up -d --build

the control plane: caddy, postgres, sandbox-manager

$ ./dev.sh up --provider openrouter

build the agent image, mount the model secrets, start the stack

$ curl -H 'Cookie: better-auth.session_token=dev-token' localhost:18080/u/dev-user/status

after seeding the dev session, the first request cold-provisions that user's sandbox through caddy

seed the dev session first (SQL in the README) · caddy on :18080 · postgres on :15432 · e2e suite: ./e2e-test/run.sh

Local commands
commandeffect
./dev.sh up [--provider fake|openrouter]build the agent image and start the stack
./dev.sh reload [--provider ...]rebuild agent code, recreate sandboxes, keep all data
./dev.sh statuscontrol plane, sandboxes, volumes
./dev.sh downstop the control plane, keep sandbox state
./dev.sh resetremove containers, volumes and images
./verify.shtypecheck, unit tests, e2e suites, sandbox lifecycle checks
06

API

Every route is reached through Caddy at /u/<user_id>/.... The last column says whether the route opens storage. Routes that never activate cannot defeat the idle pause.

Agent HTTP API
endpointpurposeopens storage
GET /healthliveness, used by the docker healthcheckno
GET /statuscached agent status: model, conversation, entriesno
POST /ingeststage document bytes in the corpusno
GET /rawread the staged bytes of a document backyes
GET /sessionreopen the conversation, report id and entry countyes
GET /inspectentries, live tasks, submissions, usage of one conversationyes
GET /entryone stored entry verbatim, tool results includedyes
GET /files · GET /filelist and read the workspace, confined to WORKSPACEyes
POST /promptsubmit input; runs any tools the model callsyes
GET /corpus/citationsone run's citations, re-verified against stored textyes
POST /session/new · /session/switchone conversation per topic: start, resumeyes
POST /resetpi.reset head marker: cut context, keep the conversationyes
POST /pauseclose the harness now, for a deterministic pauseno

The uploads, skills and artifacts routes touch only the workspace and never activate the agent.

The name

Affido is Italian for I entrust.

The same root gives affidabile, reliable, and affidamento, the act of delegating bounded powers under supervision. A deployer entrusts the agent with a bounded set of powers. The record shows what the agent did with them.

The stack is the demo.

Run it, send a prompt, stop the container, send another. The session and the citations are still there.

affido//decision-graph.mp4

1920×1080 · h264 · 00:43 · trace replay