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
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,51 @@

## Unreleased

## v2.0.0 — 2026-08-21

Enterprise network lockdown for Chorus: keep the relay off the open internet with CIDR allowlists, private bind, and VPN/VPC deployment docs.

GitHub tag `v2.0.0` is the source of truth for this release. `@chorus/plugin` remains install-from-git (packages are private). Rebuild `chorus-relay` from this tag — older binaries ignore the new flags.

### Network access restrictions

- **CIDR / IP allowlist** on `chorus-relay` (`--allow-cidr` / `relay.allowedCidrs` / `CHORUS_ALLOWED_CIDRS`) — peers outside the list get HTTP 403 before auth on `/ws`, `/host`, and `/status`
- **Explicit deny CIDRs** (`--deny-cidr` / `relay.deniedCidrs` / `CHORUS_DENIED_CIDRS`) — deny wins over allow
- **Source-port allowlist** (`--allow-port` / `relay.allowedPorts` / `CHORUS_ALLOWED_PORTS`) — for single-machine e2e and tight lockdowns (`bun run test:network-e2e`)
- **Bind policy** — `relay.bind` / `CHORUS_BIND`; `relay.allowOpenBind: false` refuses `0.0.0.0` / `::`
- Loopback IPs remain admitted by default when an allow-CIDR list is set (`allowLoopback`); source-port rules still apply
- `/status` reports `{ network: { allowlist, denylist, allowedPorts, restricted } }`
- Docs: [docs/NETWORK.md](docs/NETWORK.md) (corporate VPN, Tailscale, AWS VPC, Azure VNet, GCP VPC)

### Config & env (new)

| Control | Default | Notes |
|---|---|---|
| `relay.bind` / `CHORUS_BIND` | `0.0.0.0` | Listen address |
| `relay.allowedCidrs` / `CHORUS_ALLOWED_CIDRS` | `[]` | Empty = unrestricted |
| `relay.deniedCidrs` / `CHORUS_DENIED_CIDRS` | `[]` | Deny wins over allow |
| `relay.allowedPorts` / `CHORUS_ALLOWED_PORTS` | `[]` | Peer source-port allowlist |
| `relay.allowOpenBind` / `CHORUS_ALLOW_OPEN_BIND` | `true` | Set `false` for enterprise MDM |
| `relay.allowLoopback` / `CHORUS_ALLOW_LOOPBACK` | `true` | Admit loopback IP when allowlisted |

### Upgrade notes

- Defaults are unchanged: LAN shares without an allowlist behave like v1.0.0.
- Hosts must run a `chorus-relay` built from this release (or later) for allowlist/bind flags to take effect.
- If `allowOpenBind` is `false` and `bind` is still open (`0.0.0.0` / `::`), `/chorus-share` fails with a clear error — set a private `bind` address.

Example enterprise floor:

```json
{
"relay": {
"allowOpenBind": false,
"bind": "10.0.12.4",
"allowedCidrs": ["10.0.0.0/8", "100.64.0.0/10"]
}
}
```

## v1.0.0 — 2026-08-20

