Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
03162b6
Audit dependencies against RustSec advisories (#249)
fylorn Oct 2, 2026
172dbe2
Plugin scaffolding: the plugin set, stats, log ring and the plugin_fa…
fylorn Oct 2, 2026
accb403
Merge main into feat/plugins (session transcript, redacted bodies)
fylorn Oct 2, 2026
6f82f27
Plugin management: config section, loading with hash checks, recordin…
fylorn Oct 2, 2026
759abfa
Plugin data plane: request and reply hooks, request views, placeholde…
fylorn Oct 2, 2026
424d2cf
Add tw-plugin: run script plugins in a QuickJS-in-Wasmtime sandbox (#…
fylorn Oct 2, 2026
854eba9
Run plugins in the tw-plugin sandbox (#259)
fylorn Oct 2, 2026
cba1643
Test the plugin control plane against the real sandbox (#260)
fylorn Oct 2, 2026
ae924ba
Plugin security tests: adversarial corpus, sandbox and end-to-end tes…
fylorn Oct 2, 2026
1683065
Default plugins core ships, and route-first request hook tests (#261)
fylorn Oct 2, 2026
97ef2e7
Route first, then run the request hook once per upstream attempt (#264)
fylorn Oct 2, 2026
5540765
Default plugins, guarded updates for tool-call plugins, no sandbox wh…
fylorn Oct 2, 2026
9eab874
Run request hooks on every body sent upstream; cap live reply instanc…
fylorn Oct 2, 2026
79cca63
Plugins opt in to embeddings and legacy completions; other requests p…
fylorn Oct 2, 2026
389255f
Merge main into feat/plugins: script plugins on the unified guards
fylorn Oct 2, 2026
e8a19ca
Bump Wasmtime to 49.0.2 (RUSTSEC-2026-0325, -0326, -0327)
fylorn Oct 2, 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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# 桌面端按行读消息码清单,换行不能随平台变
crates/tw-api/msg-codes.txt text eol=lf
# 默认插件随 core 一起发:源码按字节算哈希(装上、换新版都拿它比),预先算好的
# manifest 也记着这份哈希。各平台检出、编进二进制的字节必须一样
crates/tw-gateway/src/plugin/defaults/*.js text eol=lf
crates/tw-gateway/src/plugin/defaults/manifests.json text eol=lf
40 changes: 40 additions & 0 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 依赖有没有安全公告(RustSec)。配置在仓库根目录的 deny.toml。
#
# 插件跑在 Wasmtime 的沙箱里,沙箱的安全就是 Wasmtime 的安全,而它几乎每个
# 大版本都有公告。别的依赖(rustls、hyper……)同理。
#
# **两条触发,各管一件事。**
# - 改了依赖的 PR、推到 main:这次引入或升级的 crate 有没有公告。只在依赖
# 文件变了的时候跑 —— 否则一条新公告发布出来,所有不相干的 PR 一起红,
# 而和自己的改动无关的红,看几次就没人看了。
# - 每天定时:依赖没动,公告库却在长。新公告落在已有的依赖上,在这里红,
# 失败的通知照常发。
name: Audit

on:
pull_request:
paths:
- "**/Cargo.toml"
- "Cargo.lock"
- "deny.toml"
- ".github/workflows/audit.yml"
push:
branches: [main]
paths:
- "**/Cargo.toml"
- "Cargo.lock"
- "deny.toml"
- ".github/workflows/audit.yml"
schedule:
- cron: "0 2 * * *"
workflow_dispatch:

jobs:
advisories:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 只查公告。许可证、重复版本这几项是另一回事,不在这里
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check advisories
42 changes: 41 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,41 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
# 插件沙箱里的 QuickJS 编成这个目标(crates/tw-plugin/build.rs)
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2

# 编插件沙箱要一个能出 wasm 的 clang:这里是 Homebrew 的 llvm,由 build.rs
# 自己找到(顺便测了「装了 brew 的 llvm 就能编」这条路)
- name: Toolchain for the plugin sandbox
run: bash scripts/wasm-toolchain.sh

- name: Format
run: cargo fmt --all -- --check
run: |
cargo fmt --all -- --check
# 沙箱里的那个小工程不在工作区里,上面一行管不到它
cargo fmt --manifest-path crates/tw-plugin/guest/Cargo.toml -- --check

- name: clippy
run: cargo clippy --workspace --all-targets -- -D warnings

# 沙箱里的胶水(编成 wasm32 的 no_std 小工程)。build.rs 编它时不带 -D warnings
# —— 那是给外层工作区的 —— 所以它的告警只在这一步当错误
- name: clippy (plugin sandbox guest)
run: |
LLVM="$(brew --prefix llvm)/bin"
CC_wasm32_unknown_unknown="$LLVM/clang" AR_wasm32_unknown_unknown="$LLVM/llvm-ar" \
cargo clippy --manifest-path crates/tw-plugin/guest/Cargo.toml --target wasm32-unknown-unknown --target-dir target/tw-plugin-guest -- -D warnings

# 默认不跑打真实网络的那些(它们标了 #[ignore])—— CI 上的网络
# 抖动会变成一条和代码无关的红,而那种红看几次就没人看了。
- name: Test
run: cargo test --workspace

# 编进这一版的沙箱是哪个 clang 编的、wasm 的哈希
- name: Which compiler built the plugin sandbox
run: bash scripts/wasm-toolchain.sh record target

# 桌面端从 tw-api 导出前端类型(`ts` feature)。**默认关**,上面几步一行都
# 不编它 —— 而它坏掉的表现是桌面端接下一个 tag 时才发现导不出来。导出来
# 的文件再过一遍 tsc:ts-rs 生成的东西本身也可能不是合法的 TypeScript
Expand Down Expand Up @@ -158,17 +180,25 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
# 理由见下面 windows 那一段
cache-on-failure: true

# 镜像预装的 clang-15,显式指定(见脚本)
- name: Toolchain for the plugin sandbox
run: bash scripts/wasm-toolchain.sh

- name: clippy
run: cargo clippy --workspace --all-targets -- -D warnings

- name: Test
run: cargo test --workspace

- name: Which compiler built the plugin sandbox
run: bash scripts/wasm-toolchain.sh record target

# 真二进制、真 socket、真数据面,和 macOS 那一步是同一个脚本
- name: Smoke (real binary, real socket, real data plane)
run: ./scripts/smoke.sh
Expand All @@ -185,6 +215,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
# **失败也存。**这个 action 默认只在任务成功时保存缓存,而一个
Expand All @@ -196,12 +227,21 @@ jobs:
# target 目录是安全的,它只是省掉那些与失败无关的部分。
cache-on-failure: true

# 镜像预装的 LLVM(C:\Program Files\LLVM),由 build.rs 自己找到
- name: Toolchain for the plugin sandbox
shell: bash
run: bash scripts/wasm-toolchain.sh

- name: clippy
run: cargo clippy --workspace --all-targets -- -D warnings

- name: Test
run: cargo test --workspace

- name: Which compiler built the plugin sandbox
shell: bash
run: bash scripts/wasm-toolchain.sh record target

- name: Build
run: cargo build --release -p twcore

Expand Down
85 changes: 78 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,27 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
# wasm32:插件沙箱里的 QuickJS(crates/tw-plugin/build.rs)
targets: aarch64-apple-darwin, wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2

# 编插件沙箱要一个能出 wasm 的 clang:Homebrew 的 llvm
- name: Toolchain for the plugin sandbox
run: bash scripts/wasm-toolchain.sh

# `-p tw-plugin`:网关还没接上插件之前,它不在 twcore 的依赖里,不点名就
# 没人编它 —— 而这条流水线要证明的正是每个目标平台都编得出沙箱
- name: Build
run: cargo build --release -p twcore --target aarch64-apple-darwin
run: cargo build --release -p twcore -p tw-plugin --target aarch64-apple-darwin

# 这个平台的沙箱是哪个 clang 编的、wasm 的哈希:进摘要,也交给 publish
- name: Which compiler built the plugin sandbox
run: bash scripts/wasm-toolchain.sh record target/aarch64-apple-darwin/release guest-build/aarch64-apple-darwin.txt
- uses: actions/upload-artifact@v4
with:
name: guest-build-aarch64-apple-darwin
path: guest-build/
if-no-files-found: error

# **产物自检。**一个编得过但起不来的二进制,从文件列表上看不出
# 任何问题 —— 而它会一路发到用户手里。
Expand Down Expand Up @@ -98,13 +114,31 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc, aarch64-pc-windows-msvc
targets: x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2

# 镜像预装的 LLVM(C:\Program Files\LLVM);没有就装官方发行版
- name: Toolchain for the plugin sandbox
shell: bash
run: bash scripts/wasm-toolchain.sh

# arm64 的沙箱机器码在 x64 上交叉编:build.rs 里的 Cranelift 直接编给目标平台
- name: Build
run: |
cargo build --release -p twcore --target x86_64-pc-windows-msvc
cargo build --release -p twcore --target aarch64-pc-windows-msvc
cargo build --release -p twcore -p tw-plugin --target x86_64-pc-windows-msvc
cargo build --release -p twcore -p tw-plugin --target aarch64-pc-windows-msvc

- name: Which compiler built the plugin sandbox
shell: bash
run: |
for t in x86_64-pc-windows-msvc aarch64-pc-windows-msvc; do
bash scripts/wasm-toolchain.sh record "target/$t/release" "guest-build/$t.txt"
done
- uses: actions/upload-artifact@v4
with:
name: guest-build-windows
path: guest-build/
if-no-files-found: error

# **产物自检。**一个编得过但起不来的二进制,从文件列表上看不出任何
# 问题 —— 而它会一路发到用户手里。
Expand Down Expand Up @@ -202,13 +236,25 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
targets: ${{ matrix.target }}, wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}

# 两种 runner 都预装 clang-15,两个架构用同一个版本(见脚本)
- name: Toolchain for the plugin sandbox
run: bash scripts/wasm-toolchain.sh

- name: Build
run: cargo build --release -p twcore --target ${{ matrix.target }}
run: cargo build --release -p twcore -p tw-plugin --target ${{ matrix.target }}

- name: Which compiler built the plugin sandbox
run: bash scripts/wasm-toolchain.sh record "target/${{ matrix.target }}/release" "guest-build/${{ matrix.target }}.txt"
- uses: actions/upload-artifact@v4
with:
name: guest-build-${{ matrix.target }}
path: guest-build/
if-no-files-found: error

# **产物自检。**一个编得过但起不来的二进制,从文件列表上看不出任何
# 问题 —— 而它会一路发到用户手里。
Expand Down Expand Up @@ -332,8 +378,33 @@ jobs:
- uses: actions/download-artifact@v4
with:
path: dist
pattern: twcore-*
merge-multiple: true

# 每个平台的插件沙箱是哪个 clang 编的、wasm 的哈希。**只记录,不卡发版**:
# macOS 用 Homebrew 的 llvm、Linux 用 clang-15、Windows 用镜像里的 LLVM,
# 编译器不同,哈希本来就不同;同一个编译器编的(两个 Linux)应当相同
- uses: actions/download-artifact@v4
with:
path: guest-build
pattern: guest-build-*
merge-multiple: true
- name: Which compiler built each plugin sandbox
run: |
set -euo pipefail
{
echo "### Plugin sandbox"
echo
echo "| target | guest.wasm sha256 | clang |"
echo "|---|---|---|"
for f in guest-build/*.txt; do
t=$(basename "$f" .txt)
sha=$(awk '/^guest.wasm sha256 /{print $3}' "$f")
cc=$(sed -n 's/^clang //p' "$f" | head -n 1)
echo "| $t | \`$sha\` | $cc |"
done
} | tee -a "$GITHUB_STEP_SUMMARY"

# 排练也跑这一步:构建 job 交来的正好是清单上的文件,每个都对得上
# 它的校验和
- name: Every file is here, nothing else is, and each matches its checksum
Expand Down
46 changes: 46 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,30 @@ cargo test --workspace
./scripts/smoke.sh
```

Building `tw-plugin` needs an LLVM clang that targets WebAssembly; the README's
"Build and test" says how to install one. If you changed
`crates/tw-plugin/guest`, which is not a workspace member, check it on its own
as well (on macOS with Homebrew's LLVM):

```bash
cargo fmt --manifest-path crates/tw-plugin/guest/Cargo.toml -- --check
CC_wasm32_unknown_unknown="$(brew --prefix llvm)/bin/clang" \
AR_wasm32_unknown_unknown="$(brew --prefix llvm)/bin/llvm-ar" \
cargo clippy --manifest-path crates/tw-plugin/guest/Cargo.toml --target wasm32-unknown-unknown \
--target-dir target/tw-plugin-guest -- -D warnings
```

Warnings are errors, and relaxing that on CI is the same as removing it.
The toolchain is `stable`, so a newer stable than your local one can
surface lints you cannot reproduce — `rustup update stable` before
blaming CI.

A change to a `Cargo.toml` or to `Cargo.lock` also runs the Audit
workflow: `cargo deny check advisories`, configured in `deny.toml`, fails
on any RustSec advisory against a crate in the lock file. It runs daily
on `main` as well, so an advisory published against a dependency that is
already shipped turns up without anyone touching the dependencies.

`scripts/smoke.sh` runs the real binary against a real socket and a real
data plane, talking to the control plane through `twcore call` (every
control connection starts with a Noise handshake, so curl cannot). **It catches what unit tests structurally cannot** — file
Expand Down Expand Up @@ -99,6 +118,33 @@ clean the diff is:
connections to the same handshake before HTTP. The control key never
leaves through the control plane and cannot be changed through it.

## The plugin sandbox

Script plugins run in `tw-plugin`: QuickJS-ng, from the pinned `rquickjs-sys`
crate, compiled to `wasm32-unknown-unknown` and run by Wasmtime. Its
`build.rs` does three things on every build, and nothing is committed or
downloaded:

1. **Compile the guest** (`crates/tw-plugin/guest`, outside the workspace,
with its own `Cargo.lock`) with the clang it finds. The module may import
two functions, a log line and the clock; the build fails if it imports
anything else.
2. **Snapshot it.** It runs the bridge script (`src/bridge.js`) once inside the
module and writes the initialized memory back into it, so every sandbox
starts with QuickJS already set up.
3. **Precompile it** with Cranelift for the target being built, cross targets
included. The binary embeds the result and contains only Wasmtime's
runtime, no compiler.

Wasmtime is pinned to one exact version both as a dependency and as a
build-dependency: a precompiled module only loads in the Wasmtime version, and
with the settings (`src/engine.rs`), it was compiled with. Upgrade both
together.

Only `tw-gateway` and `twcore` may depend on `tw-plugin`.
`crates/tw-plugin/tests/boundary.rs` fails when a crate that Lite or
Enterprise builds reaches it, because their builds would suddenly need clang.

## The configuration reference

`docs/config.md` and `docs/config.zh-CN.md` are written by hand, except the
Expand Down
Loading
Loading