aFFido Quickstart

experimental pi-durable 1.3.0 node 24 python 3.13 provider-agnostic 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.

The model layer is provider-agnostic: each user picks a provider from the catalog, or any OpenAI-compatible endpoint, and brings their own key. The default stack runs keyless.

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

sandbox//exploded

isolated

  1. entries150
  2. tool calls12
  3. failures kept3
  4. cited text hashed100%
  • caddy :18080 the only published endpoint
  • pi-durable-agent starts with no powers
  • SESSION_DB · WORKSPACE survive a stop
  • entries · sha256 every trace is measured
An exploded, isometric view of one Affido sandbox. Caddy on :18080 is the only published endpoint; the agent runs inside the container with no ports published. Two volumes hold the session database and the workspace. Above the agent sit the tool calls, and above them the trace ledger, where every entry and every citation keeps a sha256 of the quoted text.
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

The model layer

Affido is neutral about whose models run inside it. Each user registers a provider and its key (BYOK). The choice is not permanent: switch providers and the same conversation keeps running.

  • catalognine curated built-ins: openrouter, openai, deepseek, groq, together, fireworks, xai, cerebras, mistral
  • any endpointnot in the catalog? any OpenAI-compatible base URL and key registers as a provider
  • key custodykeys live as AES-256-GCM ciphertext in Postgres; the agent holds them in an in-memory store only
  • faux defaultthe keyless scripted provider answers with no network call; demos and e2e runs need no secret
  • egressclosed by default; AGENT_EGRESS=open is the explicit opt-in for internet providers, and the iptables host wall stays
user keyregistered once, per user
postgresAES-256-GCM at rest
agent memoryin-memory store only
nowhere elseno env · no filesystem · no logs

The default stack runs with zero model secrets: no key is mandatory. When a user brings one, the API answers with a sha256 fingerprint, never the key itself.

05

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
06

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. With no --provider flag the stack runs the keyless faux provider: no API key anywhere in the loop.

quickstart

$ docker compose up -d --build

the control plane: caddy, postgres, sandbox-manager

$ ./dev.sh up --provider openrouter

bring a real provider: build the agent image, mount the model key, 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; fake is the keyless default
./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
07

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