First stable release of Chorus: **OpenCode↔OpenCode pair programming on one live AI session**. The host shares a session; collaborators join over a LAN WebSocket relay and send prompts into the same LLM turn. Side-channel chat stays in toasts; the shared transcript is mirrored into each joiner’s OpenCode session.
Expand Down
10 changes: 9 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,15 @@ Copy [chorus.example.json](./chorus.example.json) to get started. Example enterp
"repoRemoteRewrites": [{ "from": "github.acme.com", "to": "github.com" }],
"defaultRole": "edit",
"tokenTtlMs": 86400000
},
"relay": {
"allowOpenBind": false,
"allowedCidrs": ["10.0.0.0/8", "100.64.0.0/10"]
}
}
```

These knobs are pairing controls, not a complete enterprise ACL: email and git remote are **self-asserted** on join, and later config layers (user/project) can still override `/etc/chorus/config.json`. See [docs/ENTERPRISE.md](docs/ENTERPRISE.md) for the gaps a security review will hit.
These knobs are pairing controls, not a complete enterprise ACL: email and git remote are **self-asserted** on join, and later config layers (user/project) can still override `/etc/chorus/config.json`. See [docs/ENTERPRISE.md](docs/ENTERPRISE.md) for the gaps a security review will hit. For VPN / AWS VPC / Azure VNet / GCP VPC patterns, see [docs/NETWORK.md](docs/NETWORK.md).

| Field | Default | Description |
|---|---|---|
Expand All @@ -153,6 +157,12 @@ These knobs are pairing controls, not a complete enterprise ACL: email and git r
| `security.defaultRole` | `edit` | Role when `/chorus-share` omits role |
| `security.tokenTtlMs` | — | Optional join-token TTL |
| `relay.port` / `relay.publicHost` | — | Relay listen / advertised join host |
| `relay.bind` | `0.0.0.0` | Listen address (`CHORUS_BIND`) |
| `relay.allowedCidrs` | `[]` | CIDR/IP allowlist; empty = unrestricted |
| `relay.deniedCidrs` | `[]` | Explicit deny CIDRs (deny wins) |
| `relay.allowedPorts` | `[]` | Peer source-port allowlist (single-machine e2e) |
| `relay.allowOpenBind` | `true` | If `false`, refuse bind to `0.0.0.0` / `::` |
| `relay.allowLoopback` | `true` | Admit loopback IP when an allow-CIDR list is set |
| `backup.bucket` / `region` / `endpoint` | — | Optional S3/R2 backup |
| `org.name` / `org.policyNote` | — | Shown in share + status |

Expand All @@ -166,6 +176,12 @@ These knobs are pairing controls, not a complete enterprise ACL: email and git r
| `CHORUS_SYSTEM_CONFIG` | `/etc/chorus/config.json` | Org-wide config path |
| `CHORUS_USER_CONFIG` | `~/.config/chorus/config.json` | Per-user config path override |
| `CHORUS_PORT` | `7742` | Relay listen port (overrides file) |
| `CHORUS_BIND` | `0.0.0.0` | Relay bind address |
| `CHORUS_ALLOWED_CIDRS` | — | Comma-separated CIDR/IP allowlist |
| `CHORUS_DENIED_CIDRS` | — | Comma-separated CIDR/IP denylist (deny wins) |
| `CHORUS_ALLOWED_PORTS` | — | Comma-separated peer source ports |
| `CHORUS_ALLOW_OPEN_BIND` | `true` | Set `false` to refuse `0.0.0.0` / `::` |
| `CHORUS_ALLOW_LOOPBACK` | `true` | Admit loopback when allowlist is set |
| `CHORUS_RELAY_BIN` | auto-detect | Path to `chorus-relay` binary |
| `CHORUS_HOST_TOKEN` | random | Host control secret (set by plugin when spawning) |
| `CHORUS_RELAY_HOST` | `127.0.0.1` | Relay host to attach to (e.g. `host.docker.internal:7742`) |
Expand All @@ -184,6 +200,7 @@ These knobs are pairing controls, not a complete enterprise ACL: email and git r
| Host approval | Config `security.requireApproval` (default off); when on, joiners appear in a live numbered queue — `/chorus-approve 1` (full userId still works) |
| Git repo gate | If the host share directory has `origin` (or `requireRepoMatch`), joiners must present the same remote. Extra prefixes/rewrites come from `additionalRepoRemotePrefixes` / `repoRemoteRewrites`. |
| Company email gate | If `allowedEmailDomain` is set (or `requireEmailDomainMatch`), joiners must auth with an email at that domain |
| Network allowlist | If `relay.allowedCidrs` is set, only those TCP peers reach `/ws` / `/host` (see [docs/NETWORK.md](docs/NETWORK.md)) |
| Kick | `/chorus-kick <userId>` disconnects an active joiner |

Remote tunneling (`bore` / `cloudflared`) is not implemented yet — share a LAN IP + port for now.
Expand All @@ -193,6 +210,7 @@ Remote tunneling (`bore` / `cloudflared`) is not implemented yet — share a LAN
- [docs/STATUS.md](docs/STATUS.md) — what’s done and what still needs to happen
- [docs/DECISIONS.md](docs/DECISIONS.md) — OpenCode plugin vs from-scratch, languages, license
- [docs/ENTERPRISE.md](docs/ENTERPRISE.md) — security gaps for enterprise use (claims vs proofs, policy floor, SSO, audit)
- [docs/NETWORK.md](docs/NETWORK.md) — CIDR allowlist, VPN, AWS VPC / Azure VNet / GCP VPC
- [docs/THIRD_PARTY_LICENSES.md](docs/THIRD_PARTY_LICENSES.md) — third-party license audit for commercial use

## License
Expand Down
10 changes: 8 additions & 2 deletions chorus.example.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"org": {
"name": "Example Engineering",
"policyNote": "Pair sessions require host approval and a matching git clone."
"policyNote": "Pair sessions require host approval, a matching git clone, and VPN/VPC network access."
},
"security": {
"requireApproval": true,
Expand All @@ -15,7 +15,13 @@
"tokenTtlMs": 86400000
},
"relay": {
"port": 7742
"port": 7742,
"allowOpenBind": false,
"bind": "10.0.12.4",
"allowedCidrs": ["10.0.0.0/8", "100.64.0.0/10"],
"deniedCidrs": ["203.0.113.0/24"],
"allowedPorts": [],
"allowLoopback": true
},
"backup": {
"region": "us-east-1"
Expand Down
4 changes: 3 additions & 1 deletion crates/chorus-relay/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "chorus-relay"
version = "0.2.0"
version = "2.0.0"
edition.workspace = true
license.workspace = true
authors.workspace = true
Expand Down Expand Up @@ -28,6 +28,8 @@ clap = { version = "4", features = ["derive", "env"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tower-http = { version = "0.6", features = ["trace"] }
ipnet = "2.12.1"
http = "1.5.0"

[dev-dependencies]
tokio-tungstenite = "0.26"
Expand Down
1 change: 1 addition & 0 deletions crates/chorus-relay/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Library surface for integration tests.
pub mod access;
pub mod netallow;
pub mod protocol;
pub mod server;
pub mod state;
27 changes: 27 additions & 0 deletions crates/chorus-relay/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ struct Args {
/// Shared secret for the host control channel. Generated if omitted.
#[arg(long, env = "CHORUS_HOST_TOKEN")]
host_token: Option<String>,

/// CIDR or IP allowlist (repeat or comma-separated). Empty = no IP allow restriction.
#[arg(long = "allow-cidr", env = "CHORUS_ALLOWED_CIDRS", value_delimiter = ',')]
allowed_cidrs: Vec<String>,

/// CIDR or IP denylist (repeat or comma-separated). Deny wins over allow.
#[arg(long = "deny-cidr", env = "CHORUS_DENIED_CIDRS", value_delimiter = ',')]
denied_cidrs: Vec<String>,

/// Peer source-port allowlist (repeat or comma-separated). Empty = any source port.
/// Useful for single-machine e2e where all peers are 127.0.0.1.
#[arg(long = "allow-port", env = "CHORUS_ALLOWED_PORTS", value_delimiter = ',')]
allowed_ports: Vec<u16>,

/// Allow binding to 0.0.0.0 / :: (open all interfaces). Disable for enterprise.
#[arg(long, env = "CHORUS_ALLOW_OPEN_BIND", default_value_t = true, action = clap::ArgAction::Set)]
allow_open_bind: bool,

/// When an allowlist is set, still admit loopback IPs (host plugin on same machine).
/// Source-port allowlists still apply to loopback peers.
#[arg(long, env = "CHORUS_ALLOW_LOOPBACK", default_value_t = true, action = clap::ArgAction::Set)]
allow_loopback: bool,
}

fn random_hex(bytes: usize) -> String {
Expand Down Expand Up @@ -44,6 +66,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
port: args.port,
host_token,
bind: args.bind,
allowed_cidrs: args.allowed_cidrs,
denied_cidrs: args.denied_cidrs,
allowed_ports: args.allowed_ports,
allow_open_bind: args.allow_open_bind,
allow_loopback: args.allow_loopback,
})
.await
}
Loading