Drive a local OpenCode instance from Nextcloud Talk via polling — a self-hosted chat bridge for a coding agent.
Project description
opencode-talk-bridge
Drive a local OpenCode coding agent from Nextcloud Talk — a self-hosted chat bridge.
An incoming Talk message is forwarded to a locally-running opencode serve
HTTP API; the agent's reply (long output / code as a file attachment) is posted
back into the conversation. The bridge uses polling (no webhooks), so it
works on institutional Talk instances where you have no admin access and cannot
register a webhook bot.
⚠️ This bridge runs AI coding-agent actions on your machine, triggered from chat in infrastructure you may not control. Read the Threat model first. A non-empty user allowlist is mandatory — the bridge refuses to start without one.
How it works
Nextcloud Talk ──long-poll──▶ bridge ──HTTP──▶ opencode serve (localhost)
(you type) ◀──post──────── bridge ◀──SSE──── (agent runs locally)
- Polling, not webhooks: the bridge long-polls the Talk chat endpoint
(
lookIntoFuture=1+lastKnownMessageId) using only your app password. - OpenCode stays local: the bridge talks to
opencode serveover127.0.0.1. It does not start OpenCode — run it yourself, via launchd, or via the companion menubar app. The bridge health-checks it and reports when it is down. - Prompts block; permissions are concurrent:
POST /session/{id}/messageblocks until the turn finishes, so each prompt runs in a worker thread while a shared SSE consumer (/global/event) routes permission requests back into the conversation in real time.
Requirements
- Python ≥ 3.10, macOS or Linux.
- A Nextcloud account with Talk and an app password (Settings → Security → App passwords) — not your login password.
- A running
opencode serve(OpenCode ≥ 1.15). Default endpointhttp://127.0.0.1:4096.
Install
One command — installs the opencode-talk-bridge CLI into an isolated
environment and onto your PATH (uv or
pipx):
uv tool install git+https://github.com/leiverkus/opencode-talk-bridge.git
# or: pipx install git+https://github.com/leiverkus/opencode-talk-bridge.git
Once the first PyPI release is published this becomes
uv tool install opencode-talk-bridge(no git URL) — see docs/publishing.md.
Then create your config interactively and run:
opencode-talk-bridge --init # interactive .env wizard (chmod 600)
opencode-talk-bridge --check # validate config + OpenCode health
opencode-talk-bridge # run (reads ./.env, or pass --env-file PATH)
--init asks only for the essentials; every other option keeps a safe default
and can be added later (see .env.example for the full list).
First run against a real Talk server? Follow the end-to-end smoke test — it tests the riskiest paths first and covers the two-accounts gotcha.
Upgrade with uv tool upgrade opencode-talk-bridge (or pipx upgrade …).
From source (for development)
git clone https://github.com/leiverkus/opencode-talk-bridge.git
cd opencode-talk-bridge
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]" # editable + test/lint tools
cp .env.example .env # then edit .env
The OCS client is the separate nextcloud-talk-core
package from PyPI (tracking the 1.x line) — never reimplemented or vendored here.
Configure
Edit .env (see .env.example for the full list):
| Variable | Required | Purpose |
|---|---|---|
NC_URL, NC_USER, NC_APP_PASSWORD |
✅ | Nextcloud Talk credentials (app password). |
TALK_CONVERSATIONS |
✅ | Conversation token(s) to watch, or all. |
ALLOWED_USERS |
✅ | Comma-separated user IDs allowed to issue commands. Empty ⇒ refuses to start. |
OPENCODE_URL |
– | OpenCode base URL (default http://127.0.0.1:4096). |
OPENCODE_USERNAME / OPENCODE_PASSWORD |
– | Basic-Auth if your OpenCode server is secured. |
OPENCODE_DIRECTORY |
– | Workspace directory for OpenCode sessions. |
OPENCODE_MODEL |
– | Default model providerID/modelID. |
SHARE_WEBDAV_DIR |
– | WebDAV folder (relative to user root) for code/long-output and TTS attachments. Created on demand; blank disables attachments. |
RESPONSE_STREAMING, STREAM_THROTTLE_MS |
– | Live-stream replies via message editing (default on, 1500 ms throttle). |
HIDE_TOOL_MESSAGES, HIDE_THINKING |
– | Suppress 💻 tool / 💭 thinking notices. |
BOT_LOCALE |
– | UI language: de (default) or en. |
STT_API_URL / STT_API_KEY / STT_MODEL / STT_LANGUAGE |
– | Whisper-compatible speech-to-text for voice notes. |
TTS_API_URL / TTS_API_KEY / TTS_MODEL / TTS_VOICE |
– | OpenAI-compatible text-to-speech for /tts replies. |
TASK_LIMIT, LIST_LIMIT, TRACK_BACKGROUND_SESSIONS |
– | Scheduler limit, picker size, background notices. |
DB_PATH, STATUS_FILE, LOG_LEVEL |
– | Storage + logging. |
ALLOWED_USERSmust be the stable user ID (the login, e.g.jdoe), not the display name. The bridge matches the OCSactorIdwithactorType == "users", so guests and bots can never impersonate an allowed user even if display names collide.
Run
# Validate config + check OpenCode health, then exit:
opencode-talk-bridge --check
# Run the bridge (foreground):
opencode-talk-bridge # or: python -m opencode_talk_bridge
Commands (in Talk)
Send any message (or a voice note / file) to prompt OpenCode. The reply streams live into one message as it is generated. Slash-commands:
| Command | Effect |
|---|---|
/new |
Start a fresh OpenCode session for this conversation. |
/session |
Show the current session id. |
/sessions |
List & switch recent sessions. |
/rename <title> |
Rename the current session. |
/detach |
Detach from the current session. |
/messages |
Browse messages, then revert or fork. |
/model [providerID/modelID] |
Show, pick, or set the model. |
/agent [name] |
Show, pick, or set the agent (e.g. plan/build). |
/projects |
Switch the OpenCode project. |
/worktree |
Switch the git worktree. |
/commands |
Browse & run custom OpenCode commands. |
/skills |
Browse & run OpenCode skills. |
/mcps |
Enable/disable MCP servers. |
/tts |
Toggle spoken (audio) replies (needs TTS_* + SHARE_WEBDAV_DIR). |
/task <min> <prompt> |
Schedule a task (/task every <min> … to repeat). |
/tasklist |
List & delete scheduled tasks. |
/stop |
Abort the current run. |
/status |
Show bridge & OpenCode health. |
/help |
List commands. |
/projects lists every project the OpenCode backend knows (same as the desktop
app). /sessions, /commands, and /skills are project-scoped: they show
what the conversation's current project shows — pick a project with /projects
(or set a default OPENCODE_DIRECTORY). With no project bound, /sessions falls
back to a global list across all projects.
No inline buttons on Talk → every picker is a numbered list: reply with
the number. When OpenCode asks permission for a dangerous action, reply
ja (allow once), immer (allow for this session), or nein (deny);
agent questions are answered by their option number or free text.
Set BOT_LOCALE=en for English UI strings (default de).
Run under launchd (macOS)
A user-agent example is in deploy/. Edit the paths, then:
cp deploy/com.leiverkus.opencode-talk-bridge.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.leiverkus.opencode-talk-bridge.plist
# stop / uninstall:
launchctl bootout gui/$(id -u)/com.leiverkus.opencode-talk-bridge
Secrets stay in .env (read from WorkingDirectory), never in the plist.
Status file (menubar-app contract)
The bridge atomically writes STATUS_FILE (JSON) on every state change. A
supervising app (e.g. the Swift menubar app) can poll it. The file is always
valid JSON (temp-write + rename). Schema:
{
"state": "polling",
"since": 1748600000,
"opencode_healthy": true,
"conversations": ["abcdef12"],
"last_error": null,
"version": "0.1.0"
}
state ∈ starting · polling · working · opencode_down · error ·
stopped. since is a Unix timestamp; conversations lists watched tokens;
last_error is a human-readable string or null.
Threat model
This bridge executes coding-agent actions on your machine (file writes, shell commands via OpenCode), triggered by chat messages in a Nextcloud Talk instance you may not administer (e.g. a university server). Treat that Talk instance as semi-trusted infrastructure. The trust boundary and mitigations:
- Access control is the allowlist. Only
actorType == "users"IDs inALLOWED_USERScan issue commands; every other message is ignored. An empty allowlist aborts startup. Anyone who can post as an allowlisted user can run code as you — keep the list minimal and the control conversation private. - Dangerous actions require confirmation. OpenCode's permission prompts
(shell, file writes) are surfaced into Talk and must be answered; nothing
runs without a
ja/immer. Configure OpenCode's own permission policy conservatively as defence in depth. - Secrets are never echoed. The bridge does not post your app password or tokens, and permission prompts show only the action kind and a length-capped pattern — not raw command arguments that might contain secrets. Be aware that OpenCode's answer text itself could contain sensitive repo content; it is posted into Talk, whose admins can read it.
- A compromised Talk server could inject messages appearing to come from an allowlisted user. The allowlist mitigates casual misuse, not a fully compromised IdP/server. Do not point this at sensitive repos if that is your threat model.
- Local-only OpenCode. Keep
opencode servebound to127.0.0.1. If you expose it, secure it withOPENCODE_USERNAME/OPENCODE_PASSWORD.
Development
ruff check src tests
ruff format --check src tests
pytest
All tests use mocked HTTP for both Talk and OpenCode — no live calls. CI runs the matrix on Python 3.10–3.13.
Changelog
See CHANGELOG.md.
License
MIT © Patrick Leiverkus
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file opencode_talk_bridge-0.2.7.tar.gz.
File metadata
- Download URL: opencode_talk_bridge-0.2.7.tar.gz
- Upload date:
- Size: 64.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b27d4d7524c6ad0dbffebc120d3b16cc0742def557646cc45ed2c33bfbeec4a
|
|
| MD5 |
1132c9f199fbee8b6d672c98be5bdc7c
|
|
| BLAKE2b-256 |
8fe0ba651a42631e2882e0f38f8f46b5c48a69bd1b8a5ba5050ea831e52b09d4
|
Provenance
The following attestation bundles were made for opencode_talk_bridge-0.2.7.tar.gz:
Publisher:
publish.yml on leiverkus/opencode-talk-bridge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
opencode_talk_bridge-0.2.7.tar.gz -
Subject digest:
8b27d4d7524c6ad0dbffebc120d3b16cc0742def557646cc45ed2c33bfbeec4a - Sigstore transparency entry: 1694081529
- Sigstore integration time:
-
Permalink:
leiverkus/opencode-talk-bridge@f7c9e89f5550e3dd9d01a923ed834cd5b3a4b8d4 -
Branch / Tag:
refs/tags/v0.2.7 - Owner: https://github.com/leiverkus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f7c9e89f5550e3dd9d01a923ed834cd5b3a4b8d4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file opencode_talk_bridge-0.2.7-py3-none-any.whl.
File metadata
- Download URL: opencode_talk_bridge-0.2.7-py3-none-any.whl
- Upload date:
- Size: 49.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
895a83f6f919c2415b9df20ea408628113481b4531db39865917985c02d13d43
|
|
| MD5 |
3285c96263f88941d0df436a17959907
|
|
| BLAKE2b-256 |
553f31632347698e6eca85e4b54f96d6d2d3fbb1907d1990c00c7fc450af8744
|
Provenance
The following attestation bundles were made for opencode_talk_bridge-0.2.7-py3-none-any.whl:
Publisher:
publish.yml on leiverkus/opencode-talk-bridge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
opencode_talk_bridge-0.2.7-py3-none-any.whl -
Subject digest:
895a83f6f919c2415b9df20ea408628113481b4531db39865917985c02d13d43 - Sigstore transparency entry: 1694081562
- Sigstore integration time:
-
Permalink:
leiverkus/opencode-talk-bridge@f7c9e89f5550e3dd9d01a923ed834cd5b3a4b8d4 -
Branch / Tag:
refs/tags/v0.2.7 - Owner: https://github.com/leiverkus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f7c9e89f5550e3dd9d01a923ed834cd5b3a4b8d4 -
Trigger Event:
push
-
Statement type: