Release v1.126.0

This commit is contained in:
coso
2026-08-12 21:23:20 +08:00
parent b48569b83b
commit 979950a989
16 changed files with 480 additions and 80 deletions
+5 -1
View File
@@ -81,10 +81,14 @@ jobs:
cache-dependency-path: "pnpm-lock.yaml"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: x86_64-pc-windows-msvc
- name: Configure Windows MSVC linker
shell: pwsh
run: ./scripts/lib/windows-msvc-linker.ps1 -Target x86_64-pc-windows-msvc
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
+13 -6
View File
@@ -218,7 +218,7 @@ jobs:
cache-dependency-path: "pnpm-lock.yaml"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
@@ -257,7 +257,7 @@ jobs:
node-version: "22"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
@@ -288,7 +288,7 @@ jobs:
node-version: "22"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
@@ -297,6 +297,9 @@ jobs:
shared-key: quality-rust-full
cache-on-failure: true
- name: Configure sandboxed rusty_v8 artifacts
run: node scripts/lib/rusty-v8-artifacts.mjs --github-env
- name: Prepare sherpa-onnx runtime
shell: bash
run: |
@@ -330,7 +333,11 @@ jobs:
node-version: "22"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- name: Configure Windows MSVC linker
shell: pwsh
run: ./scripts/lib/windows-msvc-linker.ps1 -Target x86_64-pc-windows-msvc
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
@@ -342,8 +349,8 @@ jobs:
- name: Configure sandboxed rusty_v8 artifacts
run: node scripts/lib/rusty-v8-artifacts.mjs --github-env
- name: Check Windows app-server and code-mode-host sidecars
run: cargo check --manifest-path lime-rs/Cargo.toml -p app-server --bin app-server -p tool-runtime --bin code-mode-host
- name: Build Windows app-server and code-mode-host sidecars
run: cargo build --manifest-path lime-rs/Cargo.toml --target x86_64-pc-windows-msvc -p app-server --bin app-server -p tool-runtime --bin code-mode-host
- name: Test Windows shell runtime fallback
run: cargo test --manifest-path lime-rs/Cargo.toml -p lime-agent --test windows_shell_runtime -- --test-threads=1
+13 -2
View File
@@ -78,6 +78,7 @@ jobs:
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release edit "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--target "$TARGET_REF" \
--title "Lime $TAG" \
--notes-file "$NOTES_FILE"
echo "Release $TAG already exists; notes refreshed"
@@ -143,10 +144,15 @@ jobs:
cache-dependency-path: "pnpm-lock.yaml"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ matrix.target }}
- name: Configure Windows MSVC linker
if: matrix.host_platform == 'win32'
shell: pwsh
run: ./scripts/lib/windows-msvc-linker.ps1 -Target x86_64-pc-windows-msvc
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
@@ -691,10 +697,15 @@ jobs:
node-version: "22"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ matrix.target }}
- name: Configure Windows MSVC linker
if: startsWith(matrix.target, 'x86_64-pc-windows-msvc')
shell: pwsh
run: ./scripts/lib/windows-msvc-linker.ps1 -Target x86_64-pc-windows-msvc
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
@@ -5137,3 +5137,13 @@ resource verifier 校验双 binary。专项 `npm run smoke:code-mode-electron-ga
`44203`。17 项 Gate B assertion 全通过,custom exec 两次 Responses 回采样、canonical `dynamicToolCall`、GUI final text、
IPC trace 均成立,production mock/invoke/console/page/provider error 为零。该证据不冒充 live OpenAI 或 Windows packaged
parity;后者由 release Windows runner 继续验证。
统一本地门禁收尾:`npm run verify:local` 首轮暴露三项既存事实源偏差。`useAgentChat` provider sync 回归在发送前
settings RPC 永久 pending,却等待 `sendMessage()` 完成后才释放 promise,形成测试自锁;测试现改为先观察 turn 未提交,
释放门禁后再等待发送完成。DevBridge 已退役 `pluginUiRuntime/start` 按 current 通用 App Server read policy 使用
`30000ms`,旧断言仍保留 `5000ms`,现只同步事实期望。App Server 同时直接拥有 `RuntimeProviderProtocol` 映射,违反
`model-provider` capability ownercanonical `ProtocolKind -> RuntimeProviderProtocol -> ProviderCapabilities` 转换现统一收回
`model-provider::ProviderCapabilities::from_route`,未放宽治理白名单,`runtime.rs` 通过复用 current imports 保持原行数预算。
修复后 `npm test -- --resume` 从第 50 批续跑至第 120 批全部通过;随后 fresh `npm run verify:local` 全绿,覆盖 120 个
Vitest smart batches、App Server client `303` checks、13 个 current/反向依赖 Rust crate、真实 Electron/App Server GUI
smoke、lint、typecheck、i18n、scripts/docs/version 门禁。Rust 仅保留既有 App Server test helper `dead_code` warning。
+18 -5
View File
@@ -31,18 +31,31 @@ Electron 双 sidecar 资源链、协议/GUI 投影以及对应治理文档;完
- `npm run verify:app-version`:通过,版本事实源一致为 `1.126.0`
- `npm run typecheck`:通过(发布 metadata 更新后复跑)。
- `npm run test:contracts`:通过;App Server client `301` 项检查及 command、harness、modality、scripts、Electron release workflow、docs boundary 子门禁全部通过。
- `npm run test:contracts`:通过;App Server client `309` 项检查及 command、harness、modality、scripts、Electron release workflow、docs boundary 子门禁全部通过。
- `npm run test:rust:related -- lime-rs/crates/agent-runtime lime-rs/crates/agent lime-rs/crates/app-server lime-rs/crates/model-provider lime-rs/crates/runtime-core lime-rs/crates/tool-runtime lime-rs/crates/services`:通过;相关 owner 与反向依赖 crate 单测无失败,存在一个既有测试辅助函数 dead-code warning。
- `npm run smoke:agent-runtime-current-fixture`:通过;history/cache、turn terminal、approval、steer、Plan、Skills、MCP、媒体、Workbench 等 current Electron fixture 闭环通过,`liveProviderUsed=false`
- `npm run verify:gui-smoke`:通过;真实 Electron/App Server `1.126.0` 初始化、工作台 reload 与 memory settings 可见,evidence result 为 `pass`
- `npm run governance:legacy-report`:通过;扫描 `2120` 个源码文件,分类漂移 `0`、边界违规 `0`
- standalone Code Mode Cargo check:通过,无 warning。
- Code Mode process Rust tests`4/4` 通过。
- sidecar/assets/fixture/Gate B script tests`50/50` 通过。
- Code Mode process Rust tests`6/6` 通过。
- sidecar/assets/fixture/package/Gate B script tests`56/56` 通过。
- `npm run electron:build:app-server-assets`:通过;macOS arm64 双 sidecar 为 `0755`manifest 双 SHA 复算一致。
- `npm run smoke:code-mode-electron-gate-b`:通过;thread `019ff39f-2ff2-7bb3-b579-cf1dc86ac042`Electron/App Server/
host PID 为 `10485/10494/10926`host parent PID 为 `10494`17 项 assertion 全通过。
- `npm run smoke:code-mode-electron-gate-b`:通过;最新 rerun thread `019ff3ca-7f26-71d2-be81-6a16b7895515`Electron/App Server/
host PID 为 `44199/44203/44521`host parent PID 为 `44203`17 项 assertion 全通过。
- `cargo fmt --all -- --check``git diff --check`:通过。
- `npm run verify:local`:通过;fresh 120 个 Vitest smart batches、contracts、13-crate changed-scope Rust、真实 Electron/App Server GUI smoke、lint、typecheck、i18n、scripts/docs/version 门禁均通过;Rust 仅保留既有 App Server test helper `dead_code` warning。
## 已公开 Release 修复轮
- 首次公开提交为 `b48569b83b457eda1a9dac043d5ad4d470fc9e86`Quality run `31563417376` 与 Release run `31563421104` 失败,GitHub Release 已创建但无 assets。用户明确要求保留 `v1.126.0`,不改发补丁版本。
- Frontend Full:修复 provider settings Promise 未释放前等待 `sendMessage()` 导致的测试超时;`useAgentChat` 主套件 `186/186` 通过。
- Rust Full:直接 workspace 测试前显式配置已校验 sandbox `rusty_v8` artifact,避免回退到不存在的上游 archive。
- Windows:根工具链与目标 workflow 固定 Rust `1.95.0`,复用 `scripts/lib/windows-msvc-linker.ps1` 导出完整 MSVC/UCRT 环境并选择 `rust-lld.exe`Quality 将双 sidecar 从 `cargo check` 提升为真实 `cargo build` 链接,不使用 `/FORCE:MULTIPLE`
- macOS package`app-server``code-mode-host` 统一执行“manifest SHA 一致,或签名后通过 `codesign --verify --strict`”校验;非 macOS 和无效签名仍 fail closed,资源回归 `17/17` 通过。
- 修复后本地门禁:`npm run verify:app-version``npm run typecheck``npm run test:contracts`、Rust related、`cargo fmt --all -- --check`、workflow YAML parse、Prettier、scripts governance 与 `git diff --check` 全通过。
- 修复后 Gate B`npm run verify:gui-smoke` 通过;run id `standalone-shell-01-20260812112209-14292`,真实 Electron/preload/IPC/App Server `1.126.0`、Workbench reload、Memory settings、21 项 assertion 全通过,mock/legacy hit 均为 `0`
- 本地无法证明 Windows MSVC 真链接与三平台 Forge 发布;重建 `v1.126.0` 后必须监控新的 Quality/Release run 到终态,并复核 Release assets。
- 已有 GitHub Release 的 workflow 分支会同步刷新 `target_commitish`、标题与 release notes,避免同名 tag 重建后页面元数据仍指向旧提交。
## 待执行门禁
+8 -9
View File
@@ -128,6 +128,9 @@ pub use crate::file_checkpoint_snapshot::FileCheckpointSnapshotSaveRequest;
pub use crate::file_checkpoint_snapshot::FileCheckpointSnapshotStore;
pub use crate::file_checkpoint_snapshot::FilesystemFileCheckpointSnapshotStore;
pub use crate::file_checkpoint_snapshot::NoopFileCheckpointSnapshotStore;
use agent_runtime::{
code_mode::RuntimeCodeModeServiceFactory, session_loop::RuntimeSessionRegistry,
};
pub use app_data::AppDataSource;
pub use app_data::AutomationManagementAppDataSource;
pub use app_data::AutomationOverviewAppDataSource;
@@ -481,7 +484,7 @@ pub trait ExecutionBackend: Send + Sync {
#[derive(Clone)]
pub struct RuntimeCore {
pub(in crate::runtime) state: Arc<Mutex<RuntimeCoreState>>,
pub(in crate::runtime) session_loops: agent_runtime::session_loop::RuntimeSessionRegistry,
pub(in crate::runtime) session_loops: RuntimeSessionRegistry,
pub(in crate::runtime) turn_driver_completions: turn_execution::RuntimeTurnDriverCompletions,
mailbox_trigger_flights: agent_mailbox_delivery::MailboxTriggerFlights,
route_recovery: model_providers::RouteRecoveryCoordinator,
@@ -513,7 +516,7 @@ pub struct RuntimeCoreEventAppender {
event_log_writer: Option<Arc<EventLogWriter>>,
trace_event_writer: Option<Arc<TraceEventWriter>>,
projection_store: Option<Arc<ProjectionStore>>,
session_loops: agent_runtime::session_loop::RuntimeSessionRegistry,
session_loops: RuntimeSessionRegistry,
}
#[derive(Debug, Default)]
@@ -546,12 +549,8 @@ impl Default for RuntimeCore {
}
impl RuntimeCore {
pub(crate) fn with_code_mode_factory(
mut self,
factory: agent_runtime::code_mode::RuntimeCodeModeServiceFactory,
) -> Self {
self.session_loops =
agent_runtime::session_loop::RuntimeSessionRegistry::with_code_mode(factory);
pub(crate) fn with_code_mode_factory(mut self, factory: RuntimeCodeModeServiceFactory) -> Self {
self.session_loops = RuntimeSessionRegistry::with_code_mode(factory);
self
}
@@ -594,7 +593,7 @@ impl RuntimeCore {
) -> Self {
Self {
state: Arc::new(Mutex::new(RuntimeCoreState::default())),
session_loops: agent_runtime::session_loop::RuntimeSessionRegistry::default(),
session_loops: RuntimeSessionRegistry::default(),
turn_driver_completions: turn_execution::RuntimeTurnDriverCompletions::default(),
mailbox_trigger_flights: agent_mailbox_delivery::MailboxTriggerFlights::default(),
route_recovery: model_providers::RouteRecoveryCoordinator::default(),
@@ -14,7 +14,7 @@ use lime_agent::{
};
use lime_core::database::dao::api_key_provider::ProviderWithKeys;
use model_provider::provider_capabilities::ProviderCapabilities;
use model_provider::runtime_provider::{RuntimeProviderAuth, RuntimeProviderProtocol};
use model_provider::runtime_provider::RuntimeProviderAuth;
use model_provider::{ModelProviderProtocol, ModelRoute};
use serde_json::{json, Value};
@@ -107,16 +107,11 @@ fn intersect_resolved_provider_capabilities(
provider.map(|provider| provider.provider.effective_provider_type().to_string())
})
.unwrap_or_else(|| selection.provider.clone());
let provider_capabilities =
RuntimeProviderProtocol::from_route_protocol(&resolved_route.protocol)
.map(|protocol| {
ProviderCapabilities::from_resolved_route(
&provider_name,
protocol,
resolved_route.endpoint.base_url.as_deref(),
)
})
.unwrap_or(ProviderCapabilities::NONE);
let provider_capabilities = ProviderCapabilities::from_route(
&provider_name,
&resolved_route.protocol,
resolved_route.endpoint.base_url.as_deref(),
);
if provider_capabilities.custom_tools {
return;
@@ -1,4 +1,5 @@
use crate::runtime_provider::{RuntimeProviderConfig, RuntimeProviderProtocol};
use app_server_protocol::ProtocolKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProviderCapabilities {
@@ -25,7 +26,17 @@ impl ProviderCapabilities {
Some(Self::from_resolved_route(provider_type, protocol, base_url))
}
pub fn from_resolved_route(
pub fn from_route(
provider_name: &str,
protocol: &ProtocolKind,
base_url: Option<&str>,
) -> Self {
RuntimeProviderProtocol::from_route_protocol(protocol).map_or(Self::NONE, |protocol| {
Self::from_resolved_route(provider_name, protocol, base_url)
})
}
fn from_resolved_route(
provider_name: &str,
protocol: RuntimeProviderProtocol,
base_url: Option<&str>,
@@ -142,33 +153,33 @@ mod tests {
Some(ProviderCapabilities::NONE)
);
assert_eq!(
ProviderCapabilities::from_resolved_route(
ProviderCapabilities::from_route(
"openai",
RuntimeProviderProtocol::Responses,
&ProtocolKind::OpenaiResponses,
Some("https://api.openai.com/v1"),
),
expected.expect("official Responses capability")
);
assert_eq!(
ProviderCapabilities::from_resolved_route(
ProviderCapabilities::from_route(
"openai",
RuntimeProviderProtocol::ChatCompletions,
&ProtocolKind::OpenaiChat,
Some("https://api.openai.com/v1"),
),
ProviderCapabilities::NONE
);
assert_eq!(
ProviderCapabilities::from_resolved_route(
ProviderCapabilities::from_route(
"gateway",
RuntimeProviderProtocol::Responses,
&ProtocolKind::OpenaiResponses,
Some("https://api.openai.com/v1"),
),
ProviderCapabilities::NONE
);
assert_eq!(
ProviderCapabilities::from_resolved_route(
ProviderCapabilities::from_route(
"azure-openai",
RuntimeProviderProtocol::AzureResponses,
&ProtocolKind::OpenaiResponses,
Some("https://resource.openai.azure.com"),
),
ProviderCapabilities::NONE
+3
View File
@@ -0,0 +1,3 @@
[toolchain]
channel = "1.95.0"
components = ["clippy", "rustfmt", "rust-src"]
+79 -7
View File
@@ -7897,25 +7897,97 @@ const checks = [
],
},
{
name: "Windows workflows build both App Server sidecars with verified V8 inputs",
files: [
".github/workflows/build-windows-test.yml",
".github/workflows/quality.yml",
],
name: "Windows test workflow builds both App Server sidecars with pinned Rust and shared linker inputs",
file: ".github/workflows/build-windows-test.yml",
snippets: [
"dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0",
"Configure Windows MSVC linker",
"./scripts/lib/windows-msvc-linker.ps1 -Target x86_64-pc-windows-msvc",
"Configure sandboxed rusty_v8 artifacts",
"node scripts/lib/rusty-v8-artifacts.mjs --github-env",
"code-mode-host sidecars",
],
},
{
name: "Windows quality explicitly checks both App Server sidecar binaries",
name: "Windows quality links both App Server sidecar binaries with pinned Rust and shared inputs",
file: ".github/workflows/quality.yml",
snippets: [
"Check Windows app-server and code-mode-host sidecars",
"dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0",
"Configure Windows MSVC linker",
"./scripts/lib/windows-msvc-linker.ps1 -Target x86_64-pc-windows-msvc",
"Configure sandboxed rusty_v8 artifacts",
"node scripts/lib/rusty-v8-artifacts.mjs --github-env",
"Build Windows app-server and code-mode-host sidecars",
"cargo build --manifest-path lime-rs/Cargo.toml --target x86_64-pc-windows-msvc -p app-server --bin app-server -p tool-runtime --bin code-mode-host",
],
absentSnippets: [
"cargo check --manifest-path lime-rs/Cargo.toml -p app-server --bin app-server -p tool-runtime --bin code-mode-host",
],
},
{
name: "Release workflow uses pinned Rust and shared Windows linker inputs",
file: ".github/workflows/release.yml",
snippets: [
"dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0",
"Configure Windows MSVC linker",
"./scripts/lib/windows-msvc-linker.ps1 -Target x86_64-pc-windows-msvc",
],
absentSnippets: ["dtolnay/rust-toolchain@stable"],
},
{
name: "Existing GitHub release is retargeted when a stable tag is rebuilt",
file: ".github/workflows/release.yml",
snippets: [
'TARGET_REF="${{ github.event.inputs.source_ref || github.sha }}"',
'gh release edit "$TAG"',
'--target "$TARGET_REF"',
'--notes-file "$NOTES_FILE"',
],
},
{
name: "Rust Full configures sandboxed V8 artifacts before testing the workspace",
file: ".github/workflows/quality.yml",
snippets: [
"quality-rust-full",
"Configure sandboxed rusty_v8 artifacts",
"node scripts/lib/rusty-v8-artifacts.mjs --github-env",
"npm run test:rust",
],
},
{
name: "Repository Rust toolchain stays pinned to the Windows V8 release toolchain",
file: "rust-toolchain.toml",
snippets: [
'channel = "1.95.0"',
'components = ["clippy", "rustfmt", "rust-src"]',
],
},
{
name: "Windows MSVC linker setup exports the complete SDK environment and rust-lld",
file: "scripts/lib/windows-msvc-linker.ps1",
snippets: [
"VsDevCmd.bat",
"-arch=x64 -host_arch=x64",
'"INCLUDE"',
'"LIB"',
'"LIBPATH"',
'"UCRTVersion"',
'"WindowsSdkDir"',
'throw "VsDevCmd.bat did not export $RequiredVariable"',
"rust-lld.exe",
'"CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER=$Linker"',
],
absentSnippets: ["/FORCE:MULTIPLE"],
},
{
name: "Rust changed-scope selection treats the root toolchain as workspace-wide",
file: "scripts/lib/rust-test-scope-core.mjs",
snippets: [
'relPath === "rust-toolchain.toml"',
'relPath === "rust-toolchain"',
'relPath === "lime-rs/rust-toolchain.toml"',
],
},
{
name: "Retired App Server agent backend crate stays deleted",
file: "lime-rs/Cargo.lock",
+48 -23
View File
@@ -119,9 +119,9 @@ function sha256(filePath) {
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
}
function isMacCodeSigned(filePath) {
function isMacCodeSigned(filePath, execFileSyncImpl = execFileSync) {
try {
execFileSync("codesign", ["--verify", "--strict", filePath], {
execFileSyncImpl("codesign", ["--verify", "--strict", filePath], {
stdio: "ignore",
});
return true;
@@ -130,13 +130,41 @@ function isMacCodeSigned(filePath) {
}
}
function verifySidecarIntegrity(
filePath,
expectedSha256,
{ platform, label, execFileSyncImpl = execFileSync },
) {
if (!/^[a-f0-9]{64}$/u.test(expectedSha256 ?? "")) {
throw new Error(`${label} manifest sha256 is invalid`);
}
const packagedSha256 = sha256(filePath);
const sha256Matches = expectedSha256 === packagedSha256;
const signedMacSidecar =
platform === "darwin" &&
!sha256Matches &&
isMacCodeSigned(filePath, execFileSyncImpl);
if (!sha256Matches && !signedMacSidecar) {
throw new Error(`${label} sha256 mismatch: ${filePath}`);
}
return {
manifest: expectedSha256,
packaged: packagedSha256,
matches: sha256Matches,
acceptedBecause: signedMacSidecar ? "macos-signed-sidecar" : "sha256",
};
}
function assertFile(filePath, label) {
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
throw new Error(`${label} is missing: ${filePath}`);
}
}
function verifyResourceRoot(root, { platform, arch }) {
export function verifyResourceRoot(
root,
{ platform, arch, execFileSyncImpl = execFileSync },
) {
const manifestPath = path.join(root, "app-server.release.json");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
const key = platformKey(platform, arch);
@@ -156,19 +184,20 @@ function verifyResourceRoot(root, { platform, arch }) {
codeModeHostBinaryName(platform),
);
assertFile(codeModeHostPath, "code-mode host sidecar");
const codeModeHostSha256 = sha256(codeModeHostPath);
if (artifact.codeModeHostSha256 !== codeModeHostSha256) {
throw new Error(
`code-mode host sidecar sha256 mismatch: ${codeModeHostPath}`,
);
}
const sidecarSha256 = sha256(sidecarPath);
const sha256Matches = artifact.sha256 === sidecarSha256;
const signedMacSidecar =
platform === "darwin" && !sha256Matches && isMacCodeSigned(sidecarPath);
if (!sha256Matches && !signedMacSidecar) {
throw new Error(`app-server sidecar sha256 mismatch: ${sidecarPath}`);
}
const codeModeHost = verifySidecarIntegrity(
codeModeHostPath,
artifact.codeModeHostSha256,
{
platform,
label: "code-mode host sidecar",
execFileSyncImpl,
},
);
const appServer = verifySidecarIntegrity(sidecarPath, artifact.sha256, {
platform,
label: "app-server sidecar",
execFileSyncImpl,
});
for (const name of [
"icon.png",
@@ -190,13 +219,9 @@ function verifyResourceRoot(root, { platform, arch }) {
resourceRoot: root,
sidecarPath,
codeModeHostPath,
codeModeHostSha256,
sha256: {
manifest: artifact.sha256,
packaged: sidecarSha256,
matches: sha256Matches,
acceptedBecause: signedMacSidecar ? "macos-signed-sidecar" : "sha256",
},
codeModeHostSha256: codeModeHost.packaged,
codeModeHostIntegrity: codeModeHost,
sha256: appServer,
};
}
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
@@ -8,6 +9,7 @@ import {
verifyElectronRuntimeBundles,
verifyMacAppIdentity,
verifyMacAppSignatures,
verifyResourceRoot,
} from "./verify-package-resources.mjs";
const tmpRoots = [];
@@ -46,6 +48,58 @@ function createRuntimeBundleRoot({ main, preload }) {
return root;
}
function sha256(content) {
return createHash("sha256").update(content).digest("hex");
}
function createResourceRoot({
appServer = "signed app-server",
codeModeHost = "signed code-mode-host",
manifestAppServer = "unsigned app-server",
manifestCodeModeHost = "unsigned code-mode-host",
platformKey = "darwin-arm64",
} = {}) {
const root = mkdtempSync(path.join(tmpdir(), "lime-electron-resources-"));
tmpRoots.push(root);
const sidecarDir = path.join(root, "app-server", platformKey);
const desktopAssetsDir = path.join(root, "desktop-assets");
mkdirSync(sidecarDir, { recursive: true });
mkdirSync(desktopAssetsDir, { recursive: true });
const executableSuffix = platformKey.startsWith("win32-") ? ".exe" : "";
writeFileSync(
path.join(sidecarDir, `app-server${executableSuffix}`),
appServer,
);
writeFileSync(
path.join(sidecarDir, `code-mode-host${executableSuffix}`),
codeModeHost,
);
for (const name of [
"icon.png",
"trayTemplate.png",
"trayTemplate@2x.png",
"tray-running.png",
"tray-stopped.png",
"tray-warning.png",
"tray-error.png",
]) {
writeFileSync(path.join(desktopAssetsDir, name), name);
}
writeFileSync(
path.join(root, "app-server.release.json"),
JSON.stringify({
artifacts: [
{
platform: platformKey,
sha256: sha256(manifestAppServer),
codeModeHostSha256: sha256(manifestCodeModeHost),
},
],
}),
);
return root;
}
function buildInfoPlist(entries) {
return [
'<?xml version="1.0" encoding="UTF-8"?>',
@@ -124,6 +178,103 @@ describe("verify-electron-package-resources runtime bundles", () => {
});
});
describe("verify-electron-package-resources sidecar integrity", () => {
it("两个 sidecar 哈希一致时不调用 codesign", () => {
const root = createResourceRoot({
appServer: "app-server",
codeModeHost: "code-mode-host",
manifestAppServer: "app-server",
manifestCodeModeHost: "code-mode-host",
});
const result = verifyResourceRoot(root, {
platform: "darwin",
arch: "arm64",
execFileSyncImpl: () => {
throw new Error("unexpected codesign call");
},
});
expect(result.sha256.acceptedBecause).toBe("sha256");
expect(result.codeModeHostIntegrity.acceptedBecause).toBe("sha256");
});
it("接受两个经过严格 codesign 验证的 macOS sidecar", () => {
const root = createResourceRoot();
const calls = [];
const result = verifyResourceRoot(root, {
platform: "darwin",
arch: "arm64",
execFileSyncImpl: (...args) => calls.push(args),
});
expect(result.sha256.acceptedBecause).toBe("macos-signed-sidecar");
expect(result.codeModeHostIntegrity.acceptedBecause).toBe(
"macos-signed-sidecar",
);
expect(calls).toEqual([
[
"codesign",
["--verify", "--strict", expect.stringMatching(/code-mode-host$/u)],
{ stdio: "ignore" },
],
[
"codesign",
["--verify", "--strict", expect.stringMatching(/app-server$/u)],
{ stdio: "ignore" },
],
]);
});
it("拒绝哈希变化且严格 codesign 失败的 macOS code-mode host", () => {
const root = createResourceRoot();
expect(() =>
verifyResourceRoot(root, {
platform: "darwin",
arch: "arm64",
execFileSyncImpl: () => {
throw new Error("invalid signature");
},
}),
).toThrow(/code-mode host sidecar sha256 mismatch/u);
});
it("拒绝哈希变化且严格 codesign 失败的 macOS app-server", () => {
const root = createResourceRoot({
codeModeHost: "code-mode-host",
manifestCodeModeHost: "code-mode-host",
});
expect(() =>
verifyResourceRoot(root, {
platform: "darwin",
arch: "arm64",
execFileSyncImpl: () => {
throw new Error("invalid signature");
},
}),
).toThrow(/app-server sidecar sha256 mismatch/u);
});
it("非 macOS sidecar 哈希变化时不接受签名例外", () => {
const root = createResourceRoot({ platformKey: "win32-x64" });
let codesignCalled = false;
expect(() =>
verifyResourceRoot(root, {
platform: "win32",
arch: "x64",
execFileSyncImpl: () => {
codesignCalled = true;
},
}),
).toThrow(/code-mode host sidecar sha256 mismatch/u);
expect(codesignCalled).toBe(false);
});
});
describe("verify-electron-package-resources macOS app identity", () => {
it("接受完整 Lime macOS app identity", () => {
const root = createPackageRoot(cleanLimeInfoPlist());
+7 -5
View File
@@ -27,6 +27,8 @@ function normalizeRepoPath(repoRoot, inputPath) {
function isWorkspaceWideRustPath(relPath) {
return (
relPath === "rust-toolchain.toml" ||
relPath === "rust-toolchain" ||
relPath === "lime-rs" ||
relPath === "lime-rs/" ||
relPath === "lime-rs/Cargo.toml" ||
@@ -114,17 +116,17 @@ export function resolveRustPathSelection(
continue;
}
if (!relPath.startsWith("lime-rs")) {
skippedPaths.push(relPath);
continue;
}
if (isWorkspaceWideRustPath(relPath)) {
rustPaths.push(relPath);
workspaceReasons.push(relPath);
continue;
}
if (!relPath.startsWith("lime-rs")) {
skippedPaths.push(relPath);
continue;
}
if (isVendoredRustPath(relPath)) {
rustPaths.push(relPath);
workspaceReasons.push(`${relPath} (vendored Rust dependency)`);
+88
View File
@@ -0,0 +1,88 @@
param(
[Parameter(Mandatory = $true)]
[string]$Target
)
$ErrorActionPreference = "Stop"
if ($Target -ne "x86_64-pc-windows-msvc") {
throw "Unsupported Windows MSVC target: $Target"
}
if ([string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) {
throw "GITHUB_ENV is required"
}
$VsWhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
if (-not (Test-Path $VsWhere)) {
throw "vswhere.exe not found at $VsWhere"
}
$RequiredComponent = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
$InstallPath = & $VsWhere -latest -products * -requires $RequiredComponent -property installationPath 2>$null
if (-not $InstallPath) {
throw "Visual Studio with $RequiredComponent was not found"
}
$VsDevCmd = Join-Path $InstallPath "Common7\Tools\VsDevCmd.bat"
if (-not (Test-Path $VsDevCmd)) {
throw "VsDevCmd.bat not found at $VsDevCmd"
}
$VariablesToExport = @(
"INCLUDE",
"LIB",
"LIBPATH",
"PATH",
"UCRTVersion",
"UniversalCRTSdkDir",
"VCINSTALLDIR",
"VCToolsInstallDir",
"WindowsLibPath",
"WindowsSdkBinPath",
"WindowsSdkDir",
"WindowsSDKLibVersion",
"WindowsSDKVersion"
)
$EnvironmentLines = & cmd.exe /c ('"{0}" -no_logo -arch=x64 -host_arch=x64 >nul && set' -f $VsDevCmd)
$VsDevCmdExitCode = $LASTEXITCODE
if ($VsDevCmdExitCode -ne 0) {
throw "VsDevCmd.bat failed with exit code $VsDevCmdExitCode"
}
$ExportedVariables = @{}
foreach ($Line in $EnvironmentLines) {
if ($Line -notmatch "^(.*?)=(.*)$") {
continue
}
$Name = $Matches[1]
$Value = $Matches[2]
if ($VariablesToExport -contains $Name) {
if ($Name -ieq "Path") {
$Name = "PATH"
}
$ExportedVariables[$Name] = $Value
"$Name=$Value" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
}
}
foreach ($RequiredVariable in @("INCLUDE", "LIB", "LIBPATH", "PATH", "UCRTVersion", "VCToolsInstallDir", "WindowsSdkDir")) {
if ([string]::IsNullOrWhiteSpace($ExportedVariables[$RequiredVariable])) {
throw "VsDevCmd.bat did not export $RequiredVariable"
}
}
$RustSysroot = (& rustc --print sysroot).Trim()
$RustHostLine = & rustc -vV | Select-String "^host: "
if (-not $RustHostLine) {
throw "rustc did not report a host target"
}
$RustHost = $RustHostLine.Line.Substring(6).Trim()
$Linker = Join-Path $RustSysroot "lib\rustlib\$RustHost\bin\rust-lld.exe"
if (-not (Test-Path $Linker)) {
throw "rust-lld.exe not found at $Linker"
}
Write-Output "Using Windows linker: $Linker"
"CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER=$Linker" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
@@ -189,8 +189,9 @@ describe("useAgentChat 兼容接口 - provider sync", () => {
});
await flushEffects();
let sendPromise: Promise<void>;
await act(async () => {
await harness
sendPromise = harness
.getValue()
.sendMessage(
"切换到同 provider 的另一个模型,但 session 还没同步完",
@@ -200,6 +201,7 @@ describe("useAgentChat 兼容接口 - provider sync", () => {
false,
"react",
);
await Promise.resolve();
});
expect(mockUpdateAgentRuntimeThreadSettings).toHaveBeenCalledWith({
@@ -208,6 +210,13 @@ describe("useAgentChat 兼容接口 - provider sync", () => {
model: nextModel,
effort: null,
});
expect(mockSubmitAgentRuntimeTurn).not.toHaveBeenCalled();
await act(async () => {
(resolveProviderSync as (() => void) | null)?.();
await sendPromise!;
});
expect(mockSubmitAgentRuntimeTurn).toHaveBeenCalledTimes(1);
expect(getSubmittedTurnStart()).not.toHaveProperty("provider");
expect(getSubmittedTurnStart()).not.toHaveProperty("model");
+1 -1
View File
@@ -818,7 +818,7 @@ describe("http-client", () => {
],
},
}),
).toBe(5000);
).toBe(30000);
expect(
resolveBridgeRequestTimeoutMs("app_server_handle_json_lines", {
request: {