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
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ pnpm --dir worker build

- `build.rs` embeds `web/out/` with `rust-embed`; setting
`LIBRA_SKIP_WEB_BUILD=1` writes a stub output for Rust-only builds. When
changing `web/`, run the real `pnpm --dir web build` and keep static export
drift clean.
changing `web/`, run the real `pnpm --dir web build`; `web/out/` is generated
and ignored, so never add its static export to a commit.
- The `web/` UI is an operational Code UI, not a marketing landing page. Favor
dense, predictable controls and existing components in `web/src/components/ui/`.
- The `worker/` app serves publish snapshots from D1/R2. Validate request input,
Expand Down
27 changes: 15 additions & 12 deletions .github/workflows/base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,11 @@ jobs:
corepack enable
corepack prepare pnpm@11.10.0 --activate

# The Rust crate's `build.rs` skips the web build whenever
# `LIBRA_SKIP_WEB_BUILD=1` (set on every other job for speed). This
# dedicated job re-asserts that the Next.js source still type-checks,
# lints, and produces a clean static export so `web/out/` cannot
# silently drift away from `WebAssets` in main. Per
# docs/improvement/web.md Phase 5 verification.
# Rust-oriented jobs skip the web build for speed. This dedicated job
# validates the Next.js source, generates a fresh static export, and
# compiles it into `WebAssets`; the generated directory is deliberately
# not version-controlled. Per docs/improvement/web.md Phase 5
# verification.
- name: Install web dependencies
run: pnpm --dir web install --frozen-lockfile

Expand All @@ -103,16 +102,20 @@ jobs:
- name: Build web (static export → web/out/)
run: pnpm --dir web build

- name: Check web/out static export drift
- name: Verify generated static export lifecycle
shell: bash
run: |
status="$(git status --porcelain -- web/out)"
if [[ -n "$status" ]]; then
echo "web/out has untracked, staged, or unstaged files after the static export build." >&2
echo "Run 'pnpm --dir web build' locally and commit the updated web/out files." >&2
printf '%s\n' "$status" >&2
test -f web/out/index.html
if git ls-files --error-unmatch -- web/out >/dev/null 2>&1; then
echo "web/out is generated build output and must not be tracked." >&2
exit 1
fi
git check-ignore -q web/out/index.html

- name: Check generated export embeds into the Rust binary
env:
LIBRA_SKIP_WEB_BUILD: "1"
run: cargo check --lib
Comment thread
cursor[bot] marked this conversation as resolved.

owner-liveness-macos:
# plan-20260714 W1 (§C.9): the operation claim decides whether a control
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ target-codex-review/
web/node_modules/
web/.next/
web/.pnpm-store/
# Static export embedded by build.rs; generated from web/ source when required.
web/out/

