Skip to content

Commit fcc4424

Browse files
committed
feat(web): W3-05 harden write Origin/rate/body gates (v0.19.131)
Browser writes require bind-derived loopback Origin; automation keeps token auth. Add per-session rate limits with Retry-After, restore fixtures dropped in W3-04, and keep omitted-kind code-control attach. Signed-off-by: Eli Ma <eli@patch.sh>
1 parent 946fde2 commit fcc4424

36 files changed

Lines changed: 1676 additions & 45 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
## [Unreleased]
44

5+
### Added (plan-20260715 W3-05, 2026-08-12)
6+
7+
- **W3-05 hardens Code UI write identity/request boundaries.** Browser
8+
attach/writes require a trusted loopback `Origin` (or same-origin Referer)
9+
derived from the bind address (including non-canonical loopback and port 80
10+
forms); automation keeps bearer/control-token auth without Origin.
11+
Per-session write rate limiting (`LIBRA_CODE_SESSION_WRITE_RATE_*`) returns
12+
`429 RATE_LIMITED` with `Retry-After`. Body limit remains fail-closed on both
13+
paths. Omitted attach `kind` with a control token resolves to automation so
14+
`libra code-control` stays compatible.
15+
516
### Added (plan-20260715 W3-04, 2026-08-11)
617

718
- **W3-04 normalizes Codex websocket events into the shared `AgentEvent`

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "libra"
3-
version = "0.19.130"
3+
version = "0.19.131"
44
edition = "2024"
55
license = "MIT"
66
description = "AI agent-native version control system with Git on-disk compatibility, SQLite-backed metadata, and tiered cloud storage"

docs/commands/code.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ Selecting `loopback` is rejected when `--host` is not a loopback address, and th
117117

118118
Browser write requests share the same 256 KiB body limit and audit-sink wiring as automation control. The browser persists the lease only in memory; reloading the page drops the lease and the next write reattaches.
119119

120+
Browser writes (including `POST /controller/attach` with `kind: "browser"`) additionally require a trusted loopback `Origin` (or same-origin `Referer` fallback) that matches the Code UI bind address (exact `http://<bound-ip>:<port>`, plus `localhost` / `127.0.0.1` / `[::1]` aliases when bound to canonical loopback). Missing or cross-site Origin fails closed with `ORIGIN_REQUIRED`. Automation writes authenticate with `X-Libra-Control-Token` / controller lease and do **not** use Origin as a substitute. Per-session write rate limiting applies to both browser and automation producers (`LIBRA_CODE_SESSION_WRITE_RATE_LIMIT` / `LIBRA_CODE_SESSION_WRITE_RATE_WINDOW_SECS`, default 120 writes / 60s) and returns `429 RATE_LIMITED` until the window recovers.
121+
120122
The embedded SPA session-lifecycle panels list threads via `GET /api/code/threads`, cancel the active turn through `POST /api/code/control/cancel` (fail-closed when `controller.canWrite` is false), and post resume selection through `POST /api/code/session/resume` with `{ "threadId": "..." }`. Thread list is repository-storage-scoped (shared across linked worktrees), while resume is working-directory scoped; listed items omit `workingDir` until ThreadProjection persists a per-thread cwd.
121123

122124
The usage panel mirrors the W2-12 `RuntimeUsageTotals` read model (cumulative, current-turn delta, sub-agent attribution) and keeps `partial`/`unknown`/`error` visible instead of pretending zero spend. `GET /api/code/usage` reads durable totals and returns an error rather than fabricated zeroes. When durable sub-agent enumeration is unavailable, the response omits `subAgents` and sets `subAgentsStatus: "unavailable"` instead of an empty array.
@@ -168,6 +170,8 @@ Code UI API errors use `{ error: { code, message } }`:
168170
|------|------|---------|
169171
| `LOOPBACK_REQUIRED` | 403 | Non-loopback client attempted an API route. |
170172
| `PAYLOAD_TOO_LARGE` | 413 | Write request body exceeded 256 KiB. |
173+
| `ORIGIN_REQUIRED` | 403 | Browser write/attach lacked a trusted loopback `Origin` (or same-origin `Referer`), or presented a cross-site Origin. |
174+
| `RATE_LIMITED` | 429 | Per-session write budget exhausted; retry after the rate-limit window (see `Retry-After` / wait for window recovery). |
171175
| `CONTROL_DISABLED` | 403 | Automation control is not enabled for this process. |
172176
| `MISSING_CONTROL_TOKEN` | 403 | Automation control token is absent. |
173177
| `INVALID_CONTROL_TOKEN` | 403 | Automation control token is invalid. |

docs/commands/zh-CN/code.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ Automation clients 使用 `POST /api/code/controller/attach` 连接,请求体
117117

118118
浏览器写请求共享与自动化控制相同的 256 KiB body limit 和 audit-sink wiring。浏览器只在内存中持久化 lease;重新加载页面会丢弃 lease,下一次写入会重新 attach。
119119

120+
浏览器写入(含 `kind: "browser"``POST /controller/attach`)还要求可信的 loopback `Origin`(或同源 `Referer` 回退),且必须匹配 Code UI bind 地址(精确的 `http://<bound-ip>:<port>`;绑定到经典 loopback 时额外接受 `localhost` / `127.0.0.1` / `[::1]` 别名)。缺失或跨站 Origin 以 `ORIGIN_REQUIRED` fail-closed。Automation 写入走 `X-Libra-Control-Token` / controller lease,**不得**用 Origin 代替身份校验。浏览器与 automation 生产者共用按 session 的写速率限制(`LIBRA_CODE_SESSION_WRITE_RATE_LIMIT` / `LIBRA_CODE_SESSION_WRITE_RATE_WINDOW_SECS`,默认 120 次 / 60s),超限返回 `429 RATE_LIMITED`,窗口恢复后可继续。
121+
120122
嵌入式 SPA 的 session-lifecycle 面板通过 `GET /api/code/threads` 列出 threads,经 `POST /api/code/control/cancel` 取消当前 turn(当 `controller.canWrite` 为 false 时 fail-closed),并经 `POST /api/code/session/resume``{ "threadId": "..." }` 发起 resume。Thread 列表按仓库存储根共享(跨 linked worktree);列表项在 ThreadProjection 持久化 per-thread cwd 之前省略 `workingDir`
121123

122124
Usage 面板镜像 W2-12 `RuntimeUsageTotals` read model(累计、本 turn 增量、sub-agent 归因),并保持 `partial`/`unknown`/`error` 可见,而不是伪装成零花费。`GET /api/code/usage` 从 durable totals 读取,失败时返回错误而不伪造零值。当 durable sub-agent 枚举不可用时,响应省略 `subAgents` 并设置 `subAgentsStatus: "unavailable"`,而不是返回空数组。
@@ -168,6 +170,8 @@ Code UI API 错误使用 `{ error: { code, message } }`:
168170
|------|------|------|
169171
| `LOOPBACK_REQUIRED` | 403 | 非 loopback client 试图访问 API route。 |
170172
| `PAYLOAD_TOO_LARGE` | 413 | 写请求体超过 256 KiB。 |
173+
| `ORIGIN_REQUIRED` | 403 | 浏览器写/attach 缺少可信 loopback `Origin`(或同源 `Referer`),或提交了跨站 Origin。 |
174+
| `RATE_LIMITED` | 429 | 当前 session 写配额耗尽;等待速率窗口恢复后重试(见 `Retry-After`)。 |
171175
| `CONTROL_DISABLED` | 403 | 当前进程未启用 automation control。 |
172176
| `MISSING_CONTROL_TOKEN` / `INVALID_CONTROL_TOKEN` | 403 | Automation control token 缺失或无效。 |
173177
| `MISSING_CONTROLLER_TOKEN` / `INVALID_CONTROLLER_TOKEN` | 403 | Lease token 对写路由缺失或无效。 |

