Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ ARG OPENCODE_VERSION=2.1.3
ARG OPENCHAMBER_VERSION=2.5.0
ARG TYPESCRIPT_VERSION=7.0.2
ARG PNPM_VERSION=12.15.0
ARG CLAUDE_CODE_VERSION=2.1.309
ARG TARGETARCH
ENV PNPM_HOME=/opt/pnpm
ENV PATH=${PNPM_HOME}/bin:${PATH}
Expand All @@ -42,10 +43,12 @@ RUN case "$TARGETARCH" in amd64) arch=x64 ;; arm64) arch=arm64 ;; *) exit 1 ;; e
"https://registry.npmjs.org/@pnpm/exe.linux-${arch}/-/exe.linux-${arch}-${PNPM_VERSION}.tgz" \
| tar -xz --strip-components=1 -C /opt/pnpm-cli \
&& ln -s /opt/pnpm-cli/pnpm /usr/local/bin/pnpm \
&& XDG_CONFIG_HOME=/opt/pnpm-config pnpm --allow-build=@opencode/cli --allow-build=node-pty add --global \
&& XDG_CONFIG_HOME=/opt/pnpm-config pnpm --allow-build=@opencode/cli --allow-build=node-pty --allow-build=@anthropic-ai/claude-code add --global \
"@opencode/cli@${OPENCODE_VERSION}" "@openchamber/web@${OPENCHAMBER_VERSION}" "typescript@${TYPESCRIPT_VERSION}" \
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
&& opencode --version \
&& openchamber --version
&& openchamber --version \
&& claude --version

FROM tools AS config
WORKDIR /build
Expand All @@ -66,11 +69,13 @@ ENV HOME=/data \
XDG_CACHE_HOME=/data/.cache \
OPENCODE_CONFIG=/etc/opencode/opencode.json \
OPENCODE_CLIENT=app \
DISABLE_AUTOUPDATER=1 \
OPENCHAMBER_CHATS_DIR=/data/workspace/.chats