# Publish Worker (Phase 6/7)
worker/node_modules/
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ pnpm --dir web install --frozen-lockfile && pnpm --dir web build
All PRs must pass these jobs on the `[self-hosted]` runner pool:
1. **compat-rustfmt** — `cargo +nightly fmt --all --check`
2. **compat-clippy** — `cargo clippy --all-targets --all-features -- -D warnings` (with `LIBRA_SKIP_WEB_BUILD=1`)
3. **compat-web-check** — `pnpm --dir web lint` + `pnpm --dir web build` so `web/out/` cannot drift from `WebAssets`
3. **compat-web-check** — `pnpm --dir web lint` + test + build, followed by `cargo check --lib` against the generated `WebAssets`; `web/out/` is ignored build output
4. **compat-redundancy** — directory-shape check on `third-party/rust/crates`
5. **compat-offline-core** — `cargo test --test compat_matrix_alignment compatibility_matrix_matches_cli_commands -- --exact` + pinned `cargo nextest run --all --no-fail-fast --retries 2` (one process per test; external-resource mutual exclusion from the generated `.config/nextest.toml`; the former `compat-offline-command` shard job is merged in) + `cargo test --doc` + a `--features test-provider` nextest pass (`--profile test-provider`, ten Code UI automation targets — `code_ui_scenarios`, `harness_self_test`, `code_codex_default_web_test`, `ai_code_ui_headless_test`, `code_codex_runtime_test`, `code_ui_remote_lease_matrix`, `code_ui_remote_sse_matrix`, `code_ui_remote_state_matrix`, `code_mcp_dual_entry_test`, `code_ui_perf_smoke_test`; the profile carries the section's single-threaded semantic) + the `otlp`/`keyring`/`test-upgrade` feature sections verbatim on `cargo test`
6. **compat-network-remotes** — `cargo test --features test-network --test network_remotes_test`
Expand Down Expand Up @@ -234,7 +234,7 @@ The publish Worker uses its own D1 schema in `sql/publish/` (`0001_publish.sql`,
`LIBRA_D1_ACCOUNT_ID`, `LIBRA_D1_API_TOKEN`, `LIBRA_D1_DATABASE_ID`

### Build & Runtime
- `LIBRA_SKIP_WEB_BUILD=1` — skip the Next.js web build in `build.rs` (set by every CI job except `compat-web-check`)
- `LIBRA_SKIP_WEB_BUILD=1` — skip the Next.js web build in `build.rs` (used by Rust-oriented CI jobs; `compat-web-check` generates the export explicitly before embedding it)
- `LIBRA_LOG`, `RUST_LOG` — `tracing-subscriber` env filter
- `LIBRA_LOG_FILE` — tracing sink path (append-mode by default; time-rolled when `LIBRA_LOG_ROTATION` is set)
- `LIBRA_LOG_ROTATION` — rolling strategy for `LIBRA_LOG_FILE`: `never` (default) / `minutely` / `hourly` / `daily` (`tracing-appender`, time-split only — no old-file pruning); inspect via `libra logfile info`
Expand Down
1 change: 1 addition & 0 deletions docs/development/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
| [`read-tree`](read-tree.md) | `partial` | Reads a tree-ish (tree/commit/ref/tag/`HEAD`) into the index, replacing it; index-only (working tree untouched); `--json`. `-m`/`-u`/`--reset`/`--prefix` deferred |
| [`update-index`](update-index.md) | `partial` | `--add`/`--remove` (re)stage/drop working-tree paths; `--cacheinfo <mode>,<object>,<path>` registers an entry from an object id (no worktree read, object need not exist); path traversal rejected; `--json`. stat-refresh / `--force-remove` / `--chmod` / `--assume-unchanged` / `--index-info` deferred |
| [`update-ref`](update-ref.md) | `partial` | Update/create/delete a `refs/heads/<branch>` ref with compare-and-swap (`<old>`; all-zero = must-not-exist), `-d`, `-m`, `--json`; ref read + write/delete + `update-ref` reflog run in one SQLite transaction (CAS operand never logged). Scoped to `refs/heads/*`; HEAD / tags / remotes / arbitrary namespaces / `ref:` values / `--stdin` / `--no-deref` rejected or deferred |
| [`upgrade`](upgrade.md) | `intentionally-different` | Manual, Ed25519-signed self-upgrade over Libra's stable channel: validates the manifest and anti-rollback floors, supports `--check`/`--yes`/`--json`, and installs through a locked, rollback-capable transaction. Git has no equivalent; needs no repository. |
| [`hooks`](hooks.md) | `intentionally-different` | Hidden compatibility entry for AI provider hook configs installed by `libra agent enable`; not a Git hooks bridge (`.git/hooks` / `core.hooksPath` rejected by D3) |
| [`index-pack`](index-pack.md) | `partial` | hidden plumbing command; `--stdin`, `--keep[=<MSG>]`, progress flags, and `--fix-thin` (accepted no-op — Libra's decoder requires self-contained packs; nothing to complete on the packs it indexes) supported |
| [`init`](init.md) | `partial` | fresh repository initialization plus Git-style safe re-initialization/top-up of existing repos (`Reinitialized existing ...`, layout top-up, `--shared` re-apply, `core.sharedRepository` persisted, DB/config/refs otherwise preserved) supported; numeric `--shared=<mode>` rejects non-traversable directory modes before partial repo creation; recursive submodule init not implemented |
Expand Down
43 changes: 43 additions & 0 deletions docs/development/commands/upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# upgrade 命令开发设计

## 命令实现目标

`libra upgrade` 是不依赖仓库的 Libra 扩展:检查 Ed25519 签名的 stable
release manifest,并在用户确认或传入 `--yes` 后,以可回滚的安装事务替换官方
安装的当前二进制。它不读取或修改仓库状态;Git 没有对应命令。

## 对比 Git 与兼容性

- 兼容级别:`intentionally-different`。
- 已支持:默认交互检查和安装、`--check`(只报告)、`-y`/`--yes`(非交互安装),
以及全局 `--json`/`--machine` 输出。
- `--check` 与 `--yes` 互斥。机器输出和 quiet 模式绝不提示;发现可用版本时,
调用方必须选择 `--check` 或 `--yes`。

## 设计方案

- 入口与分发:`src/cli.rs::Commands::Upgrade` →
`command::upgrade::execute_safe`,无需仓库 preflight。
- `command::upgrade::UpgradeArgs` 仅承载确认策略;签名 manifest 的获取、平台
选择、反回滚状态、安装标记和安装事务由 `internal::upgrade/` 统一负责。
- 每次手工检查先验证 manifest 并持久化其反回滚 floors。确认安装前会再次获取和
验证 manifest;控制面变化、暂停或撤销都会拒绝继续使用旧计划。
- 安装事务受 `internal::upgrade::lock::UpgradeLock` 保护,下载内容同时验证 size
和 sha256,并运行新二进制的 probe;事务或 probe 失败时恢复旧二进制。
- 仅安装脚本写入了官方 install marker 的二进制可自升级。源代码构建、改名副本和
不受支持的平台会给出可操作的拒绝结果。

## 当前状态

- 公开状态:已公开(`Commands::Upgrade`)。
- 用户文档:[docs/commands/upgrade.md](../../commands/upgrade.md)。
- 测试:`tests/command/upgrade_cmd_test.rs` 覆盖 CLI/官方安装标记路径;
`tests/upgrade_auto_test.rs` 和 `tests/upgrade_publish_contract_test.rs` 在
`--features test-upgrade` 下覆盖签名、状态转换、安装/回滚与发布契约。

## 维护要求

- 改动命令参数或用户可见状态时,同时更新用户文档、此设计文档、
`COMPATIBILITY.md` 和 `docs/development/commands/README.md`。
- 改动 manifest、反回滚 floor、安装 marker 或事务语义时,必须同步维护
`internal::upgrade/` 的跨层测试;不能将验证或持久化错误静默降级。
38 changes: 19 additions & 19 deletions docs/development/gap/surface-registry.tsv
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
add add git-compatible COMPATIBILITY.md:120
commit -m git-compatible COMPATIBILITY.md:146
add add git-compatible COMPATIBILITY.md:121
commit -m git-compatible COMPATIBILITY.md:147
diff --check git-compatible docs/commands/diff.md#options
diff --exit-code git-compatible docs/commands/diff.md#options
diff --name-only git-compatible docs/commands/diff.md#options
Expand All @@ -22,7 +22,7 @@ diff-files --stat absent docs/commands/diff.md#options
diff-files --summary absent docs/commands/diff.md#options
diff-files -C absent docs/commands/diff.md#options
diff-files -s absent docs/commands/diff.md#options
diff-files porcelain flag surface intentionally-different COMPATIBILITY.md:166
diff-files porcelain flag surface intentionally-different COMPATIBILITY.md:167
diff-index --cached deferred docs/commands/diff-index.md#comparison-with-git
diff-index --exit-code absent docs/commands/diff.md#options
diff-index -C absent docs/commands/diff.md#options
Expand All @@ -34,19 +34,19 @@ diff-tree --name-only absent docs/commands/diff.md#options
diff-tree --stdin absent docs/commands/diff-tree.md#comparison-with-git
diff-tree -M absent docs/commands/diff.md#options
diff-tree -r absent docs/commands/diff-tree.md#comparison-with-git
format-patch --attach git-compatible COMPATIBILITY.md:160
format-patch --base git-compatible COMPATIBILITY.md:160
format-patch --cc git-compatible COMPATIBILITY.md:160
format-patch --in-reply-to git-compatible COMPATIBILITY.md:160
format-patch --inline git-compatible COMPATIBILITY.md:160
format-patch --signoff git-compatible COMPATIBILITY.md:160
format-patch --stdout git-compatible COMPATIBILITY.md:160
format-patch --to git-compatible COMPATIBILITY.md:160
format-patch -s git-compatible COMPATIBILITY.md:160
hash-object hash-object git-compatible COMPATIBILITY.md:175
mv mv git-compatible COMPATIBILITY.md:127
rev-parse rev-parse git-compatible COMPATIBILITY.md:153
status status git-compatible COMPATIBILITY.md:129
update-index --add git-compatible COMPATIBILITY.md:178
update-index --remove git-compatible COMPATIBILITY.md:178
write-tree write-tree git-compatible COMPATIBILITY.md:176
format-patch --attach git-compatible COMPATIBILITY.md:161
format-patch --base git-compatible COMPATIBILITY.md:161
format-patch --cc git-compatible COMPATIBILITY.md:161
format-patch --in-reply-to git-compatible COMPATIBILITY.md:161
format-patch --inline git-compatible COMPATIBILITY.md:161
format-patch --signoff git-compatible COMPATIBILITY.md:161
format-patch --stdout git-compatible COMPATIBILITY.md:161
format-patch --to git-compatible COMPATIBILITY.md:161
format-patch -s git-compatible COMPATIBILITY.md:161
hash-object hash-object git-compatible COMPATIBILITY.md:176
mv mv git-compatible COMPATIBILITY.md:128
rev-parse rev-parse git-compatible COMPATIBILITY.md:154
status status git-compatible COMPATIBILITY.md:130
update-index --add git-compatible COMPATIBILITY.md:179
update-index --remove git-compatible COMPATIBILITY.md:179
write-tree write-tree git-compatible COMPATIBILITY.md:177
12 changes: 11 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ DEFAULT_VERSION="v0.22.10"
# the environment. The PEM is the same key as the hex, in SubjectPublicKeyInfo
# form for `openssl pkeyutl` (kept in sync by the trusted_keys unit tests).
LIBRA_RELEASE_MANIFEST_KEY_ID="libra-release-1"
# shellcheck disable=SC2034 # Audited by Rust tests against the PEM and native trust table.
LIBRA_RELEASE_MANIFEST_PUBLIC_KEY_HEX="68aa00ea9358d455645010d811d40702b3f67cec4bdff52d3d4fb8107afaeed3"
LIBRA_RELEASE_MANIFEST_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAaKoA6pNY1FVkUBDYEdQHArP2fOxL3/UtPU+4EHr67tM=
Expand Down Expand Up @@ -799,7 +800,11 @@ verify_stable_manifest() {
published_at=$(json_string_field published_at "$head_file")
expires_at=$(json_string_field expires_at "$head_file")
min_key_generation=$(sed -n 's/.*"min_key_generation":\([0-9][0-9]*\).*/\1/p' "$head_file" | head -n1)
paused=$(sed -n 's/.*"paused":\(true\|false\).*/\1/p' "$head_file" | head -n1)
# Do not use BRE `\|` here: BSD sed treats it as a literal rather than
# alternation, which would leave `paused` empty and bypass a signed pause.
# The canonical grammar above fixes this field's surrounding shape; the
# explicit boolean check below still keeps extraction fail-closed.
paused=$(sed -n 's/.*"paused":\([^,]*\),"revoked_versions":.*/\1/p' "$head_file" | head -n1)

[ "$channel" = "stable" ] || { rm -rf "$work_dir"; error_exit "signed manifest channel '${channel:-?}' is not 'stable'" "verify" "refusing to install"; }
[ -n "$STABLE_VERSION" ] || { rm -rf "$work_dir"; error_exit "signed manifest carries no version" "verify" "refusing to install"; }
Expand Down Expand Up @@ -860,6 +865,11 @@ verify_stable_manifest() {
error_exit "signed manifest lifetime is outside the pinned key's validity window (published_at ${published_at}, expires_at ${expires_at})" "verify" \
"the signing key window ended or has not begun — re-download install.sh"
fi
if [ "$paused" != "true" ] && [ "$paused" != "false" ]; then
rm -rf "$work_dir"
error_exit "signed manifest paused field '${paused:-?}' is not boolean" "verify" \
"refusing to install — the payload field layout is not the release contract"
fi
if [ "$paused" = "true" ]; then
rm -rf "$work_dir"
error_exit "releases are PAUSED by the publisher (signed manifest paused=true)" "verify" \
Expand Down
41 changes: 31 additions & 10 deletions src/internal/upgrade/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,16 +389,41 @@ mod unix_impl {
)
}

/// Open the floors micro-lock, retrying only Darwin's transient
/// `ENOENT` during the concurrent first-create window. Once one
/// contender creates the regular lock file, the next fd-relative
/// no-follow open observes and locks that same file.
fn open_floors_lock_file(&self) -> Result<std::fs::File, InstallDirError> {
const CREATE_RACE_RETRIES: u32 = 4;
for attempt in 0..CREATE_RACE_RETRIES {
match self.openat(
FLOORS_LOCK_FILE_NAME,
libc::O_RDWR | libc::O_CREAT,
0o600 as libc::c_int,
) {
Ok(file) => return Ok(file),
Err(InstallDirError::Io { ref detail, .. })
if attempt + 1 < CREATE_RACE_RETRIES
&& (detail.contains("No such file")
|| detail.contains("(os error 2)")) =>
{
std::thread::yield_now();
}
Err(err) => return Err(err),
}
}
Err(InstallDirError::Io {
name: FLOORS_LOCK_FILE_NAME.to_string(),
detail: "floors lock creation retry loop ended unexpectedly".into(),
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Floors lock retry is too weak

Medium Severity

open_floors_lock_file retries Darwin's first-create ENOENT only four times and only calls yield_now, so it never waits for the winning creator to land. Concurrent first-time floor recording can still fail to open .libra-upgrade-floors.lock, which matches the remaining concurrent_floor_recorders_serialize_and_never_regress failure.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ea585f4. Configure here.


/// Blocking floors micro-lock: kernel-queued, so unlike repeated
/// non-blocking probes it cannot be starved by a stream of short-lived
/// holders. Callers bound the wait externally (worker thread +
/// timeout) because flock itself has none.
pub fn lock_floors_blocking(&self) -> Result<UpgradeLock, InstallDirError> {
let file = self.openat(
FLOORS_LOCK_FILE_NAME,
libc::O_RDWR | libc::O_CREAT,
0o600 as libc::c_int,
)?;
let file = self.open_floors_lock_file()?;
// SAFETY: flock on an owned fd.
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
if rc != 0 {
Expand All @@ -415,11 +440,7 @@ mod unix_impl {
/// holders only perform one atomic read-merge-write, so contention
/// clears in milliseconds unless a holder is externally stalled.
pub fn try_lock_floors(&self) -> Result<Option<UpgradeLock>, InstallDirError> {
let file = self.openat(
FLOORS_LOCK_FILE_NAME,
libc::O_RDWR | libc::O_CREAT,
0o600 as libc::c_int,
)?;
let file = self.open_floors_lock_file()?;
// SAFETY: flock on an owned fd.
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if rc != 0 {
Expand Down
Loading