Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
81252fa
ops(tls): auto-ACME + dual-domain Caddy edge for domain migration
haowei2000 Jul 14, 2026
87fcdbf
feat(connector): signed opt-in self-update (ed25519 manifest, drain, …
haowei2000 Jul 14, 2026
9de7701
feat(connector): manual-update reminder when self-update is off
haowei2000 Jul 14, 2026
85b98a3
feat(protocol): golden fixtures, gateway frame constructors, version …
haowei2000 Jul 14, 2026
7032ae0
feat(protocol): extract cheers-bridge-protocol crate (connector mini-…
haowei2000 Jul 14, 2026
928fc07
fix(chat): stop hover toolbar overlapping own-message header
haowei2000 Jul 14, 2026
862b899
feat(protocol): gateway builds typed outbound frames; root-context do…
haowei2000 Jul 14, 2026
f8b2275
Merge pull request #184 from haowei2000/claude/message-bar-button-ove…
haowei2000 Jul 14, 2026
4ac0dd8
feat(protocol): typed control-stream inbound parse on the gateway
haowei2000 Jul 14, 2026
0846569
fix(cd): rebuild gateway image when the shared bridge-protocol crate …
haowei2000 Jul 14, 2026
00af541
fix(compose): pass connector release repo/version through to the gateway
haowei2000 Jul 14, 2026
bf5697e
Merge pull request #183 from haowei2000/ops/prod-domain-tocheers
haowei2000 Jul 14, 2026
1826a6c
Merge pull request #185 from haowei2000/feat/connector-auto-update
haowei2000 Jul 14, 2026
e9cc8af
fix(protocol): commit the data/* fixtures the unanchored gitignore sw…
haowei2000 Jul 14, 2026
8b1f058
Merge pull request #186 from haowei2000/feat/bridge-protocol
haowei2000 Jul 14, 2026
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
8 changes: 7 additions & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
filters: |
gateway:
- "server/**"
# The gateway image compiles the shared bridge-protocol crate
# (path dep) — a protocol change alone must rebuild it.
- "packages/cheers-acp-connector-rs/bridge-protocol/**"
- "docker-compose.yml.template"
- ".github/workflows/cd.yml"
frontend:
Expand Down Expand Up @@ -85,7 +88,10 @@ jobs:
id: build
uses: docker/build-push-action@v6
with:
context: ./server
# Repo-root context: the gateway build needs the shared
# bridge-protocol crate under packages/ (path dependency).
context: .
file: ./server/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ jobs:
filters: |
gateway:
- "server/**"
# The gateway's frame constructors are pinned to the shared
# bridge-protocol golden fixtures — a fixture edit must re-run
# gateway tests too.
- "packages/cheers-acp-connector-rs/bridge-protocol/**"
- ".github/workflows/ci.yml"
frontend:
- "frontend/**"
Expand Down Expand Up @@ -185,11 +189,13 @@ jobs:
cargo fmt --manifest-path packages/cheers-acp-connector-rs/Cargo.toml -- --check
cargo fmt --manifest-path packages/cheers-mcp-server/Cargo.toml -- --check

# --workspace: the connector dir is a 2-member mini-workspace (binary +
# the shared cheers-bridge-protocol crate the gateway also depends on).
- name: Check Rust connector daemon
run: cargo check --manifest-path packages/cheers-acp-connector-rs/Cargo.toml
run: cargo check --workspace --manifest-path packages/cheers-acp-connector-rs/Cargo.toml

- name: Test Rust connector daemon
run: cargo test --manifest-path packages/cheers-acp-connector-rs/Cargo.toml
run: cargo test --workspace --manifest-path packages/cheers-acp-connector-rs/Cargo.toml

- name: Check Rust MCP server
run: cargo check --manifest-path packages/cheers-mcp-server/Cargo.toml
62 changes: 62 additions & 0 deletions .github/workflows/release-connector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,65 @@ jobs:
# On tag pushes this creates/updates the release for the tag; each matrix
# job appends its binaries to the same release.
fail_on_unmatched_files: true

# Signed update manifest for the connector's opt-in self-updater ([update]
# auto = true). Runs after every binary is attached: downloads them back from
# the release, records their sha256 in connector-manifest.json, and signs the
# manifest bytes with the CONNECTOR_SIGNING_KEY ed25519 secret (the matching
# public key ships inside the connector at
# packages/cheers-acp-connector-rs/release-signing-pubkey.pem). A connector
# only swaps binaries whose hash appears in a manifest that verifies against
# that pinned key — a compromised gateway or GitHub proxy can't push code.
manifest:
name: sign update manifest
needs: build
if: startsWith(github.ref, 'refs/tags/connector-v')
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Download release assets
run: |
tag="${GITHUB_REF#refs/tags/}"
echo "TAG=$tag" >> "$GITHUB_ENV"
echo "VERSION=${tag#connector-v}" >> "$GITHUB_ENV"
mkdir assets
gh release download "$tag" --repo "$GITHUB_REPOSITORY" --dir assets \
--pattern 'cce-acp-connector-*' --pattern 'cheers-mcp-server-*'
ls -l assets

- name: Build + sign manifest
env:
CONNECTOR_SIGNING_KEY: ${{ secrets.CONNECTOR_SIGNING_KEY }}
run: |
if [ -z "$CONNECTOR_SIGNING_KEY" ]; then
echo "::error::CONNECTOR_SIGNING_KEY secret is not set — cannot publish a signed" \
"update manifest. Self-updating connectors will not pick up this release." \
"Add the ed25519 private key (PEM) as a repo secret and re-run this job."
exit 1
fi
python3 - << 'EOF'
import hashlib, json, os, pathlib
assets = {
p.name: {"sha256": hashlib.sha256(p.read_bytes()).hexdigest()}
for p in sorted(pathlib.Path("assets").iterdir())
}
manifest = {"version": os.environ["VERSION"], "assets": assets}
pathlib.Path("connector-manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n"
)
EOF
printf '%s' "$CONNECTOR_SIGNING_KEY" > signing-key.pem
openssl pkeyutl -sign -inkey signing-key.pem -rawin \
-in connector-manifest.json -out manifest.sig.bin
base64 -w0 manifest.sig.bin > connector-manifest.json.sig
rm -f signing-key.pem manifest.sig.bin
cat connector-manifest.json

- name: Attach manifest to release
uses: softprops/action-gh-release@v2
with:
files: |
connector-manifest.json
connector-manifest.json.sig
fail_on_unmatched_files: true
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
.env
.env.local
*.pem
# PUBLIC verification key (not a secret) — compiled into the connector so it can
# verify signed release manifests; the private half lives only in CI secrets.
!packages/cheers-acp-connector-rs/release-signing-pubkey.pem

# Python
__pycache__/
Expand All @@ -21,7 +24,8 @@ target/
*.rs.bk

# 运行时与数据(设计书约定排除)
data/
# 仅根目录的数据目录(不锚定会吞掉任意层级的 data/,例如 bridge-protocol/fixtures/data/)
/data/
tmp/
.tmp/
*.db
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ Required order when connector behavior materially changes:
1. Update `packages/cheers-acp-connector-rs/Cargo.toml` and `Cargo.lock` when the Rust connector version or dependencies change.
2. Run `cargo fmt --check`, `cargo test`, and `cargo check` for `packages/cheers-acp-connector-rs`.
3. Rebuild and push the `opencode-bot` image from the same merged commit so container deployments contain the new Rust connector and MCP server binaries.
4. Upgrade machines that run the connector locally by installing the Rust binary from the repo or the approved release artifact, then restart the corresponding connector daemon.
5. Upgrade container deployments by pulling or deploying the rebuilt `opencode-bot` image and recreating the service.
4. Upgrade machines that run the connector locally by installing the Rust binary from the repo or the approved release artifact, then restart the corresponding connector daemon. Hosts that opted into `[update] auto = true` pick the release up themselves once the gateway's `CHEERS_CONNECTOR_RELEASE_VERSION` is bumped — that only works if the tag's `manifest` CI job succeeded (it signs `connector-manifest.json` with the `CONNECTOR_SIGNING_KEY` repo secret; the matching public key is `packages/cheers-acp-connector-rs/release-signing-pubkey.pem`, compiled into the binary).
5. Upgrade container deployments by pulling or deploying the rebuilt `opencode-bot` image and recreating the service (containers never self-update).

Do not reintroduce the old npm connector package or the retired `@haowei0520/acp-connector` release workflow.

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ UI: frontend NodePort → <http://localhost:30080> (sign in `admin` / `admin1234

```bash
# First-time install: build images → load into kind → install the release
docker build -t cheers/gateway:dev server
docker build -t cheers/gateway:dev -f server/Dockerfile . # root context: server needs packages/.../bridge-protocol
docker build -t cheers/frontend:dev --build-arg VITE_API_BASE_URL=/api/v1 frontend
kind load docker-image cheers/gateway:dev cheers/frontend:dev --name cheers
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /tmp/jwt_priv.pem
Expand All @@ -71,7 +71,7 @@ helm upgrade --install cheers deploy/helm/cheers -n cheers --create-namespace \
```bash
# Redeploy after a code change: rebuild → reload into kind → roll the pod.
# Shortcut for all of the below: ./scripts/redeploy.sh [gateway|frontend|both]
docker build -t cheers/frontend:dev --build-arg VITE_API_BASE_URL=/api/v1 frontend # gateway: docker build -t cheers/gateway:dev server
docker build -t cheers/frontend:dev --build-arg VITE_API_BASE_URL=/api/v1 frontend # gateway: docker build -t cheers/gateway:dev -f server/Dockerfile .
kind load docker-image cheers/frontend:dev --name cheers
kubectl -n cheers rollout restart deployment/cheers-frontend # or deployment/cheers-gateway
kubectl -n cheers rollout status deployment/cheers-frontend
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ UI:前端 NodePort → <http://localhost:30080>(登录 `admin` / `admin12345

```bash
# 首次安装:构建镜像 → 加载进 kind → 安装 release
docker build -t cheers/gateway:dev server
docker build -t cheers/gateway:dev -f server/Dockerfile . # 根上下文:server 依赖 packages/.../bridge-protocol
docker build -t cheers/frontend:dev --build-arg VITE_API_BASE_URL=/api/v1 frontend
kind load docker-image cheers/gateway:dev cheers/frontend:dev --name cheers
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /tmp/jwt_priv.pem
Expand All @@ -68,7 +68,7 @@ helm upgrade --install cheers deploy/helm/cheers -n cheers --create-namespace \
```bash
# 代码变更后重新部署:重建 → 重新加载进 kind → 滚动重启 pod。
# 以下步骤的快捷方式:./scripts/redeploy.sh [gateway|frontend|both]
docker build -t cheers/frontend:dev --build-arg VITE_API_BASE_URL=/api/v1 frontend # gateway 用:docker build -t cheers/gateway:dev server
docker build -t cheers/frontend:dev --build-arg VITE_API_BASE_URL=/api/v1 frontend # gateway 用:docker build -t cheers/gateway:dev -f server/Dockerfile .
kind load docker-image cheers/frontend:dev --name cheers
kubectl -n cheers rollout restart deployment/cheers-frontend # 或 deployment/cheers-gateway
kubectl -n cheers rollout status deployment/cheers-frontend
Expand Down
2 changes: 1 addition & 1 deletion deploy/helm/cheers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ deploy/helm/cheers/
kind create cluster --name cheers --config deploy/kind-config.yaml

# 1) build the app images and load them into the cluster
docker build -t cheers/gateway:dev server
docker build -t cheers/gateway:dev -f server/Dockerfile .
docker build -t cheers/frontend:dev --build-arg VITE_API_BASE_URL=/api/v1 frontend
kind load docker-image cheers/gateway:dev cheers/frontend:dev --name cheers

Expand Down
19 changes: 15 additions & 4 deletions docker-compose.production.tls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,32 @@ services:
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=${RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS:-https://cheers.example.com}

tls-edge:
image: caddy:2-alpine
# Custom Caddy build with the Cloudflare DNS plugin, for ACME DNS-01.
build:
context: .
dockerfile: docker/Dockerfile.caddy
image: ${TLS_EDGE_IMAGE:-cheers/tls-edge:latest}
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
volumes:
- ./docker/Caddyfile:/etc/caddy/Caddyfile:ro
- ${TLS_CERT_DIR:-./certs}:/etc/caddy/certs:ro
# caddy_data persists the issued ACME certs across restarts — without it
# Caddy re-requests on every boot and will hit Let's Encrypt rate limits.
- caddy_data:/data
- caddy_config:/config
environment:
# Primary public host. APP_DOMAIN_LEGACY is served in parallel during a
# domain migration; leave it empty when there is only one domain.
- APP_DOMAIN=${APP_DOMAIN:-cheers.example.com}
- APP_DOMAIN_LEGACY=${APP_DOMAIN_LEGACY:-}
- STORAGE_S3_BUCKET=${STORAGE_S3_BUCKET:-cheers-files}
- TLS_CERT_FILE=${TLS_CERT_FILE:-/etc/caddy/certs/fullchain.pem}
- TLS_KEY_FILE=${TLS_KEY_FILE:-/etc/caddy/certs/privkey.pem}
# ACME account email (expiry notices) and the Cloudflare API token used
# for the DNS-01 challenge. The token needs Zone:DNS:Edit on the zone(s)
# covering APP_DOMAIN / APP_DOMAIN_LEGACY. Provide it out-of-band in .env.
- ACME_EMAIL=${ACME_EMAIL:-}
- CF_API_TOKEN=${CF_API_TOKEN:-}
depends_on:
- gateway
- frontend
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ services:
- TRUST_PROXY_HEADERS=${TRUST_PROXY_HEADERS:-true}
# 自助注册默认关闭(安全默认);公开实例显式设为 true。
- OPEN_REGISTRATION=${OPEN_REGISTRATION:-false}
# Connector self-update: the release version this gateway advertises in
# its hello + serves via /api/v1/connector/download. Unset ⇒ no update
# signal (download proxy then tracks the latest GitHub release).
- CHEERS_CONNECTOR_RELEASE_REPO=${CHEERS_CONNECTOR_RELEASE_REPO:-}
- CHEERS_CONNECTOR_RELEASE_VERSION=${CHEERS_CONNECTOR_RELEASE_VERSION:-}
# SMTP(可选)
- SMTP_HOST=${SMTP_HOST:-}
- SMTP_PORT=${SMTP_PORT:-587}
Expand Down
24 changes: 18 additions & 6 deletions docker/Caddyfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
{
admin off
email {$ACME_EMAIL}
log {
output stdout
format console
}
}

http://{$APP_DOMAIN} {
redir https://{$APP_DOMAIN}{uri} 301
}

{$APP_DOMAIN} {
tls {$TLS_CERT_FILE} {$TLS_KEY_FILE}
# Shared reverse-proxy rules, applied to every hostname we serve. Keeping them
# in a snippet lets the primary and legacy domains share one definition during
# a domain migration.
(cheers_app) {
encode gzip

handle /api/v1/* {
Expand Down Expand Up @@ -46,3 +45,16 @@ http://{$APP_DOMAIN} {
reverse_proxy frontend:80
}
}

# Primary host, plus an optional legacy host that stays live during a domain
# migration (set APP_DOMAIN_LEGACY empty once the old domain is retired). Caddy
# provisions and auto-renews a certificate for each hostname via the Cloudflare
# ACME DNS-01 challenge, which works while the domain stays behind Cloudflare's
# proxy. For a grey-cloud (proxy-off) deployment, delete the tls block so Caddy
# falls back to automatic HTTP-01 / TLS-ALPN-01 and use stock caddy:2-alpine.
{$APP_DOMAIN} {$APP_DOMAIN_LEGACY} {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
import cheers_app
}
20 changes: 20 additions & 0 deletions docker/Dockerfile.caddy
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Custom Caddy image with the Cloudflare DNS provider compiled in.
#
# Why: the production TLS edge obtains its certificate through the ACME
# DNS-01 challenge. DNS-01 is the only ACME method that still works while the
# domain sits behind Cloudflare's proxy ("orange cloud") — TLS-ALPN-01 fails
# because Cloudflare terminates TLS at its edge, and HTTP-01 is intercepted by
# "Always Use HTTPS". DNS-01 also renews unattended every ~60 days without
# ever toggling the cloud color. It needs the caddy-dns/cloudflare module,
# which is not part of the stock caddy:2-alpine image, so we build our own.
#
# If you instead run the domain grey-cloud (DNS-only / proxy off), you do not
# need this image at all: use stock `caddy:2-alpine` and drop the `tls { dns
# cloudflare ... }` block in the Caddyfile so Caddy falls back to automatic
# HTTP-01 / TLS-ALPN-01 issuance.
FROM caddy:2-builder-alpine AS builder
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare

FROM caddy:2-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
20 changes: 20 additions & 0 deletions docs/arch/CONNECTOR_TOML_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ Applies to the whole daemon process, not a single bot.
| `state_path` | string | `state.json` | Where the daemon persists runtime state (relative to the config file's directory). |
| `log_dir` | string | `~/.cheers/acp-connector/<name>/` | Where the daemon writes its `stdout`/`stderr` logs. When set, they become `<log_dir>/<name>.stdout.log` / `.stderr.log`. |

### `[update]` — opt-in self-update (optional)

Process-wide. When enabled and the gateway hello advertises a newer connector
release (`server_capabilities.latest_connector_version`, i.e. the gateway's
`CHEERS_CONNECTOR_RELEASE_VERSION`), the connector downloads that release's
`connector-manifest.json` + `.sig` through the gateway's download proxy, verifies
the manifest's **ed25519 signature** against the release key compiled into the
binary, verifies each binary's **sha256** against the manifest, waits until no
prompt turn is in flight, atomically swaps itself (and the sibling
`cheers-mcp-server`, which ships in lockstep) and re-execs with the same argv/PID.
The replaced binary is kept as `<exe>.old`; if the new binary fails to reach a
healthy bridge connection 3 boots in a row it is rolled back automatically and
that version is blocked from retry. Never active inside containers (update the
image instead); `CHEERS_ACP_NO_SELF_UPDATE=1` force-disables it everywhere.

| Key | Type | Default | Meaning |
|-------------------|--------|---------|---------|
| `auto` | bool | `false` | Opt in to automatic self-update. Off by default: updating executes code fetched over the network, so the host owner must enable it deliberately. Even when off, a newer advertised release is logged as a manual-update warning at startup and on connect, and shown by `cce-acp-connector status`. |
| `public_key_file` | string | embedded key | Path (relative to the config file) to an ed25519 SPKI PEM that overrides the release-signing public key compiled into the binary. Only needed by forks publishing their own signed releases. |

---

## Per-bot: `[accounts.<id>]`
Expand Down
17 changes: 17 additions & 0 deletions docs/arch/CONNECTOR_TOML_CONFIG.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ version = 1
| `state_path` | string | `state.json` | 守护进程持久化运行态的位置(相对配置文件所在目录)。 |
| `log_dir` | string | `~/.cheers/acp-connector/<name>/` | 守护进程写 `stdout`/`stderr` 日志的位置。设置后日志为 `<log_dir>/<name>.stdout.log` / `.stderr.log`。 |

### `[update]` — 自动更新(可选,需显式开启)

进程级配置。开启后,当网关 hello 通告了更新的连接器版本
(`server_capabilities.latest_connector_version`,即网关的
`CHEERS_CONNECTOR_RELEASE_VERSION`),连接器会通过网关的下载代理拉取该版本的
`connector-manifest.json` + `.sig`,用编译进二进制的发布公钥校验清单的
**ed25519 签名**,再逐个校验二进制的 **sha256**,等到没有进行中的对话轮次后,
原子替换自身(以及同目录配套发布的 `cheers-mcp-server`)并以相同 argv/PID
重新 exec。被替换的旧二进制保留为 `<exe>.old`;新二进制若连续 3 次启动都无法
建立健康的桥接连接,会自动回滚并拉黑该版本。容器内永不自更新(应更新镜像);
`CHEERS_ACP_NO_SELF_UPDATE=1` 可强制禁用。

| 键 | 类型 | 默认 | 含义 |
|-------------------|--------|------------|------|
| `auto` | bool | `false` | 是否开启自动更新。默认关闭:更新本质是执行从网络下载的代码,必须由宿主机所有者显式开启。即使关闭,发现新版本时也会在启动和连接时打印手动更新提醒,并在 `cce-acp-connector status` 中显示。 |
| `public_key_file` | string | 内置公钥 | ed25519 SPKI PEM 路径(相对配置文件),覆盖编译进二进制的发布签名公钥。仅自建签名发布的 fork 需要。 |

---

## 单个 bot:`[accounts.<id>]`
Expand Down
Loading
Loading