COPY --from=config /build/runtime.json /etc/opencode/opencode.json
COPY AGENTS.md /etc/opencode/AGENTS.md
COPY config/quota-toast.jsonc /etc/opencode/quota-toast.jsonc
COPY config/openchamber-policy.json /etc/openchamber/policy.json
COPY skills /etc/opencode/skills
COPY --chmod=755 entrypoint.sh healthcheck.sh openchamber-healthcheck.sh /usr/local/bin/
RUN mkdir -p /data/workspace /data/.config/opencode "$PNPM_HOME" \
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ docker compose exec opencode opencode auth login github-copilot --server http://
docker compose exec opencode opencode auth login google --server http://127.0.0.1:4096
docker compose exec opencode opencode auth login anthropic --method key --server http://127.0.0.1:4096
docker compose exec opencode opencode auth login openai --server http://127.0.0.1:4096
docker compose exec opencode claude auth login --claudeai
```

- **GitHub Copilot:** complete device login with a subscribed account.
- **Gemini:** choose the API-key method or set `GOOGLE_GENERATIVE_AI_API_KEY`. Organization-backed Code Assist OAuth also needs `OPENCODE_GEMINI_PROJECT_ID`; see the [Gemini plugin's account requirements](https://github.com/jenslys/opencode-gemini-auth).
- **Anthropic:** enter a [Console API key](https://console.anthropic.com/settings/keys) or set `ANTHROPIC_API_KEY`. Claude Pro and Max subscriptions do not work outside Claude Code.
- **Anthropic:** enter a [Console API key](https://console.anthropic.com/settings/keys) or set `ANTHROPIC_API_KEY`. For a Claude Pro or Max plan, use the Claude Code provider below instead.
- **OpenAI:** choose the API-key method with a [platform key](https://platform.openai.com/api-keys) or set `OPENAI_API_KEY`.
- **Claude Code:** sign in with `claude auth login --claudeai` to use a Claude Pro or Max plan through [opencode-claude](https://github.com/openchamber/opencode-claude); the login persists in `/data/.claude`. Plan usage only, not Console API keys.
- **Git access:** set `GH_TOKEN` or run `docker compose exec opencode gh auth login --hostname github.com --git-protocol https --web`. This is separate from Copilot login; stored credentials persist in `/data/.config/gh`.
- **WakaTime:** put your API key in `/data/.wakatime.cfg`, owned by `1000:1000` with mode `600`, using the [plugin's configuration format](https://github.com/angristan/opencode-wakatime#prerequisites).

Expand All @@ -66,6 +68,7 @@ Set the variables below before creating the container. For remote servers, open
| Integration | Setup |
| --- | --- |
| [Context7](https://mcp.context7.com/mcp) | Remote library documentation. |
| [Gmail](https://developers.google.com/workspace/gmail/api/guides/configure-mcp-server) | Remote server at `https://gmailmcp.googleapis.com/mcp/v1` (Developer Preview). Create a Web application OAuth client with redirect URI `http://127.0.0.1:19876/callback` and set `GMAIL_MCP_CLIENT_ID` and `GMAIL_MCP_CLIENT_SECRET`; read and draft only, no send. The callback listener is on the container loopback, so after consent copy the failed `127.0.0.1:19876` URL from the browser and run `docker compose exec opencode curl -fsS "<url>"`. |
| [Grafana](https://github.com/grafana/mcp-grafana) | `GRAFANA_URL` and `GRAFANA_SERVICE_ACCOUNT_TOKEN`; bundled tools are read-only. |
| [Karakeep](https://docs.karakeep.app/integrations/mcp) | `KARAKEEP_API_KEY` and `KARAKEEP_API_ADDR` (defaults to `https://hoard.natwelch.com`); includes write tools. |
| [Lunch Money](https://github.com/akutishevsky/lunchmoney-mcp) | `LUNCHMONEY_API_TOKEN` from [developer settings](https://my.lunchmoney.app/developers); required to start its MCP server. |
Expand All @@ -82,7 +85,7 @@ Grafana and Karakeep can connect before their credentials are validated; check a

## Agents and configuration

- **Build** is the default agent and can use all configured tools. **General** can implement and test; **Explore** is read-only. Both subagents may use Context7 but deny Grafana, Lunch Money, Karakeep, Notion, Robinhood, and further delegation. Build and General ask before `rm` commands whose text contains `.config`, `.local`, `HOME`, `~`, or `/data/.`, or ends in `/data`. Tool permissions do not sandbox shell access.
- **Build** is the default agent and can use all configured tools. **General** can implement and test; **Explore** is read-only. Both subagents may use Context7 but deny Gmail, Grafana, Lunch Money, Karakeep, Notion, Robinhood, and further delegation. Build and General ask before `rm` commands whose text contains `.config`, `.local`, `HOME`, `~`, or `/data/.`, or ends in `/data`. Tool permissions do not sandbox shell access.
- [Superpowers](https://github.com/obra/superpowers) supplies planning, debugging, testing, and review skills. The image also ships [`commit-push-pr` and `go-repo`](skills/). [`AGENTS.md`](AGENTS.md) requires repository edits in `/data/workspace/.worktrees/<repository>/<task>`.
- [OpenCode Quota](https://github.com/slkiser/opencode-quota) provides `/quota` and `/quota_status`.
- Defaults and pinned plugins live in [`opencode.jsonc`](opencode.jsonc). The image converts it to `/etc/opencode/opencode.json`, which overrides matching settings in global and project `opencode.json(c)` files. See the [V2 configuration guide](https://opencode.ai/v2/docs/config).
Expand All @@ -103,7 +106,7 @@ code.example.com {
}
```

`/health` is unauthenticated and reports diagnostics, so keep it off the internet. OpenChamber runs in [enterprise mode](https://docs.openchamber.dev/security/#enterprise-mode), which disables the relay and untrusted extensions. Login is rate limited; add a passkey under Settings after first sign-in.
`/health` is unauthenticated and reports diagnostics, so keep it off the internet. OpenChamber runs in [enterprise mode](https://docs.openchamber.dev/security/#enterprise-mode), which disables the relay and untrusted extensions. [Resource Monitor](https://github.com/openchamber/openchamber-resource-monitor) is allowed in `/etc/openchamber/policy.json`; install it once from Settings, Extensions with that URL. Login is rate limited; add a passkey under Settings after first sign-in.

The container runs as UID 1000 with dropped capabilities and resource limits. Login grants code execution and access to stored credentials; this is a trusted-user service, not a multi-tenant sandbox.

Expand Down
2 changes: 2 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ services:
GRAFANA_SERVICE_ACCOUNT_TOKEN: ${GRAFANA_SERVICE_ACCOUNT_TOKEN:-}
KARAKEEP_API_ADDR: ${KARAKEEP_API_ADDR:-}
KARAKEEP_API_KEY: ${KARAKEEP_API_KEY:-}
GMAIL_MCP_CLIENT_ID: ${GMAIL_MCP_CLIENT_ID:-}
GMAIL_MCP_CLIENT_SECRET: ${GMAIL_MCP_CLIENT_SECRET:-}
OPENCODE_PASSWORD: ${OPENCODE_PASSWORD:?Set OPENCODE_PASSWORD}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
Expand Down
5 changes: 5 additions & 0 deletions config/openchamber-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"allowedExtensions": [
"https://github.com/openchamber/openchamber-resource-monitor"
]
}
18 changes: 18 additions & 0 deletions opencode.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
{ "action": "grafana_*", "resource": "*", "effect": "deny" },
{ "action": "lunchmoney_*", "resource": "*", "effect": "deny" },
{ "action": "karakeep_*", "resource": "*", "effect": "deny" },
{ "action": "gmail_*", "resource": "*", "effect": "deny" },
{ "action": "notion_*", "resource": "*", "effect": "deny" },
{ "action": "robinhood_*", "resource": "*", "effect": "deny" },
{ "action": "subagent", "resource": "*", "effect": "deny" },
Expand All @@ -50,6 +51,7 @@
{ "action": "grafana_*", "resource": "*", "effect": "deny" },
{ "action": "lunchmoney_*", "resource": "*", "effect": "deny" },
{ "action": "karakeep_*", "resource": "*", "effect": "deny" },
{ "action": "gmail_*", "resource": "*", "effect": "deny" },
{ "action": "notion_*", "resource": "*", "effect": "deny" },
{ "action": "robinhood_*", "resource": "*", "effect": "deny" },
{ "action": "subagent", "resource": "*", "effect": "deny" },
Expand All @@ -71,6 +73,16 @@
"type": "remote",
"url": "https://mcp.notion.com/mcp"
},
"gmail": {
"type": "remote",
"url": "https://gmailmcp.googleapis.com/mcp/v1",
"oauth": {
"client_id": "{env:GMAIL_MCP_CLIENT_ID}",
"client_secret": "{env:GMAIL_MCP_CLIENT_SECRET}",
"redirect_uri": "http://127.0.0.1:19876/callback",
"scope": "https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.compose"
}
},
"robinhood": {
"type": "remote",
"url": "https://agent.robinhood.com/mcp/trading"
Expand Down Expand Up @@ -127,6 +139,7 @@
"opencode-wakatime@1.4.0",
"superpowers@git+https://github.com/obra/superpowers.git#2ce8aa15d05bd57213e6f746f953aafb7ded7a8b",
"@slkiser/opencode-quota@5.0.3",
"@openchamber/opencode-claude@1.5.0",
{
"package": "opencode-runtime-fallback@0.3.0",
"options": {
Expand Down Expand Up @@ -173,6 +186,11 @@
"action": "provider.use",
"resource": "openai",
"effect": "allow"
},
{
"action": "provider.use",
"resource": "claude-code",
"effect": "allow"
}
]
}
9 changes: 9 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
minimumReleaseAgeStrict: true
minimumReleaseAgeExclude:
- '@anthropic-ai/claude-code-darwin-arm64@2.1.309'
- '@anthropic-ai/claude-code-darwin-x64@2.1.309'
- '@anthropic-ai/claude-code-linux-arm64-musl@2.1.309'
- '@anthropic-ai/claude-code-linux-arm64@2.1.309'
- '@anthropic-ai/claude-code-linux-x64-musl@2.1.309'
- '@anthropic-ai/claude-code-linux-x64@2.1.309'
- '@anthropic-ai/claude-code-win32-arm64@2.1.309'
- '@anthropic-ai/claude-code-win32-x64@2.1.309'
- '@anthropic-ai/claude-code@2.1.309'
- '@openchamber/sdk@2.5.0'
- '@openchamber/web@2.5.0'
- '@opencode/ai@2.1.3'
Expand Down
4 changes: 2 additions & 2 deletions scripts/test-router-integration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,13 @@ try {
for (const name of external ? ["lunchmoney", "grafana", "karakeep"] : ["lunchmoney", "karakeep"]) {
assert.equal(data.find(server => server.name === name)?.status.status, "connected", JSON.stringify(data));
}
for (const name of ["context7", "notion"])
for (const name of ["context7", "gmail", "notion"])
assert.ok(data.some(server => server.name === name), `missing ${name}`);
return true;
});
// Without credentials, a dropped provider policy shows up as OpenCode's free models.
const models = (await request("/api/model")).data;
assert.ok(models.every(m => ["google", "github-copilot", "anthropic", "openai"].includes(m.providerID)), `policies hide other providers' models: ${models.map(m => m.providerID)}`);
assert.ok(models.every(m => ["google", "github-copilot", "anthropic", "openai", "claude-code"].includes(m.providerID)), `policies hide other providers' models: ${models.map(m => m.providerID)}`);
session = (await request("/api/session", { title: "V2 integration test", agent: "build",
location: { directory } })).data;
await request(`/api/session/${session.id}/command`, { name: "quota_announcements", text: "" });
Expand Down
4 changes: 2 additions & 2 deletions tests/routing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ test("image-owned skills are discoverable", async () => {
});

test("existing providers, MCP integrations and quota are preserved", () => {
assert.deepEqual(Object.keys(config.mcp.servers).sort(), ["context7", "grafana", "karakeep", "lunchmoney", "notion", "robinhood"]);
assert.deepEqual(Object.keys(config.mcp.servers).sort(), ["context7", "gmail", "grafana", "karakeep", "lunchmoney", "notion", "robinhood"]);
assert.ok(config.plugins.includes("@slkiser/opencode-quota@5.0.3"));
const allowed = config.policies.filter(p => p.action === "provider.use" && p.effect === "allow").map(p => p.resource);
assert.deepEqual(allowed.sort(), ["anthropic", "github-copilot", "google", "openai"]);
assert.deepEqual(allowed.sort(), ["anthropic", "claude-code", "github-copilot", "google", "openai"]);
Comment thread
icco marked this conversation as resolved.
assert.ok(!config.permissions?.some(rule => rule.action === "*" && rule.effect === "allow"));
assert.deepEqual(config.agents.build.permissions[0], { action: "*", resource: "*", effect: "allow" });
for (const name of ["general", "explore"])
Expand Down
Loading