- 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.
experimental pi-durable 1.3.0 node 24 python 3.13 self-hosted
Self-hosted AI agents
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
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.
The runtime image deletes npm, npx and corepack. Extending the toolbox at runtime fails with command not found.
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.
Skills live on the per-user volume. Pause, stop, eviction and reprovision do not remove them. Only a full reset does.
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.
Example from e2e-test/citations.sh. Status: attribution_verified.
A rejected quote writes nothing. The reply does not get a citation id.
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
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.
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.
| service | role |
|---|---|
| caddy | the only published endpoint; per-user routes /u/<user_id>/* |
| oltp | postgres: the sandbox registry and the web client database |
| sandbox-manager | python 3.13; owns provisioning, resolve and eviction through the docker socket |
| pi-durable-agent | one per user, created on demand, never a compose service |
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.
$ 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
| command | effect |
|---|---|
| ./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 status | control plane, sandboxes, volumes |
| ./dev.sh down | stop the control plane, keep sandbox state |
| ./dev.sh reset | remove containers, volumes and images |
| ./verify.sh | typecheck, unit tests, e2e suites, sandbox lifecycle checks |
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.
| endpoint | purpose | opens storage |
|---|---|---|
| GET /health | liveness, used by the docker healthcheck | no |
| GET /status | cached agent status: model, conversation, entries | no |
| POST /ingest | stage document bytes in the corpus | no |
| GET /raw | read the staged bytes of a document back | yes |
| GET /session | reopen the conversation, report id and entry count | yes |
| GET /inspect | entries, live tasks, submissions, usage of one conversation | yes |
| GET /entry | one stored entry verbatim, tool results included | yes |
| GET /files · GET /file | list and read the workspace, confined to WORKSPACE | yes |
| POST /prompt | submit input; runs any tools the model calls | yes |
| GET /corpus/citations | one run's citations, re-verified against stored text | yes |
| POST /session/new · /session/switch | one conversation per topic: start, resume | yes |
| POST /reset | pi.reset head marker: cut context, keep the conversation | yes |
| POST /pause | close the harness now, for a deterministic pause | no |
The uploads, skills and artifacts routes touch only the workspace and never activate the agent.
The name
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.
Run it, send a prompt, stop the container, send another. The session and the citations are still there.