Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dfb1781
docs(bcs): add bridge-provider design spec (BCN Provider 2.0 x cfuse …
vzvince Aug 31, 2026
3e68d5e
docs(bcs): bridge-provider implementation plan (16 tasks, TDD)
vzvince Aug 31, 2026
5292903
feat(bridge-provider): crate scaffold + provider config
vzvince Aug 31, 2026
6dc1c65
feat(bridge-provider): SSE frame encoder with 8MiB guard
vzvince Aug 31, 2026
7e06c29
feat(bridge-provider): StreamEvent to Provider 2.0 wire mapping
vzvince Aug 31, 2026
85ca094
test(bridge-provider): cover forbidden-event rejection in wire mapping
vzvince Aug 31, 2026
92cd6ab
feat(bridge-provider): webhook skeleton with auth chain + bot.ping
vzvince Aug 31, 2026
7d85b20
fix(bridge-provider): drop dead imports, add run_terminated error
vzvince Aug 31, 2026
4eb2b1b
feat(bridge-provider): idempotency ledger
vzvince Aug 31, 2026
5dcbd68
feat(bridge-provider): session store with dual-id mapping
vzvince Aug 31, 2026
95145ff
feat(bridge-provider): CliSession subprocess plumbing
vzvince Aug 31, 2026
108d9e4
feat(bridge-provider): Engine trait and turn types
vzvince Aug 31, 2026
f41aa36
feat(bridge-provider): CfuseCc driver mapping claude stream-json
vzvince Aug 31, 2026
02916ce
feat(bridge-provider): CfuseCodex driver mapping codex SSE
vzvince Aug 31, 2026
34aba20
docs(bcs): amend plan T10/T13 — codex CLI emits JSONL not SSE (probe-…
vzvince Aug 31, 2026
475af69
fix(bridge-provider): realign CfuseCodex to codex exec JSONL
vzvince Aug 31, 2026
329d79e
feat(bridge-provider): run loop and chat.send SSE end-to-end
vzvince Aug 31, 2026
b974318
feat(bridge-provider): HITL interaction bridging via control channel
vzvince Aug 31, 2026
f938f71
feat(bridge-provider): chat.inject with engine transcript sink
vzvince Aug 31, 2026
741cb27
test(bridge-provider): cover cc sink branch and inject prefix assertions
vzvince Aug 31, 2026
4bae5a1
feat(bridge-provider): chat.abort with terminal-state matrix
vzvince Aug 31, 2026
a320189
fix(bridge-provider): close chat.send/abort startup TOCTOU
vzvince Aug 31, 2026
270d6db
feat(bridge-provider): binary entrypoint with graceful shutdown
vzvince Aug 31, 2026
3c49f37
test(bridge-provider): protocol regression e2e
vzvince Aug 31, 2026
fc9624f
test(bridge-provider): pin buffered replay on re-attach
vzvince Aug 31, 2026
aad9300
fix(bridge-provider): final review fixes (exec command shape, subscri…
vzvince Aug 31, 2026
9722f40
fix(bridge-provider): stream Codex through app-server
vzvince Sep 4, 2026
3be5e74
feat(bridge-provider): expose engine event traces
vzvince Sep 5, 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
428 changes: 428 additions & 0 deletions scripts/bcn_sim.py

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions scripts/engine-tee.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# engine-tee.sh — cfuse 引擎采集包装器(开发调试用,不进生产)
#
# bridge 配置里的 cfuse_bin 指向本脚本。本脚本把三方数据全部落盘后再转给
# 真正的引擎(REAL_ENGINE 指定,mock fixture 或真 cfuse),逐行无缓冲透传:
#
# bridge→引擎 stdin $ENGINE_LOG_DIR/engine.stdin.jsonl (bridge 写给引擎的行)
# 引擎→bridge stdout $ENGINE_LOG_DIR/engine.stdout.ndjson (cfuse 原始事件,一行一个)
# 引擎 stderr $ENGINE_LOG_DIR/engine.stderr.log
# 每次引擎启动 $ENGINE_LOG_DIR/runs.log (时间/pid/args)
#
# 用法(bind 会通过环境变量自带):
# REAL_ENGINE=/path/to/mock_cc.sh ENGINE_LOG_DIR=/tmp/bridge-dev \
# ./scripts/engine-tee.sh --cc --output-format stream-json ...
set -o pipefail

DIR="${ENGINE_LOG_DIR:-/tmp/bridge-dev}"
REAL="${REAL_ENGINE:?REAL_ENGINE not set — point it at the engine binary/script}"
mkdir -p "$DIR"

printf '[%s] engine pid=%s args=%s\n' "$(date '+%F %T')" "$$" "$*" >> "$DIR/runs.log"

tee -a "$DIR/engine.stdin.jsonl" \
| python3 -c '
import fcntl
import os
import sys

for size in (1024 * 1024, 64 * 1024, 16 * 1024, 8 * 1024):
try:
actual = fcntl.fcntl(1, fcntl.F_SETPIPE_SZ, size)
except (AttributeError, OSError):
continue
if actual >= 8 * 1024:
break

os.execvp(sys.argv[1], sys.argv[1:])
' "$REAL" "$@" 2> >(tee -a "$DIR/engine.stderr.log" >&2) \
| tee -a "$DIR/engine.stdout.ndjson"
exit $?
24 changes: 24 additions & 0 deletions src/bcs/Cargo.lock

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

2 changes: 2 additions & 0 deletions src/bcs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"crates/adapters/http/bcs-http",
"crates/adapters/http/bcs-provider-http",
"crates/adapters/ws/bcs-ws",
"crates/adapters/bridge-provider",
# service-api
"crates/service-api/bcs-config-api",
"crates/service-api/bcs-service-api",
Expand Down Expand Up @@ -124,6 +125,7 @@ version = "0.1.0"
# Async runtime
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }
tokio-stream = "0.1"
tokio-tungstenite = { version = "0.26", features = ["native-tls"] }

# HTTP server
Expand Down
33 changes: 33 additions & 0 deletions src/bcs/crates/adapters/bridge-provider/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "bridge-provider"
description = "BCN Provider 2.0 bridge to local coding engines (cfuse cc/codex)"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true

[lints]
workspace = true

[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
axum = { workspace = true }
bcs-protocol = { workspace = true }
libc = "0.2"
futures = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tokio-util = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
uuid = { workspace = true }

[dev-dependencies]
reqwest = { workspace = true }
tempfile = { workspace = true }
98 changes: 98 additions & 0 deletions src/bcs/crates/adapters/bridge-provider/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::{net::SocketAddr, path::{Path, PathBuf}};
use serde::Deserialize;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum EngineKind { CfuseCc, CfuseCodex }

#[derive(Debug, Clone, Deserialize)]
pub struct BotConfig {
pub provider_bot_ref: String,
pub engine: EngineKind,
pub model: Option<String>,
pub cwd: PathBuf,
pub permission_mode: Option<String>,
pub cfuse_bin: Option<PathBuf>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ProviderConfig {
pub provider_id: String,
pub listen: SocketAddr,
pub bcs_to_provider_token: String,
pub bot_runtime_token: Option<String>,
#[serde(default)]
pub trace_dir: Option<PathBuf>,
#[serde(rename = "bot")]
pub bots: Vec<BotConfig>,
}

#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("read config: {0}")]
Read(#[from] std::io::Error),
#[error("parse config: {0}")]
Parse(#[from] toml::de::Error),
}

impl ProviderConfig {
pub fn load(path: &Path) -> Result<Self, ConfigError> {
let text = std::fs::read_to_string(path)?;
Ok(toml::from_str(&text)?)
}
pub fn bot(&self, provider_bot_ref: &str) -> Option<&BotConfig> {
self.bots.iter().find(|b| b.provider_bot_ref == provider_bot_ref)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn loads_provider_config_and_finds_bot() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bridge.toml");
std::fs::write(
&path,
r#"
provider_id = "bridge-1"
listen = "127.0.0.1:21100"
bcs_to_provider_token = "tok-b2p"

[[bot]]
provider_bot_ref = "cc-worker"
engine = "cfuse-cc"
model = "sonnet"
cwd = "/tmp"
"#,
)
.unwrap();
let cfg = ProviderConfig::load(&path).unwrap();
assert_eq!(cfg.provider_id, "bridge-1");
let bot = cfg.bot("cc-worker").unwrap();
assert_eq!(bot.engine, EngineKind::CfuseCc);
assert_eq!(bot.model.as_deref(), Some("sonnet"));
assert!(cfg.bot("nope").is_none());
}

#[test]
fn rejects_unknown_engine_kind() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bridge.toml");
std::fs::write(
&path,
r#"
provider_id = "bridge-1"
listen = "127.0.0.1:21100"
bcs_to_provider_token = "t"
[[bot]]
provider_bot_ref = "x"
engine = "bogus"
cwd = "/tmp"
"#,
)
.unwrap();
assert!(ProviderConfig::load(&path).is_err());
}
}
Loading
Loading