opencode + privacy-preserving image redaction
Run opencode inside an OpenShell sandbox where image payloads are kept away from the public inference provider and recovered, on demand, through a private local model.
opencode talks to inference.local, which is routed to a host-side OpenAI-compatible redaction proxy. The proxy decides per request, from a content policy, whether to let an image through to the public model or to strip it and tell opencode to recover the data via a local-model MCP server running on the same host.
opencode (sandbox)
├── chat ── inference.local ──▶ redaction proxy (host :8000) ──▶ public model (gpt-5.5)
│ │ if content_policy image: redact
│ └─ strips image, injects "use the local_model MCP"
└── tool ── local_model MCP (host :8001) ──▶ private/local model (e.g. LM Studio :1234)
The "demo magic": the proxy reads an OpenShell-style policy file (policies/content-policy.yaml) on every request. OpenShell itself does not interpret the content_policy block (that would need code changes) - only this proxy does. Flip one line and the next image request changes routing, no restart.
Layout
| Path | What |
|---|---|
proxy/ |
The redaction proxy + call_inference MCP server (a uv project). |
proxy/Makefile |
Start/stop the proxy and MCP; flip the demo policy. |
policies/opencode-policy.yaml |
Sandbox policy you give to OpenShell at create time. |
policies/content-policy.yaml |
Copy of the policy plus the content_policy block the proxy reads. |
sandbox/opencode.json |
opencode config: inference.local provider + local_model MCP. |
sandbox/opencode-provision.sh |
Writes the config + workspace inside the sandbox. |
sandbox/setup-sandbox.sh |
One command: create sandbox, provision, run web UI, expose it. |
Prerequisites
- An OpenShell gateway registered and selected:
openshell gateway list(the active one must be reachable ashost.openshell.internalfrom sandboxes - this is the default for the local podman/VM gateway). - A private/local OpenAI-compatible model for the MCP to call (the example defaults to LM Studio at
http://localhost:1234). - An
NVIDIA_API_KEYfor the public upstream (https://inference-api.nvidia.com). uvinstalled.
1. Start the host services
cd proxy
cp .env.example .env # then put your NVIDIA_API_KEY in .env
uv sync
make start # starts the MCP (:8001) and proxy (:8000)
The proxy and MCP bind to 0.0.0.0 so the sandbox can reach them via host.openshell.internal.
2. One-time gateway wiring (inference.local -> the proxy)
openshell provider create --name local-proxy --type openai \
--credential OPENAI_API_KEY=dummy \
--config OPENAI_BASE_URL=http://host.openshell.internal:8000/v1
openshell inference set --provider local-proxy --model openai/openai/gpt-5.5 --no-verify
--no-verify because host-side validation cannot resolve host.openshell.internal; runtime egress originates inside the sandbox and works.
3. Create the opencode sandbox
cd sandbox
./setup-sandbox.sh
This creates the opencode sandbox with policies/opencode-policy.yaml, writes opencode.json, starts opencode web on loopback inside the sandbox, and exposes it. Open the printed URL (e.g. http://opencode--web.openshell.localhost:<gateway-port>/).
Sanity check from the CLI:
ssh -F /tmp/opencode-ssh-config openshell-opencode \
'export HOME=/sandbox; cd /sandbox/workspace; opencode run --model inference-local/openai/openai/gpt-5.5 "say hi"'
4. Demo: flip the content policy
Default state ships as redact. Watch the proxy log while you drive opencode from the web UI:
cd proxy && make tail-proxy-log
- Allow (
make demo-allow): attach an image in opencode and ask about it. The proxy logsimage_redaction_skipped reason=allowed_by_policyand the image reaches the public model. - Redact (
make demo-redact): re-send. The proxy logsimage_redaction_applied, the image is replaced with theredact_description, and opencode calls thelocal_modelMCP tool to recover the data from the private model. The image never reaches the public model.
make demo-status shows the current rule. The proxy re-reads the file per request, so no restart is needed between toggles.
Request/response captures land in proxy/request-logs/ (gitignored) for inspection.
Tear down
cd proxy && make stop
openshell service delete opencode web
openshell sandbox delete opencode
Tests
cd proxy && make test