Documentation
Security model
PoietesCMD is a single-owner installation. This page says what is protected, how, and where the edges are.
Sign-in
- No default credentials. The owner account is created once, at
/setup, and needs a setup token that is printed to the API's log (or set asPCMD_SETUP_TOKEN). Without access to the server, nobody can claim a fresh installation. - Password. At least 12 characters, stored as a scrypt hash (N = 32768, r = 8, p = 1).
- Sessions. A random 256-bit token in a cookie that is
HttpOnlyandSameSite=Strict, andSecurewhen the console is served over HTTPS. The database stores only a hash of the token. Sessions expire after inactivity (7 days by default). Changing the password ends all other sessions. - Throttling. Ten failed sign-ins from one address, or sixty in total, within 15 minutes block further attempts. Sign-in and setup are additionally rate limited per address.
Every API route except health and auth state requires a session. There is no unauthenticated agent API.
Request forgery
Three layers protect state-changing requests:
- The session cookie is
SameSite=Strict, so other sites cannot send it. - Every such request must carry a per-session CSRF token in a header. The token is only readable by the console's own pages.
- Requests that browsers label as cross-site, or whose
Originis not the console's own, are rejected. WithPCMD_PUBLIC_ORIGINset, only that origin is accepted.
The API sends no CORS headers, so no other origin can read its responses.
Provider credentials
- Keys come from environment variables or are stored encrypted (AES-256-GCM, key from
PCMD_ENCRYPTION_KEY, ciphertext bound to its connection). - Keys are never returned by the API. The console shows whether an environment variable is set, or the last four characters of a stored key.
- Keys are never placed in prompts, tool inputs, URLs, artifacts, browser storage or client bundles.
- Every log line, task event, tool result, stored error and artifact passes through a redactor that removes the configured secrets and anything shaped like an OpenAI or Anthropic key. Logs also drop
Authorizationandx-api-keyvalues and passwords in connection URLs. - The OpenAI-compatible adapter is constructed with explicit settings, so a key in
OPENAI_API_KEYis never sent to a different base URL.
What the agent can touch
- Files: only the workspace folder, read-only, and only supported text types. Paths are resolved and checked after following symbolic links. There is no tool that writes into the workspace.
- Output: only the artifact store, under a path derived from the task and tool call.
- Network: only
fetch_url, only GET, only public addresses. See Tools and limits for the exact rules. - Host: nothing. There is no shell or code-execution tool.
A task's tools are fixed when you create it. They are the only tools the model is told about, and a call to any other tool is refused.
Untrusted content
Files you upload and pages the agent fetches may contain text written to steer a model ("ignore your instructions and…"). The defence does not rely on the model resisting it:
- Tool output cannot add tools, change limits or approve anything. Those are enforced in the worker from the task's own settings.
- A tool outside the task's list is refused even if the model asks for it.
- An operation that needs approval waits for you, and your approval covers one exact set of arguments.
- The network restrictions apply to every request, approved or not.
The model can still be misled about what to write by the content it reads. Read results with that in mind, especially for tasks that fetch pages.
Rendering
Model answers and artifacts are rendered as Markdown with raw HTML disabled. Images are not loaded, so generated content cannot make your browser call a third party. Links open in a new tab without referrer or opener. Downloads are served as attachments with nosniff and a sandboxing content security policy, never as pages.
The console sends a content security policy that allows scripts, styles, images and connections from its own origin only, and forbids framing.
Deployment notes
- Publish only the console. In Compose the API and PostgreSQL are not published at all.
- Serve it over HTTPS if it leaves your machine, and set
PCMD_PUBLIC_ORIGIN. - Per-address limits depend on
X-Forwarded-For. If the console is exposed without a reverse proxy that sets it, a client can supply that header itself and evade per-address limits; the total-failure cap still applies. Put a reverse proxy in front. PCMD_PROXY_SECRETlets the API tell the console's proxy from other callers. Forwarded headers are believed only with it.- Containers run as an unprivileged user. The Docker socket is not mounted anywhere.
Audit
Settings → Security shows the audit log: sign-ins, failed sign-ins, provider changes and tests, approval decisions, memory changes, file uploads and deletions, and schedule changes, each with time and detail. Task events are a separate, per-task record of everything the worker did.
Lost password
There is no reset by email: the installation has one owner and no mail service. Recovery needs access to the server, which is the point. Remove the owner record and the sessions, restart the API, and run first-run setup again:
docker compose exec -T postgres psql -U poietes -d poietes -c "DELETE FROM sessions; DELETE FROM owner;"
docker compose restart api
docker compose logs api # a new setup token is printed
Then open /setup and choose a new password. Everything else is kept: tasks, memory, schedules, files, and stored provider keys, which are protected by PCMD_ENCRYPTION_KEY rather than by the password. In development, run the same SQL against 127.0.0.1:15791 and restart pnpm dev.