Privatemode reference instance
May 15, 2026 · View on GitHub
Reference documentation for running Wirken against the Privatemode 2.0
confidential-inference proxy. Everything described here is verified on
the current wirken binary; aspirational sections that did not survive
contact with a live run were removed. Claims about Privatemode itself
(model capabilities, endpoint shapes, release history) are sourced from
Privatemode's docs at https://docs.privatemode.ai/ and the public
source at https://github.com/edgelesssys/privatemode-public.
Quickstart
One sequential procedure to get a working Wirken + Privatemode instance. Assumes you already have Docker and a built wirken binary. Every step runs locally.
- Get a Privatemode access key. Sign up at https://www.privatemode.ai and generate an API key from the dashboard.
- Start the proxy on loopback. The upstream image binds
0.0.0.0by default; the-p 127.0.0.1:…form below constrains that to local traffic only:
Confirm it's up:docker run -d --name privatemode-proxy \ -p 127.0.0.1:8080:8080 \ ghcr.io/edgelesssys/privatemode/privatemode-proxy:latest \ --apiKey <YOUR_ACCESS_KEY>curl -s http://127.0.0.1:8080/v1/models | head. You should see JSON, not an error. - Configure Wirken.
wirken setupand choose:- Step 1 (provider): pick Privatemode. Accept the default proxy URL (
http://localhost:8080). Paste your access key. Accept the default model (kimi-k2.5). - Step 2 (channel): pick at least one — Telegram or Signal is easiest for a first run.
- Step 1 (provider): pick Privatemode. Accept the default proxy URL (
- Start wirken.
wirken run. You should seeProvider: openai/kimi-k2.5and your channel listed as a route. - Send a test message through the channel you configured. The response should come back in the same channel.
- Verify the round-trip landed in the audit log.
The session log records onewirken sessions list # find the session id wirken sessions verify <id> # exits 0 on an intact hash chainLlmRequestevent per turn, carryingprovider: "openai"andmodel: "kimi-k2.5"— that is Wirken's record that the turn went through Privatemode.
If any step fails, see Troubleshooting. For a mixed-provider agent (e.g., Privatemode on Signal, Anthropic on Slack), see Route a single channel to Privatemode instead of step 3.
What works
wirken setup picks Privatemode and writes a provider.json pointing at the local proxy. wirken run opens an OpenAI-shape client at POST http://localhost:8080/v1/chat/completions. The access key lives encrypted in the XChaCha20-Poly1305 vault. Default model is kimi-k2.5; context window and tool support depend on the chosen model — see the Models table.
End-to-end verified: message arrives on a channel adapter → agent runs → outbound request hits the local Privatemode proxy → response routes back through the channel. Every turn writes one LlmRequest event to the hash-chained session log, and wirken sessions verify walks the chain.
Architecture
Three trust zones, three enforcement mechanisms.
- Channels do not trust each other. Enforcement: per-channel process isolation via Rust phantom types.
- Tool execution is not trusted. Enforcement: configurable sandbox —
exec-only(default since 0.7.5),gvisor, oroff. Mode is read from~/.wirken/sandbox.json. Seedocs/security-properties.mdfor the full matrix. - Inference operator is not trusted. Enforcement: confidential-computing attestation inside the Privatemode local proxy. The proxy fetches a signed manifest from
cdn.confidential.cloud:443, validates the remote TEE attestation (AMD SEV-SNP, Intel TDX, NVIDIA H100 CC per the Privatemode docs), and establishes an end-to-end encrypted channel before forwarding any client request. Wirken trusts the proxy handshake — it does not independently verify the attestation.
Stacked, the only parties who see plaintext are the end user and the TEE.
Request flow
- Message arrives at a channel adapter. Adapter authenticates to core with Ed25519.
- Core loads channel-scoped credentials from the vault.
- Skills run in the configured sandbox (
exec-onlyby default) if tools are invoked. - Core emits an inference request to
privatemode-proxyatlocalhost:8080. - Proxy has already attested the Privatemode backend. It encrypts the request, ships it to
api.privatemode.ai:443, receives the encrypted response. - Proxy returns plaintext to Wirken.
- Wirken routes the agent output back through the originating channel adapter.
- Every transition is appended to the hash-chained audit log.
API shape
Privatemode exposes both Anthropic-compatible and OpenAI-compatible surfaces on the same proxy. Privatemode does not designate either as primary — the choice is Wirken's.
| Shape | Proxy path | Supports |
|---|---|---|
| OpenAI | POST /v1/chat/completions | Chat, tools, streaming |
| Anthropic | POST /v1/messages | Chat, streaming (SSE), system prompt, max_tokens required |
| OpenAI | POST /v1/embeddings | qwen3-embedding-4b only |
| OpenAI | POST /v1/audio/transcriptions | whisper-large-v3, voxtral-mini-3b |
All requests use Content-Type: application/json. The --apiKey flag on the proxy authenticates the proxy-to-backend leg.
Wirken uses the OpenAI shape (POST /v1/chat/completions). Implementation in crates/agent/src/llm.rs::privatemode(). Anthropic-shape and embeddings / transcription endpoints are exposed by the same proxy and documented upstream; Wirken does not send to them today.
Models
Verified against https://docs.privatemode.ai/models/overview/ as of Privatemode v1.38.0.
| Model ID | Context | Tools | Vision | Status |
|---|---|---|---|---|
kimi-k2.5 | 262k | Yes | Yes | Stable |
gemma-3-27b | 128k | Yes¹ | Yes | Stable |
gpt-oss-120b | 128k | Yes | No | Stable |
qwen3-coder-30b-a3b | 128k | Yes | No | Deprecated — migrate to kimi-k2.5 |
qwen3-embedding-4b | 32k | — | — | Stable (embeddings, 1024 or 2560 dim) |
¹ Gemma 3 cannot generate mixed text and tool outputs in the same response.
Wirken's default: kimi-k2.5. Matches Privatemode's recommended flagship: 262k context, tools, multimodal. Alternates are selectable via model = "..." in the provider config.
Route a single channel to Privatemode
The more interesting configuration — and the Privatemode partnership's headline demo — routes privacy-sensitive channels (Signal, iMessage) to Privatemode while other channels keep using a cheap-or-capable provider (Anthropic, OpenAI) under the same agent identity. One agent, one skill set, one permission surface; provider chosen per line. This is the #60 feature.
Pre-reqs: run the generic Quickstart once to get a default provider wired. Then:
- Store the Privatemode key in the vault under its own slot. The wizard would have stored it as
privatemode-access-keywhen Privatemode was your default; if Privatemode is an override, add the slot explicitly:echo "<YOUR_ACCESS_KEY>" | wirken credentials add privatemode-access-key --stdin - Re-run
wirken setupand answer yes to "Configure a per-channel LLM provider override?" When prompted:- Channel:
signal(or whichever channel you want routed to Privatemode) - Provider:
privatemode - Model:
kimi-k2.5 - Base URL:
http://localhost:8080/v1 - Name this key:
privatemode-access-key
- Channel:
- Inspect
~/.wirken/provider.json. It should now contain achannel_overridesblock:{ "provider": "anthropic", "model": "claude-sonnet-4-6", "channel_overrides": { "signal": { "provider": "privatemode", "model": "kimi-k2.5", "base_url": "http://localhost:8080/v1", "api_key_name": "privatemode-access-key" } } } wirken run. A Signal message now routes to Privatemode; a Slack message stays on the agent's default. The audit log'sLlmRequestevents carry the per-turn provider and model.
Fail-closed behavior: if api_key_name points at a key that doesn't exist, wirken run refuses to start with a message naming the missing key. Nothing silently degrades to the default.
Credentials
The Privatemode access key is stored in Wirken's XChaCha20-Poly1305 vault as a single inference credential (privatemode-access-key when used as a channel override; otherwise the default <provider>-api-key slot). The operator delivers the key to the proxy via --apiKey on the docker run command line — that leg is proxy ↔ backend, not Wirken's surface.
Deployment
Wirken does not manage the proxy lifecycle. Operators start privatemode-proxy themselves. The wirken setup wizard prints the reference command:
docker run -d --name privatemode-proxy \
-p 127.0.0.1:8080:8080 \
ghcr.io/edgelesssys/privatemode/privatemode-proxy:latest \
--apiKey <key>
Security note: the proxy binary binds 0.0.0.0 by default. The -p 127.0.0.1:8080:8080 form above restricts the published port to loopback so it is not reachable from other hosts. Do not drop the 127.0.0.1: prefix on a multi-tenant or internet-exposed host.
Under systemd, run the same docker invocation from a user unit, or point ExecStart= at the binary directly. Under Kubernetes, use Privatemode's upstream Helm chart at privatemode-proxy/charts/privatemode-proxy/ in https://github.com/edgelesssys/privatemode-public. Wirken does not ship unit files or charts of its own.
Run with manifest auto-fetch enabled (the default). Pinning via --manifestPath is not recommended for production per the upstream guide.
Verifying a round-trip
- Start the proxy (see Quickstart step 2).
wirken runwith Privatemode configured — either as the default provider or as a channel override.- Send a message through a configured channel.
- Confirm the agent response arrives in that channel.
wirken sessions listshows the session.wirken sessions verify <id>walks the hash-chained session log and exits 0 on an intact chain.- Each LLM turn writes one
LlmRequestevent into the session log carryingproviderandmodel. For Privatemode turns the provider isopenai(because Privatemode ships OpenAI-shape today) and the model iskimi-k2.5. A surfaced per-turn view in the CLI is out of scope here — operators who want to confirm directly can query the session log viasqlite3 ~/.wirken/audit.db "SELECT payload FROM session_events WHERE session_id = '<id>' AND event_type = 'LlmRequest'".
Troubleshooting
curl http://127.0.0.1:8080/v1/modelstimes out or is refused. The proxy container isn't up.docker ps | grep privatemode-proxy— if absent, re-run step 2 of the Quickstart. If present but unhealthy,docker logs privatemode-proxy; the common first-run cause is attestation againstcdn.confidential.cloud:443being blocked by an outbound firewall.- Proxy logs say "401" or "invalid api key". The key you passed to
--apiKeyis wrong. Generate a fresh one at https://www.privatemode.ai and restart the container. This is a proxy-to-backend error, not a Wirken error —wirken runwill print success, then every message will fail at inference. wirken runaborts with "no key named X". You configured a channel override but never ranwirken credentials add X --stdinto create the key. The message names the exact key and the fix.wirken runstarts but messages get no reply. Check two things in order: (a)docker logs privatemode-proxyfor proxy-side errors, (b)tail -f ~/.wirken/audit.db-backed session events orwirken sessions listto see if anLlmRequestevent was even emitted. If no event, the adapter never reached the agent — usually an adapter config problem. If the event is there but no response, the call to the proxy failed; proxy logs will say why.- First message after a cold start takes 10+ seconds. The proxy fetches a signed manifest from
cdn.confidential.cloud:443and runs the remote attestation handshake on first request. Second and subsequent requests are fast. Not a bug. - I see two
privatemode-proxycontainers listening on the same port.docker ps, kill the extra one. Wirken does not manage the proxy lifecycle — it assumes the operator owns:8080.
Verified claims
Grounded in the Privatemode 2.0 launch email (2026-04-22) and the public release notes.
- Privatemode supports Anthropic
/v1/messagesas of release v1.37.0. - Privatemode 2.0 bundles Kimi K2.5, the browser web app, and separate input/output/cached token pricing. The desktop app is deprecated in favor of the web app.
- Proxy attestation happens before Wirken sends any request. The proxy writes manifest transitions to
log.txtin its workspace. gpt-oss-120bis stable with 128k context and tool calling (text-only, no vision).kimi-k2.5shipped in v1.38.0 (2026-04-22) with 262k context and multimodal input.
Known gotchas
reasoning_contentdeprecation: removed from Anthropic-shape responses on 2026-05-01. If Wirken's audit writer recordsreasoning_contentdirectly, it must switch to the supported field before that date.- Client-version cutoff: Privatemode backends dropped support for clients older than v1.33 on 2026-04-17. Pin the proxy image to
:latestor a release ≥ v1.33. - Cache token reporting: Anthropic-shape responses from Privatemode do not return
cache_creation_input_tokensseparately; those tokens are folded intoinput_tokens. Audit records must not assume the field is present. - No Rust SDK: Privatemode ships official SDKs at
sdk/jsandsdk/wasmin the public repo. Wirken speaks the documented HTTP API directly (reqwestagainst the local proxy). - Default bind: the proxy binary binds
0.0.0.0— loopback-only exposure is the deployer's responsibility via port publication (docker-p 127.0.0.1:...) or systemd socket activation.
Gaps
- Wirken does not verify Privatemode attestation independently; it trusts the proxy handshake.
- No integration test in CI exercises the Privatemode code path. The adapter + LLM client paths have unit coverage, but the end-to-end request/response with the real proxy (or a stub) is verified by hand.
- The
privatemode-access-keycredential is per-agent, not per-caller. Multi-user deployments that want to bill inference to distinct humans need per-caller key scoping that does not exist today.
References
- Privatemode docs: https://docs.privatemode.ai/
- API overview: https://docs.privatemode.ai/api/overview/
- Anthropic endpoint: https://docs.privatemode.ai/api/messages/
- Proxy configuration guide: https://docs.privatemode.ai/guides/proxy-configuration/
- Models overview: https://docs.privatemode.ai/models/overview/
- Release notes: https://docs.privatemode.ai/release/
- Public source (MIT): https://github.com/edgelesssys/privatemode-public