docs/development/plan/plan-20260715.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1468,24 +1468,26 @@ command id、跨 adapter durable interaction source 和本卡完整 approval TTL
14681468
**Description:** 在 Web default 前补齐 Code Web write surface 的**身份与请求边界**:token/lease/repo-session scope、浏览器 Origin(CSRF)、请求体上限、per-session rate limit。目录信任、external RPC env allowlist 与 secret-like env 拒绝复用 20260708 A0-08,不在本文重新设计。**Current evidence(2026-07-27 核对)**:loopback-only 路由分组、请求体大小限制、controller token、lease 已存在(`src/internal/ai/web/mod.rs` 路由注释与 `code_ui.rs` token 校验);**本卡增量**为浏览器 Origin 校验与 per-session rate limit——现有 security matrix 尚无 Origin/rate 场景,必须新增,不得以现有用例冒充覆盖。**边界(R27,Codex R27 P1-1 收窄)**:SSE 慢消费者断开/backpressure 属容量-传输轴,归 **W3-08**;control sidecar 文件面隔离归 **W3-10**;端口/host posture 归 **W3-11**;敏感值 redaction 归 **W3-12**
14691469

14701470
**Acceptance criteria:**
1471-
- [ ] Browser/automation write 仅 loopback + valid token/lease + repo/session scope 校验通过。
1472-
- [ ] **CORS / Origin 校验(新增)**:浏览器对 `/api/code/*` 的写请求必须携带可信 loopback `Origin`(必要时以同源 `Referer` 作兼容校验),缺失或跨站时 fail-closed;静态资源与 API 同端口也不得放宽。非浏览器 automation 不走该浏览器判定,而使用专用 bearer token/JSON-RPC transport,并继续校验 repo/session scope、controller lease、request limit、rate limit 与 command idempotency;禁止通过伪造 Origin 绕过身份边界(GC-CODE-11)。
1473-
- [ ] 每 session rate limit 可配置,默认保护 human/automation producer;触发后有稳定错误与恢复语义。
1474-
- [ ] 请求体大小上限对浏览器与 automation 两条路径同时生效,超限返回稳定错误而非截断。
1471+
- [x] Browser/automation write 仅 loopback + valid token/lease + repo/session scope 校验通过。
1472+
- [x] **CORS / Origin 校验(新增)**:浏览器对 `/api/code/*` 的写请求必须携带可信 loopback `Origin`(必要时以同源 `Referer` 作兼容校验),缺失或跨站时 fail-closed;静态资源与 API 同端口也不得放宽。非浏览器 automation 不走该浏览器判定,而使用专用 bearer token/JSON-RPC transport,并继续校验 repo/session scope、controller lease、request limit、rate limit 与 command idempotency;禁止通过伪造 Origin 绕过身份边界(GC-CODE-11)。
1473+
- [x] 每 session rate limit 可配置,默认保护 human/automation producer;触发后有稳定错误与恢复语义。
1474+
- [x] 请求体大小上限对浏览器与 automation 两条路径同时生效,超限返回稳定错误而非截断。
14751475

14761476
**Verification:**(Origin/rate 两个场景过滤为本任务新增用例,标 `(new)`
1477-
- [ ] `LIBRA_ENABLE_TEST_PROVIDER=1 cargo test --features test-provider --test code_ui_remote_security_matrix -- --test-threads=1`
1478-
- [ ] `LIBRA_ENABLE_TEST_PROVIDER=1 cargo test --features test-provider --test code_ui_remote_security_matrix origin -- --test-threads=1` (new):浏览器写请求缺失/跨站 Origin fail-closed、可信 loopback Origin 通过、automation 无 Origin 走 bearer token 路径。
1479-
- [ ] `LIBRA_ENABLE_TEST_PROVIDER=1 cargo test --features test-provider --test code_ui_remote_security_matrix rate_limit -- --test-threads=1` (new):per-session rate limit 触发与恢复。
1480-
- [ ] `LIBRA_ENABLE_TEST_PROVIDER=1 cargo test --features test-provider --test code_ui_remote_security_matrix body_size -- --test-threads=1` (new 或点名既有 case):浏览器与 automation 写请求超 body limit fail-closed。
1481-
- [ ] `cargo test --test ai_security_runtime_test`
1477+
- [x] `LIBRA_ENABLE_TEST_PROVIDER=1 cargo test --features test-provider --test code_ui_remote_security_matrix -- --test-threads=1`
1478+
- [x] `LIBRA_ENABLE_TEST_PROVIDER=1 cargo test --features test-provider --test code_ui_remote_security_matrix origin -- --test-threads=1` (new):浏览器写请求缺失/跨站 Origin fail-closed、可信 loopback Origin 通过、automation 无 Origin 走 bearer token 路径。
1479+
- [x] `LIBRA_ENABLE_TEST_PROVIDER=1 cargo test --features test-provider --test code_ui_remote_security_matrix rate_limit -- --test-threads=1` (new):per-session rate limit 触发与恢复。
1480+
- [x] `LIBRA_ENABLE_TEST_PROVIDER=1 cargo test --features test-provider --test code_ui_remote_security_matrix body_size -- --test-threads=1` (new 或点名既有 case):浏览器与 automation 写请求超 body limit fail-closed。
1481+
- [x] `cargo test --test ai_security_runtime_test`
14821482

14831483
**Dependencies:** W3-01;A0-08 trusted_dirs/env allowlist 已随 v0.18.30 落地,W0-01 只确认其当前接口锚点与语义。
14841484

14851485
**Files likely touched:** `src/internal/ai/web/code_ui.rs`(请求校验中间件), `src/internal/ai/web/mod.rs`(路由分组), `tests/data/code_ui_remote/security_cases.json`.
14861486

14871487
**Estimated scope:** M(R27 自原超限 M 卡(AC=10)拆出)。
14881488

1489+
**实现轮 Codex review(2026-08-11/12,model=`gpt-5.6-terra`****PASS**(r7;无 material P0/P1。闭合:bound-IP/`port 80` Origin allowlist、`ORIGIN_REQUIRED`/`RATE_LIMITED` 文档与错误表、`Retry-After`、post-attach `/messages` Origin 矩阵、omitted-kind automation attach 与 `code-control` 兼容)。验证:security matrix 22/22、`write_guards` 8/8、`ai_code_ui_wire_test` attach round-trip。发布 **v0.19.131**。残差:control sidecar → **W3-10**;SSE backpressure → **W3-08**
1490+
14891491
### Task W3-06: SSE/event wire v2 协议与 v1 兼容窗口(R27 三轴之一:protocol)
14901492

14911493
**Description:** 当前 Code UI SSE 通过 `CodeUiSession::broadcast_snapshot` 每次发送完整 `CodeUiSessionSnapshot`,lag recovery 也重新发送整包 snapshot。Web default 后,长会话的 transcript/plan/tool/patchset 事件会成为热路径;必须把 runtime event log、projection fold 与 HTTP/SSE wire contract 对齐。本卡交付 **v2 delta/cursor 协议与 v1/v2 协商**,并保留 v1 snapshot SSE 一版兼容,避免前端和 automation client 同步破坏。**边界(R27/R30)**:backlog 上限/resync/慢消费者 backpressure 归 **W3-08**;大 session projection p95 硬门禁归 **W3-14**;内置前端切到 v2 归 **W3-09**;本卡不改 `web/src`

install.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ $ErrorActionPreference = "Stop"
2222
# One of the release version surfaces. `compat_version_surface_sync` pins it
2323
# to Cargo.toml: this value is substituted verbatim into the download URL, so
2424
# a stale value silently installs an old binary when -Version is not given.
25-
$DefaultVersion = "v0.19.130"
25+
$DefaultVersion = "v0.19.131"
2626
$ExeName = "libra.exe"
2727
$ReleaseAsset = "libra-windows-amd64.exe"
2828

install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ INSTALL_DIR="${LIBRA_INSTALL_DIR:-$LIBRA_HOME/bin}"
1818
# user opts in with LIBRA_ALLOW_FALLBACK=1. Default behaviour is fail-fast so
1919
# offline installs cannot silently regress to a stale version. Bump this on
2020
# every release so the opt-in fallback remains useful.
21-
DEFAULT_VERSION="v0.19.130"
21+
DEFAULT_VERSION="v0.19.131"
2222

2323
# ─── theme (Dusk) ────────────────────────────────────────────────────────────
2424
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ -z "${LIBRA_NO_TUI:-}" ] && [ "${TERM:-dumb}" != "dumb" ]; then

src/command/code_control.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,9 @@ async fn dispatch_json_rpc_request(
258258
Err(error) => return DispatchResult::Error(error),
259259
};
260260
let mut body = json!({ "clientId": params.client_id });
261-
if let Some(kind) = params.kind {
262-
body["kind"] = Value::String(kind);
263-
}
261+
// Default omitted kind to automation: this shim always authenticates
262+
// with X-Libra-Control-Token and never sends a browser Origin.
263+
body["kind"] = Value::String(params.kind.unwrap_or_else(|| "automation".to_string()));
264264
send_post(
265265
client,
266266
base_url,

src/internal/ai/web/code_ui.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -344,12 +344,11 @@ pub struct CodeUiEventEnvelope {
344344
#[serde(rename_all = "camelCase")]
345345
pub struct CodeUiControllerAttachRequest {
346346
pub client_id: String,
347-
#[serde(default = "default_controller_attach_kind")]
348-
pub kind: CodeUiControllerKind,
349-
}
350-
351-
fn default_controller_attach_kind() -> CodeUiControllerKind {
352-
CodeUiControllerKind::Browser
347+
/// When omitted, the HTTP handler resolves `browser` vs `automation` from
348+
/// whether `X-Libra-Control-Token` is present (`code-control` shim omits
349+
/// `kind` and authenticates with the control token).
350+
#[serde(default)]
351+
pub kind: Option<CodeUiControllerKind>,
353352
}
354353

355354
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -1546,6 +1545,10 @@ pub fn code_ui_error_codes() -> &'static [(&'static str, u16)] {
15461545
("LOOPBACK_REQUIRED", 403),
15471546
// Then the body-limit middleware (write surface only).
15481547
("PAYLOAD_TOO_LARGE", 413),
1548+
// Browser Origin / CSRF gate (W3-05); automation skips this check.
1549+
("ORIGIN_REQUIRED", 403),
1550+
// Per-session write rate limit (browser + automation, W3-05).
1551+
("RATE_LIMITED", 429),
15491552
// Then automation control-token gate.
15501553
("CONTROL_DISABLED", 403),
15511554
("MISSING_CONTROL_TOKEN", 403),
@@ -1914,11 +1917,11 @@ mod tests {
19141917
}
19151918

19161919
#[test]
1917-
fn attach_request_defaults_to_browser_kind() {
1920+
fn attach_request_omitted_kind_deserializes_as_none() {
19181921
let request: CodeUiControllerAttachRequest =
19191922
serde_json::from_value(serde_json::json!({ "clientId": "browser-1" })).unwrap();
19201923

1921-
assert_eq!(request.kind, CodeUiControllerKind::Browser);
1924+
assert_eq!(request.kind, None);
19221925
}
19231926

19241927
#[tokio::test]
@@ -2101,6 +2104,10 @@ mod tests {
21012104
// mod.rs `enforce_code_write_body_limit` /
21022105
// `code_control_body_too_large_response`.
21032106
("PAYLOAD_TOO_LARGE", 413),
2107+
// mod.rs browser Origin gate / write_guards (W3-05).
2108+
("ORIGIN_REQUIRED", 403),
2109+
// mod.rs per-session write rate limiter (W3-05).
2110+
("RATE_LIMITED", 429),
21042111
// mod.rs `parse_optional_u64` (?limit/?offset parser).
21052112
("INVALID_QUERY_PARAM", 400),
21062113
// mod.rs `code_threads_handler` storage path build.

0 commit comments

Comments
 (0)