From 223fbbc04cd9f6ecb825fab3aa65d339aab2f568 Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:29:39 +0800 Subject: [PATCH 01/12] feat(tooling): resolve Claude/Grok latest from npm instead of bundled manifests Stop compiling reviewed version/hash JSON. Discover CLIs from login/process PATH and fetch live latest so install/update no longer depends on a hand-maintained pin. Co-authored-by: Cursor --- .trellis/spec/backend/claude-code-cli.md | 84 +++- .../spec/backend/external-agent-lifecycle.md | 57 ++- .trellis/spec/backend/health.md | 6 +- .trellis/spec/frontend/agent-directory.md | 15 + src-tauri/src/services/tooling.rs | 102 ++--- src-tauri/src/services/tooling/claude.rs | 68 ++-- .../services/tooling/claude_npm_manifest.json | 12 - src-tauri/src/services/tooling/grok.rs | 75 ++-- src-tauri/src/services/tooling/grok_npm.rs | 380 +++++++++++++----- .../services/tooling/grok_npm_manifest.json | 13 - src-tauri/src/services/tooling/versions.rs | 27 +- 11 files changed, 543 insertions(+), 296 deletions(-) delete mode 100644 src-tauri/src/services/tooling/claude_npm_manifest.json delete mode 100644 src-tauri/src/services/tooling/grok_npm_manifest.json diff --git a/.trellis/spec/backend/claude-code-cli.md b/.trellis/spec/backend/claude-code-cli.md index 30e860b8b..39f9785ca 100644 --- a/.trellis/spec/backend/claude-code-cli.md +++ b/.trellis/spec/backend/claude-code-cli.md @@ -22,8 +22,11 @@ get_tool_versions(["claude"]) -> ToolVersion OfficialNpmTool = Grok | Claude GrokNpmInstallPlan::npm_argv_for(tool) -> closed exact-version argv -grok_npm::claude_manifest() -> validated bundled manifest -grok_npm::registries_matching_manifest(manifest) -> reviewed registry list +grok_npm::resolve_published_manifest(tool) -> live /latest + platform integrity +grok_npm::registries_matching_manifest(manifest) -> registries whose hashes match +fetch_npm_latest_for_tool(package, tool, local) -> dist-tags.latest or /latest version +build_tool_search_paths(tool) -> login PATH + process PATH + product env +default_install(installs) -> PATH default, else the sole entry Windows: claude-tool --action observe|install|update --job-id --pipe UserHelperAction::ClaudeTool { action } -> independent wire identities 15–17 @@ -52,19 +55,48 @@ generic command execution capability is added. - Fresh install requires absence. Update requires one confirmed official npm installation and its actual global prefix. Native, Homebrew and other package-manager ownership is not silently converted to npm. A same/newer - installed version is not downgraded to the bundled version. - -### Reviewed package and shared mirror policy - -- Exact version and SHA-512 authority is - `tooling/claude_npm_manifest.json`, compiled into the product. It contains - `@anthropic-ai/claude-code` plus the reviewed Darwin/Windows x64/arm64 native - optional packages. Do not repeat version/hash literals in generic specs. -- The existing shared registry chain is Tencent, Huawei, npmmirror, npmjs. - Each candidate must return matching root and current-platform package name, + installed version is not downgraded to an older published version. + +### CLI discovery + +- macOS search directories are the login-shell PATH, the current process + PATH, `~/.local/bin`, and product env (`GROK_BIN_DIR` / `~/.grok/bin` for + Grok). Do not walk mise, nvm, fnm or Volta internal install trees, and do + not add per-manager adapters to make those trees visible. If the CLI is on + the user's PATH, that is enough. +- `is_mise_dispatcher` stays in the health filesystem probe. Enumerate must + not use it to drop PATH hits. +- `default_install` prefers the PATH-default entry. Several copies may exist; + that is not `tool_owner_unsupported` when one of them is PATH default. No + PATH default and more than one copy remains unsupported. +- Windows still uses the existing manager search plus the ordinary-user + helper; it does not copy the macOS env-only rule onto Alice's PATH. + +### Live package and shared mirror policy + +- Exact version and SHA-512 authority is the current npm `latest` document, + resolved at runtime. Prefer `registry.npmjs.org`; if that host is + unreachable, try the shared mainland chain (Tencent, Huawei, npmmirror). + The document supplies `@anthropic-ai/claude-code` version, `dist.integrity`, + and the current Darwin/Windows x64/arm64 optional package version. Do not + compile a reviewed version/hash JSON into the product, and do not repeat + version/hash literals in generic specs. +- Directory `latest_version` display uses `fetch_npm_latest_for_tool`: npmjs + packument `dist-tags` first, then the same `/latest` document fallback. Claude + may consider the `next` tag only when the local version is already newer + than `latest`. Display and install may race; both must resolve live, never + a compiled pin. +- After the published version is known, each candidate in Tencent, Huawei, + npmmirror, npmjs must return matching root and current-platform package name, exact version and SHA-512. HTTPS-only, no redirects, per-request timeout and a streaming 1 MiB metadata limit apply. Missing/mismatching sources fail - closed; runtime does not install `latest`. + closed. npm argv is always `package@`; never + `package@latest`. A mainland `dist-tags.latest` (for example npmmirror) may + point at an older release; that is why argv never uses the `latest` tag. +- `default_install_command()` is a command-shape fixture + (`@xai-official/grok@1.2.3` plus the Tencent registry). It is not version + authority. macOS and formal Windows resolve `resolve_published_manifest` + before any npm plan. - npm receives exact package/version, general registry and the matching `@anthropic-ai:registry` option for that invocation. Scope config must not silently redirect the request to a different registry. Global/user npmrc and @@ -115,7 +147,10 @@ generic command execution capability is added. | Condition | Required result | | --------------------------------------------------------------- | ---------------------------------------------------------------------- | | Claude Desktop surface or generic launch requested | `surface_not_supported` / `action_not_supported`, no source or process | -| Root/platform metadata differs from manifest | Skip source; source failure if none match | +| Root/platform metadata differs from the resolved latest | Skip source; source failure if none match | +| `/latest` document version is the tag `latest` | Reject; resolve a concrete semver first | +| Multiple PATH-visible copies, one is PATH default | Use PATH default; not `tool_owner_unsupported` | +| macOS discovery walks mise/nvm/fnm/Volta install trees | Contract regression; env/PATH only | | Node/npm missing, too old or wrong architecture | `tool_host_missing`; no installer | | Multiple installs, unrecognized owner or prefix mismatch | `tool_owner_unsupported`; no conversion/write | | Already same/newer npm install on update | No-op; never downgrade | @@ -131,9 +166,11 @@ generic command execution capability is added. Good: a verified mirror installs the exact official optional package and the actual CLI reports the expected version. Base: a native installation remains usable/readable but must update through its original owner. Bad: `npm @latest`, -global mirror changes, treating an npm success exit as runnable proof, -executing a user-writable npm from the elevated Windows parent, or a renderer -parser that still expects Claude Desktop/`managed_desktop`. +a compiled reviewed version JSON, walking mise/nvm trees, treating +`len>1` as unsupported when PATH default exists, global mirror changes, +treating an npm success exit as runnable proof, executing a user-writable +npm from the elevated Windows parent, or a renderer parser that still expects +Claude Desktop/`managed_desktop`. ## 6. Tests Required @@ -145,6 +182,10 @@ owner/prefix rejection, CLI-only policy and post-install observation. Renderer tests must parse compact `claude-code` readiness as `cli` / `cli_tooling` and reject Desktop/`managed_desktop`. Mirror smoke uses an isolated temporary home/prefix/cache and no login or inference. +`grok_npm` tests must parse a `/latest` document version, reject +`version=latest`, keep fixture argv free of `@latest`, and must not +`include_str!` a version/hash JSON. `default_install` tests must prefer +PATH default over a second copy. Windows native helper execution and real vendor login require their own matching-host evidence; macOS and portable tests do not establish it. Helper contract tests must require `npm.cmd` discovery plus @@ -154,12 +195,15 @@ Helper contract tests must require `npm.cmd` discovery plus ## 7. Wrong vs Correct ```text -wrong: install latest from any mirror; npm exit 0 -> installed +wrong: install `@latest` from any mirror; npm exit 0 -> installed +wrong: compile a reviewed version/hash JSON and treat it as latest +wrong: walk ~/.mise / nvm / volta trees; treat any second copy as unsupported wrong: run user npm from the elevated desktop process wrong: Command::new("npm.cmd") as the helper application name wrong: renderer surfacesForAgent(claude-code)=desktop; sourceKind=managed_desktop -correct: compiled manifest -> matching registry/root/platform -> closed plan - -> ordinary-user execution -> actual CLI version/owner readback +correct: login/process PATH + product env -> PATH-default owner +correct: registry /latest -> exact version + integrity -> matching registry + -> closed plan -> ordinary-user execution -> actual CLI version/owner correct: Windows .cmd shim -> cmd /D /S /C call "{quoted}" via raw_arg correct: renderer admits compact CLI readiness (cli_tooling, no surfaces array) ``` diff --git a/.trellis/spec/backend/external-agent-lifecycle.md b/.trellis/spec/backend/external-agent-lifecycle.md index 6961e2283..f167adf9b 100644 --- a/.trellis/spec/backend/external-agent-lifecycle.md +++ b/.trellis/spec/backend/external-agent-lifecycle.md @@ -144,12 +144,12 @@ command, argument vector, token, hash, package format, signer or bypass flags. | Product | Owner and current lifecycle policy | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Grok Build | CLI Tooling owner; default fresh install uses the official `@xai-official/grok` npm package, a bundled exact-version manifest, and a mainland-first registry chain. Native `x.ai` install is an explicit secondary action. Updates preserve the observed `native_internal` or `official_npm` owner. | +| Grok Build | CLI Tooling owner; default fresh install uses the official `@xai-official/grok` npm package, the current registry `latest` resolved at runtime to an exact version + SHA-512, and a mainland-first registry chain. Native `x.ai` install is an explicit secondary action. Updates preserve the observed `native_internal` or `official_npm` owner. | | Codex | Dedicated Codex Desktop installer; Agent action returns `managed_by_codex_desktop` and does not occupy the Agent job slot. | | QoderWork CN | Managed desktop; install/launch admitted, FyAgent update disabled. Source is the reviewed first-party `/qoder-work-cn/releases/latest/` aliases and same-host Electron-builder version feed. | | TRAE Work CN | Managed desktop; install/launch admitted, FyAgent update disabled. Resolve `data.solo` with `region=cn`; never TRAE Code/`data.manifest`. | | WorkBuddy | Managed desktop; install/launch admitted, FyAgent update disabled. Resolve the closed `/v2/update` platform IDs and reviewed macOS suffix rewrite. | -| Claude Code | CLI only; exact official npm manifest, shared verified mirrors, owner-preserving updates and ordinary-user execution. See [Claude Code CLI](./claude-code-cli.md). | +| Claude Code | CLI only; runtime-resolved official npm latest, shared verified mirrors, owner-preserving updates and ordinary-user execution. See [Claude Code CLI](./claude-code-cli.md). | | OpenCode Desktop | Desktop only; use reviewed stable desktop artifacts and closed bundle identity on supported hosts. No public OpenCode CLI installer. | - Grok npm optional-package admission is resolved by the signed product on the @@ -157,14 +157,22 @@ command, argument vector, token, hash, package format, signer or bypass flags. an unsupported architecture produces no install plan. The current helper's platform selector is defined only for macOS/Windows; Linux compilation and package support cannot be inferred from these product-host tests. - The bundled manifest contains no Linux optional package and no generic - `std::env::consts::OS` fallback. + The runtime-resolved optional-package map admits no Linux package and no + generic `std::env::consts::OS` fallback. - Before execution, the product matches both `@xai-official/grok` and the - current platform package SHA-512 against one allowed registry. On formal - Windows it then sends only the compact exact-version/registry/allow-scripts - control to the ordinary-user helper. The helper validates and executes that - closed control; it does not resolve registry metadata, choose a platform - package, or invent `@latest`. + current platform package SHA-512 against one allowed registry. Version + authority is `resolve_published_manifest` (npmjs `/latest` first, then the + mainland chain). The host then sends only the compact exact-version / + registry / allow-scripts control to the ordinary-user helper. The helper + validates and executes that closed control; it does not resolve registry + metadata, choose a platform package, or invent `@latest`. +- macOS Claude/Grok discovery uses login-shell PATH, process PATH, and product + env. It does not walk mise/nvm/fnm/Volta trees. `default_install` is the + PATH-default copy. See [Claude Code CLI](./claude-code-cli.md). +- CLI `ToolVersion.latest_version` is always live: Claude/Grok/Codex/Gemini/ + OpenClaw from npm registry (OpenCode may fall back to GitHub latest; Hermes + from PyPI). Desktop products keep vendor feeds. Do not compile a reviewed + CLI version/hash JSON. - Qoder display version comes only from an unindented top-level `version:` in bounded same-host `latest.yml`/`latest-mac.yml`. The feed ZIP and `sha512` are metadata, not admitted artifacts. Windows ARM64 remains unsupported @@ -316,7 +324,9 @@ leaf: | OpenCode Uninstall DisplayName is `OpenCode ` | Keep the ARP hint; do not require exact `OpenCode`. | | OpenCode Uninstall DisplayName is `OpenCode Dev`, `OpenCodeAI`, or a prerelease version | Skip that ARP entry. | | OpenCode KnownPath relative is missing | Drop the observation; do not retain KnownPath Missing. | -| Grok default install has no native expected owner | Plan official npm from the bundled exact-version manifest; never `@latest`. | +| Grok default install has no native expected owner | Resolve official npm `latest` to an exact version + integrity; never install `@latest`. | +| Multiple PATH-visible Claude/Grok copies, one is PATH default | Use PATH default; do not fail as owner-unsupported. | +| CLI latest display uses a compiled version/hash JSON | Contract regression; resolve live registry/`/latest`. | | Grok macOS/Windows architecture has no closed platform package or manifest integrity | Produce no npm plan/action; do not fall back to Linux or another product package. | | Non-macOS/non-Windows development host | No admitted CLI package; verify host compilation separately under the development-environment contract, without inventing a Linux installer or crate-wide rejection policy. | | Grok registry metadata does not match both root and current-platform SHA-512 | Skip that registry; fail with source exhaustion when none match. | @@ -343,8 +353,9 @@ leaf: - **Bad:** use a researched CDN URL, infer install from a config directory, update Qoder/TRAE/WorkBuddy, choose the first candidate, fake percent without total bytes, or label Windows wizard handoff as installed evidence. -- **Bad:** install Grok with `@latest`, change the user's global npmrc, or - claim mainland sign-in/inference because the CLI installed. +- **Bad:** install Grok with `@latest`, compile a reviewed npm version JSON, + change the user's global npmrc, or claim mainland sign-in/inference because + the CLI installed. - **Bad:** treat GitHub latest failure as OpenCode uninstallable, freeze only the NSIS stub path `OpenCode/OpenCode.exe`, require exact Uninstall DisplayName equality, or describe Windows OpenCode as supported while @@ -371,8 +382,10 @@ Assertion points: incomplete evidence, expires capabilities and rejects drift; - Qoder/Trae/WorkBuddy/OpenCode source parsers enforce exact host, platform, schema, redirect and version rules without stale URL fallback; -- Claude CLI tests cover the compiled npm manifest, shared registry/argv/helper, +- Claude CLI tests cover runtime-resolved npm latest, shared registry/argv/helper, actual version/owner verification, and rejection of the retired Desktop path; + `grok_npm` must reject `version=latest` and must not `include_str!` a + version/hash JSON; `default_install` prefers PATH default; - renderer `surfacesForAgent` / readiness `sourceKind` stay aligned with lifecycle policy: Grok and Claude are compact CLI/`cli_tooling`; - macOS exact-path deployment, cancellation boundary, running-app protection, @@ -390,8 +403,8 @@ Assertion points: - job single-flight, terminal slot release, transfer monotonicity, unknown total, cancel refusal after side-effect boundary and unknown job ID; - Grok owner-preserving lifecycle and ordinary-user helper with no elevated - fallback; product-host cfg maps only Darwin/Windows x64/arm64, the bundled - manifest has no Linux optional package, registry admission matches both + fallback; product-host cfg maps only Darwin/Windows x64/arm64, runtime + resolution admits no Linux optional package, registry admission matches both package integrities, and the helper receives only the compact host-selected plan; - renderer polls until a terminal native stage and does not paint a poll cap @@ -445,6 +458,20 @@ sourceKind === "cli_tooling" Wrong: +```rust +include_str!("claude_npm_manifest.json"); +npm_argv = ["i", "-g", "@anthropic-ai/claude-code@latest"]; +``` + +Correct: + +```rust +let manifest = grok_npm::resolve_published_manifest(OfficialNpmTool::Claude).await?; +// npm argv is package@; never @latest +``` + +Wrong: + ```rust let candidate = inventory.candidates.first().unwrap(); launch(candidate.path)?; diff --git a/.trellis/spec/backend/health.md b/.trellis/spec/backend/health.md index 50375c3c6..b346f24a1 100644 --- a/.trellis/spec/backend/health.md +++ b/.trellis/spec/backend/health.md @@ -31,8 +31,10 @@ decoration must not turn an unknown fact into a positive result. Evidence rules: - Local file discovery proves presence, not successful execution or login. - CLI health uses bounded filesystem/package metadata and never `--version`, - login shell, remote release metadata or the regular CLI readiness observer. + CLI health may reuse Tooling search directories (login PATH, process PATH, + product env) as filesystem roots. It still never executes `--version`, + never spawns the CLI through a login shell, never fetches remote release + metadata, and never uses the regular CLI readiness observer. - Desktop health uses the explicit Desktop-only inventory entry point. Existing inventory capabilities remain owned by the installation service; the health view cannot manufacture target IDs or invoke a lifecycle action. diff --git a/.trellis/spec/frontend/agent-directory.md b/.trellis/spec/frontend/agent-directory.md index 7f74953ee..7bbd21b89 100644 --- a/.trellis/spec/frontend/agent-directory.md +++ b/.trellis/spec/frontend/agent-directory.md @@ -199,6 +199,8 @@ Do not send the user to the Models section to pick a filesystem destination. must not send a registry, version, hash, or npm command. Default one-click install is official npm; official CLI is an explicit secondary control. Native-owned installs may offer “改用官方 npm 方式”; that must not auto-run. + CLI latest/update availability comes from native `latest_version` / + `allowedActions`. The renderer never embeds a reviewed npm version. - Windows vendor-wizard success uses `官方安装窗口已打开。完成安装后请刷新安装状态。` It must not say the product is installed. OpenCode Windows ARM64 remains unavailable. @@ -238,6 +240,7 @@ Do not send the user to the Models section to pick a filesystem destination. | Native DTO contains unknown/excess/forbidden field | Strict parser failure; never spread raw object into UI. | | Inventory is `multiple` and the user confirms a destination | Start the native action and immediately dismiss the picker back to the originating control; the card shows job progress. Do not keep 「安装中…」 on the dialog until the job finishes. | | Claude/Grok compact CLI readiness uses `cli_tooling` | Parse and project install/update; do not fail the directory scan. | +| Renderer embeds a reviewed Claude/Grok npm version | Contract regression; show native `latest_version` only. | | Claude/Grok readiness uses `managed_desktop` or `desktop` surface | Fail closed at the parser; do not render a Desktop install card. | | Route changes/unmounts | Clear transient selection/confirmation; do not cancel native work unless user explicitly requested it. | @@ -349,6 +352,18 @@ surfacesForAgent("claude-code") === ["cli"] parseAgentInstallReadiness admits sourceKind === "cli_tooling" ``` +Wrong: + +```ts +const CLAUDE_REVIEWED_VERSION = "2.1.261"; +``` + +Correct: + +```ts +readiness.localVersion; // native latest_version / allowedActions decide update +``` + Native owns identity, legality and side effects; the page owns strict projection, explicit user selection and evidence-correct wording. diff --git a/src-tauri/src/services/tooling.rs b/src-tauri/src/services/tooling.rs index 17a2065a4..2892f8bae 100644 --- a/src-tauri/src/services/tooling.rs +++ b/src-tauri/src/services/tooling.rs @@ -866,30 +866,9 @@ fn tool_executable_candidates(tool: &str, dir: &Path) -> Vec } } -fn extend_mise_node_search_paths(paths: &mut Vec, home: &Path) { - if home.as_os_str().is_empty() { - return; - } - - let mise_base = home.join(".local/share/mise"); - push_unique_path(paths, mise_base.join("shims")); - - let node_installs = mise_base.join("installs").join("node"); - if node_installs.exists() { - if let Ok(entries) = std::fs::read_dir(&node_installs) { - for entry in entries.flatten() { - let bin_path = entry.path().join("bin"); - if bin_path.exists() { - push_unique_path(paths, bin_path); - } - } - } - } -} - -/// 构建某工具的候选搜索目录(原生安装优先,PATH 兜底)。 -/// 单探兜底 (`scan_cli_version`) 与全量枚举 (`enumerate_tool_installations`) 共用, -/// 确保两条路径看到的是同一组安装位置。 +/// 构建某工具的候选搜索目录。识别跟用户环境走:登录 shell 的 PATH、当前进程 +/// PATH、以及产品自己的环境变量(如 `GROK_BIN_DIR`),不遍历 mise/nvm/volta +/// 的内部安装树。单探兜底与全量枚举共用,确保两条路径看到同一组位置。 fn build_tool_search_paths(tool: &str) -> Vec { let resolved_home = crate::config::get_home_dir(); #[cfg(target_os = "windows")] @@ -901,7 +880,6 @@ fn build_tool_search_paths(tool: &str) -> Vec { #[cfg(target_os = "macos")] let home = resolved_home; - // 常见的安装路径(原生安装优先) let mut search_paths: Vec = Vec::new(); if tool == "grok" { #[cfg(target_os = "windows")] @@ -915,22 +893,16 @@ fn build_tool_search_paths(tool: &str) -> Vec { } if !home.as_os_str().is_empty() { push_unique_path(&mut search_paths, home.join(".local/bin")); - push_unique_path(&mut search_paths, home.join(".npm-global/bin")); - push_unique_path(&mut search_paths, home.join("n/bin")); - push_unique_path(&mut search_paths, home.join(".volta/bin")); - extend_mise_node_search_paths(&mut search_paths, &home); + #[cfg(target_os = "windows")] + { + push_unique_path(&mut search_paths, home.join(".npm-global/bin")); + push_unique_path(&mut search_paths, home.join("n/bin")); + push_unique_path(&mut search_paths, home.join(".volta/bin")); + } } #[cfg(target_os = "macos")] { - push_unique_path( - &mut search_paths, - std::path::PathBuf::from("/opt/homebrew/bin"), - ); - push_unique_path( - &mut search_paths, - std::path::PathBuf::from("/usr/local/bin"), - ); if tool == "hermes" { let python_base = home.join("Library").join("Python"); if python_base.exists() { @@ -944,6 +916,10 @@ fn build_tool_search_paths(tool: &str) -> Vec { } } } + if let Some(login) = login_shell_path() { + extend_from_cli_path_env(&mut search_paths, Some(std::ffi::OsString::from(login))); + } + extend_from_cli_path_env(&mut search_paths, std::env::var_os("PATH")); } #[cfg(target_os = "windows")] @@ -982,27 +958,25 @@ fn build_tool_search_paths(tool: &str) -> Vec { } } extend_windows_cli_manager_search_paths(&mut search_paths, &home); - } - - let fnm_base = home.join(".local/state/fnm_multishells"); - if fnm_base.exists() { - if let Ok(entries) = std::fs::read_dir(&fnm_base) { - for entry in entries.flatten() { - let bin_path = entry.path().join("bin"); - if bin_path.exists() { - push_unique_path(&mut search_paths, bin_path); + let fnm_base = home.join(".local/state/fnm_multishells"); + if fnm_base.exists() { + if let Ok(entries) = std::fs::read_dir(&fnm_base) { + for entry in entries.flatten() { + let bin_path = entry.path().join("bin"); + if bin_path.exists() { + push_unique_path(&mut search_paths, bin_path); + } } } } - } - - let nvm_base = home.join(".nvm/versions/node"); - if nvm_base.exists() { - if let Ok(entries) = std::fs::read_dir(&nvm_base) { - for entry in entries.flatten() { - let bin_path = entry.path().join("bin"); - if bin_path.exists() { - push_unique_path(&mut search_paths, bin_path); + let nvm_base = home.join(".nvm/versions/node"); + if nvm_base.exists() { + if let Ok(entries) = std::fs::read_dir(&nvm_base) { + for entry in entries.flatten() { + let bin_path = entry.path().join("bin"); + if bin_path.exists() { + push_unique_path(&mut search_paths, bin_path); + } } } } @@ -1025,8 +999,6 @@ fn build_tool_search_paths(tool: &str) -> Vec { } } - #[cfg(target_os = "macos")] - extend_from_cli_path_env(&mut search_paths, std::env::var_os("PATH")); #[cfg(target_os = "windows")] search_paths.retain(|path| crate::windows_runtime::is_local_command_path(path)); search_paths @@ -4181,22 +4153,6 @@ mod tests { ))); } - #[test] - fn mise_node_search_paths_include_shims_and_installed_node_bins() { - let temp = tempfile::tempdir().expect("temp dir should be created"); - let home = temp.path(); - let node_bin = home - .join(".local/share/mise/installs/node/25.8.0") - .join("bin"); - std::fs::create_dir_all(&node_bin).expect("node bin should be created"); - - let mut paths = Vec::new(); - extend_mise_node_search_paths(&mut paths, home); - - assert!(paths.contains(&home.join(".local/share/mise/shims"))); - assert!(paths.contains(&node_bin)); - } - #[cfg(target_os = "macos")] #[test] fn tool_executable_candidates_macos_uses_plain_binary_name() { diff --git a/src-tauri/src/services/tooling/claude.rs b/src-tauri/src/services/tooling/claude.rs index 81f28a815..3774523a3 100644 --- a/src-tauri/src/services/tooling/claude.rs +++ b/src-tauri/src/services/tooling/claude.rs @@ -10,7 +10,7 @@ use super::ToolVersion; use fyagent_user_helper::claude::CLAUDE_MIN_NODE_MAJOR; #[cfg(any(target_os = "macos", test))] use fyagent_user_helper::claude::{installation_owner, ClaudeOwner}; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "windows"))] use fyagent_user_helper::grok_npm::OfficialNpmTool; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -35,7 +35,7 @@ impl ClaudeLifecycleError { Self::OwnerUnsupported => { "当前 Claude Code 不是可确认的 npm 安装,请使用原安装方式更新;未更换安装来源。" } - Self::SourceUnverified => "镜像未能提供与官方清单一致的 Claude Code 版本。", + Self::SourceUnverified => "暂时无法从官方 npm 获取可用的 Claude Code 版本。", Self::ExecutionFailed => "Claude Code 安装未完成,请检查网络及当前用户的安装权限。", Self::VerificationFailed => "无法确认 Claude Code 已安装到指定版本,请刷新安装状态。", } @@ -73,14 +73,21 @@ pub(super) async fn version() -> ToolVersion { None, ), }; + let latest_version = super::versions::fetch_npm_latest_for_tool( + &crate::proxy::http_client::get(), + fyagent_user_helper::claude::CLAUDE_NPM_PACKAGE, + "claude", + version.as_deref(), + ) + .await; ToolVersion { name: "claude".to_string(), version, error, - latest_version: super::grok_npm::claude_manifest_version(), + latest_version, installed_but_broken: broken, distribution_owner: owner, - latest_source: Some("reviewed_npm_manifest".to_string()), + latest_source: Some("npm".to_string()), } } @@ -93,14 +100,26 @@ pub(super) async fn version() -> ToolVersion { installed_but_broken: observed.detected && observed.normalized_version.is_none(), error: (!observed.detected).then(|| "Claude Code is not installed".to_string()), version: observed.normalized_version, - latest_version: grok_npm::claude_manifest_version(), + latest_version: super::versions::fetch_npm_latest_for_tool( + &crate::proxy::http_client::get(), + fyagent_user_helper::claude::CLAUDE_NPM_PACKAGE, + "claude", + observed.normalized_version.as_deref(), + ) + .await, distribution_owner: observed.owner.map(|owner| owner.as_str().to_string()), - latest_source: Some("reviewed_npm_manifest".to_string()), + latest_source: Some("npm".to_string()), }, Err(error) => ToolVersion { name: "claude".to_string(), version: None, - latest_version: grok_npm::claude_manifest_version(), + latest_version: super::versions::fetch_npm_latest_for_tool( + &crate::proxy::http_client::get(), + fyagent_user_helper::claude::CLAUDE_NPM_PACKAGE, + "claude", + None, + ) + .await, error: Some(error.message().to_string()), installed_but_broken: false, distribution_owner: None, @@ -121,19 +140,20 @@ struct Installation { #[cfg(target_os = "macos")] fn observe() -> Result, ClaudeLifecycleError> { let installs = super::enumerate_tool_installations("claude"); - if installs.len() > 1 { - return Err(ClaudeLifecycleError::OwnerUnsupported); + match super::default_install(&installs) { + Some(install) => Ok(Some(Installation { + owner: installation_owner( + &install.path, + &install.real.to_string_lossy(), + &install.source, + ), + path: install.path.clone(), + real: install.real.clone(), + version: install.version.clone(), + })), + None if installs.is_empty() => Ok(None), + None => Err(ClaudeLifecycleError::OwnerUnsupported), } - Ok(installs.into_iter().next().map(|install| Installation { - owner: installation_owner( - &install.path, - &install.real.to_string_lossy(), - &install.source, - ), - path: install.path, - real: install.real, - version: install.version, - })) } #[cfg(target_os = "macos")] @@ -147,8 +167,9 @@ pub(super) async fn run(action: ToolLifecycleAction) -> Result<(), ClaudeLifecyc let before = tokio::task::spawn_blocking(observe) .await .map_err(|_| ClaudeLifecycleError::ExecutionFailed)??; - let manifest = - grok_npm::claude_manifest().map_err(|_| ClaudeLifecycleError::SourceUnverified)?; + let manifest = grok_npm::resolve_published_manifest(OfficialNpmTool::Claude) + .await + .map_err(|_| ClaudeLifecycleError::SourceUnverified)?; if let Some(installed) = &before { if action == ToolLifecycleAction::Install || installed.owner != ClaudeOwner::Npm { return Err(ClaudeLifecycleError::OwnerUnsupported); @@ -240,8 +261,9 @@ pub(super) async fn run(action: ToolLifecycleAction) -> Result<(), ClaudeLifecyc { return Err(ClaudeLifecycleError::OwnerUnsupported); } - let manifest = - grok_npm::claude_manifest().map_err(|_| ClaudeLifecycleError::SourceUnverified)?; + let manifest = grok_npm::resolve_published_manifest(OfficialNpmTool::Claude) + .await + .map_err(|_| ClaudeLifecycleError::SourceUnverified)?; if action == GrokToolAction::Update && before .normalized_version diff --git a/src-tauri/src/services/tooling/claude_npm_manifest.json b/src-tauri/src/services/tooling/claude_npm_manifest.json deleted file mode 100644 index 904124e2a..000000000 --- a/src-tauri/src/services/tooling/claude_npm_manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "channel": "stable", - "package": "@anthropic-ai/claude-code", - "version": "2.1.261", - "integrity": { - "@anthropic-ai/claude-code": "sha512-j6+AkfCl6/UJBcx66nlZUmWc4XGK3TscvW19Tiat+oDwkz3WqQfKzjvHO5FhR+shXTtktqs6vqSBrJmeSWpU3Q==", - "@anthropic-ai/claude-code-darwin-arm64": "sha512-HW2cIc5MWj9BOzLxm+3zNTfidepTMAFirwYO8H2ywTamOZvmwb8LzvRlRDLgUzEpUJBIkv9bEvI/RxfMk3zKOg==", - "@anthropic-ai/claude-code-darwin-x64": "sha512-gg/I/q0RBE+3mJW4tEJW+h+egnhwWkyD5WQwnsHe2IomssbpGdf3UwqCVkMitQ7d22ws49O8QWon/v5Sx5gYow==", - "@anthropic-ai/claude-code-win32-arm64": "sha512-0JgclPx5WsIimpRonJQlnMvOAzg3EvbstbSYrWaqTGnbuW7eiCk1v9+zAVjnhIJ3uAs9JBavmP55QNdRVYlwng==", - "@anthropic-ai/claude-code-win32-x64": "sha512-QtUUxcOz3gsMiO2d99QGCRaqcjtpYBudioUXMNH9ImeGqqbLArbjJKvder7uAVzhr8lRvk+DUIOwaqjV8kUNXA==" - } -} diff --git a/src-tauri/src/services/tooling/grok.rs b/src-tauri/src/services/tooling/grok.rs index 44321a6e0..d116350ca 100644 --- a/src-tauri/src/services/tooling/grok.rs +++ b/src-tauri/src/services/tooling/grok.rs @@ -291,26 +291,23 @@ pub(super) fn observe_grok_owner( installs: &[ToolInstallation], config_toml: Option<&str>, ) -> GrokOwnerObservation { - if installs.is_empty() { - return GrokOwnerObservation::Absent; - } + let Some(install) = default_install(installs) else { + return if installs.is_empty() { + GrokOwnerObservation::Absent + } else { + GrokOwnerObservation::Ambiguous + }; + }; let config_owner = config_toml.and_then(parse_grok_cli_installer); - let mut owners = std::collections::BTreeSet::new(); - for install in installs { - owners.insert(owner_from_install( - &install.path, - &install.real.to_string_lossy(), - &install.source, - config_owner, - )); - } - if owners.len() > 1 { - return GrokOwnerObservation::Ambiguous; - } - match owners.iter().next() { - Some(GrokDistributionOwner::NativeInternal) => GrokOwnerObservation::NativeInternal, - Some(GrokDistributionOwner::OfficialNpm) => GrokOwnerObservation::OfficialNpm, - None => GrokOwnerObservation::Absent, + let owner = owner_from_install( + &install.path, + &install.real.to_string_lossy(), + &install.source, + config_owner, + ); + match owner { + GrokDistributionOwner::NativeInternal => GrokOwnerObservation::NativeInternal, + GrokDistributionOwner::OfficialNpm => GrokOwnerObservation::OfficialNpm, } } @@ -875,14 +872,18 @@ async fn run_official_npm( )); } - let manifest = match super::grok_npm::bundled_manifest() { + let manifest = match super::grok_npm::resolve_published_manifest( + fyagent_user_helper::grok_npm::OfficialNpmTool::Grok, + ) + .await + { Ok(manifest) => manifest, Err(_) => { return Err(fail_job( action, Some(GrokDistributionOwner::OfficialNpm), "official_source_unreachable", - "官方 npm 版本清单不可用", + "暂时无法读取官方 npm 最新版本", None, false, Some("official_npm"), @@ -980,7 +981,7 @@ fn execute_official_npm( ); } last_detail = format!( - "grok --version 与清单版本 {} 不一致\n{}\n{}", + "grok --version 与目标版本 {} 不一致\n{}\n{}", target_version, decode_command_output(&output.stdout), decode_command_output(&output.stderr) @@ -1310,7 +1311,11 @@ async fn windows_npm_plans_for_action( { return Vec::new(); } - let Ok(manifest) = super::grok_npm::bundled_manifest() else { + let Ok(manifest) = super::grok_npm::resolve_published_manifest( + fyagent_user_helper::grok_npm::OfficialNpmTool::Grok, + ) + .await + else { return Vec::new(); }; let matching = super::grok_npm::registries_matching_manifest(&manifest).await; @@ -1434,7 +1439,7 @@ mod tests { } #[test] - fn mixed_native_and_npm_installs_are_ambiguous() { + fn mixed_installs_follow_path_default() { let installs = [ install( "/Users/me/.grok/bin/grok", @@ -1449,6 +1454,28 @@ mod tests { false, ), ]; + assert_eq!( + observe_grok_owner(&installs, Some("[cli]\ninstaller = \"internal\"\n")), + GrokOwnerObservation::NativeInternal + ); + } + + #[test] + fn mixed_installs_without_path_default_are_ambiguous() { + let installs = [ + install( + "/Users/me/.grok/bin/grok", + "/Users/me/.grok/downloads/grok-macos-aarch64", + "system", + false, + ), + install( + "/Users/me/.nvm/versions/node/v22.14.0/bin/grok", + "/Users/me/.nvm/versions/node/v22.14.0/lib/node_modules/@xai-official/grok/bin/grok", + "nvm", + false, + ), + ]; assert_eq!( observe_grok_owner(&installs, Some("[cli]\ninstaller = \"internal\"\n")), GrokOwnerObservation::Ambiguous diff --git a/src-tauri/src/services/tooling/grok_npm.rs b/src-tauri/src/services/tooling/grok_npm.rs index b9b785c9c..f4f64accb 100644 --- a/src-tauri/src/services/tooling/grok_npm.rs +++ b/src-tauri/src/services/tooling/grok_npm.rs @@ -1,8 +1,7 @@ -//! Shared host-owned Grok/Claude npm manifests and registry selection. +//! Shared host-owned Grok/Claude npm registry selection. //! -//! Version and SHA-512 truth come from the bundled JSON compiled into the -//! signed application. Registry metadata is compared against that manifest; -//! `@latest` is never queried. +//! Latest version and SHA-512 come from registry metadata at runtime. npm still +//! receives the resolved exact version; `@latest` is never an install argv. use std::collections::BTreeMap; use std::time::Duration; @@ -14,14 +13,19 @@ use fyagent_user_helper::grok_npm::{ #[cfg(test)] use fyagent_user_helper::GROK_NPM_PACKAGE; -const MANIFEST_JSON: &str = include_str!("grok_npm_manifest.json"); -const CLAUDE_MANIFEST_JSON: &str = include_str!("claude_npm_manifest.json"); const METADATA_TIMEOUT: Duration = Duration::from_secs(20); const METADATA_MAX_BYTES: usize = 1024 * 1024; +const VERSION_AUTHORITY: [GrokNpmRegistry; 4] = [ + GrokNpmRegistry::Npmjs, + GrokNpmRegistry::Tencent, + GrokNpmRegistry::Huawei, + GrokNpmRegistry::Npmmirror, +]; #[derive(Debug, Clone, PartialEq, Eq)] pub(super) struct GrokNpmManifest { version: String, + platform_version: String, integrity: BTreeMap, tool: OfficialNpmTool, } @@ -31,6 +35,10 @@ impl GrokNpmManifest { &self.version } + pub(super) fn platform_version(&self) -> &str { + &self.platform_version + } + pub(super) fn package_integrity(&self) -> Option<&str> { self.integrity.get(self.tool.package()).map(String::as_str) } @@ -40,26 +48,66 @@ impl GrokNpmManifest { } } -pub(super) fn bundled_manifest() -> Result { - parse_manifest(MANIFEST_JSON) +pub(super) async fn resolve_published_manifest( + tool: OfficialNpmTool, +) -> Result { + let client = metadata_client().ok_or(GrokNpmPlanError::Missing)?; + let platform = tool + .current_platform_package() + .ok_or(GrokNpmPlanError::InvalidPlatformPackage)?; + let mut last_error = GrokNpmPlanError::Missing; + for registry in VERSION_AUTHORITY { + match load_manifest_from_registry(&client, tool, platform, registry).await { + Ok(manifest) => return Ok(manifest), + Err(error) => last_error = error, + } + } + Err(last_error) } -pub(super) fn claude_manifest() -> Result { - parse_manifest_for(OfficialNpmTool::Claude, CLAUDE_MANIFEST_JSON) +pub(super) async fn fetch_published_version(package: &str) -> Option { + let client = metadata_client()?; + for registry in VERSION_AUTHORITY { + let Some(json) = fetch_json(&client, registry, package, "latest").await else { + continue; + }; + let Some(version) = json.get("version").and_then(|value| value.as_str()) else { + continue; + }; + if GrokNpmInstallPlan::for_execution(version, GrokNpmRegistry::Npmjs, false).is_ok() { + return Some(version.to_string()); + } + } + None } -pub(super) fn claude_manifest_version() -> Option { - claude_manifest().ok().map(|manifest| manifest.version) +pub(super) fn install_command_for_version(version: &str) -> Option { + let plan = plan_for_registry( + &GrokNpmManifest { + version: version.to_string(), + platform_version: version.to_string(), + integrity: BTreeMap::new(), + tool: OfficialNpmTool::Grok, + }, + GrokNpmRegistry::Tencent, + false, + ) + .ok()?; + Some(format!("npm {}", plan.npm_argv().join(" "))) } -pub(super) fn bundled_manifest_version() -> Option { - bundled_manifest().ok().map(|manifest| manifest.version) +/// Command shape for tests and generic shell fallbacks. Live Grok/Claude +/// install/update resolve the published version first and do not use this. +pub(super) fn default_install_command() -> Option { + install_command_for_version("1.2.3") } +#[cfg(test)] pub(super) fn parse_manifest(json: &str) -> Result { parse_manifest_for(OfficialNpmTool::Grok, json) } +#[cfg(test)] fn parse_manifest_for( tool: OfficialNpmTool, json: &str, @@ -83,17 +131,7 @@ fn parse_manifest_for( let mut map = BTreeMap::new(); for (name, hash) in integrity { let hash = hash.as_str().ok_or(GrokNpmPlanError::InvalidIntegrity)?; - use base64::Engine; - let encoded = hash - .strip_prefix("sha512-") - .ok_or(GrokNpmPlanError::InvalidIntegrity)?; - let decoded = base64::engine::general_purpose::STANDARD - .decode(encoded) - .map_err(|_| GrokNpmPlanError::InvalidIntegrity)?; - if decoded.len() != 64 { - return Err(GrokNpmPlanError::InvalidIntegrity); - } - map.insert(name.clone(), hash.to_string()); + map.insert(name.clone(), decode_sha512(hash)?.to_string()); } if !map.contains_key(tool.package()) { return Err(GrokNpmPlanError::InvalidIntegrity); @@ -107,11 +145,26 @@ fn parse_manifest_for( GrokNpmInstallPlan::for_execution(version, GrokNpmRegistry::Npmjs, false)?; Ok(GrokNpmManifest { version: version.to_string(), + platform_version: version.to_string(), integrity: map, tool, }) } +fn decode_sha512(hash: &str) -> Result<&str, GrokNpmPlanError> { + use base64::Engine; + let encoded = hash + .strip_prefix("sha512-") + .ok_or(GrokNpmPlanError::InvalidIntegrity)?; + let decoded = base64::engine::general_purpose::STANDARD + .decode(encoded) + .map_err(|_| GrokNpmPlanError::InvalidIntegrity)?; + if decoded.len() != 64 { + return Err(GrokNpmPlanError::InvalidIntegrity); + } + Ok(hash) +} + pub(super) fn plan_for_registry( manifest: &GrokNpmManifest, registry: GrokNpmRegistry, @@ -135,12 +188,6 @@ pub(super) fn plan_for_registry( ) } -pub(super) fn default_install_command() -> Option { - let manifest = bundled_manifest().ok()?; - let plan = plan_for_registry(&manifest, GrokNpmRegistry::Tencent, false).ok()?; - Some(format!("npm {}", plan.npm_argv().join(" "))) -} - fn metadata_client() -> Option { let builder = reqwest::Client::builder() .https_only(true) @@ -185,50 +232,129 @@ pub(super) async fn registries_matching_manifest( let Some(client) = metadata_client() else { return Vec::new(); }; - let Some(expected_package) = manifest.package_integrity() else { - return Vec::new(); - }; - let Some(platform) = manifest.tool.current_platform_package() else { - return Vec::new(); - }; - let Some(expected_platform) = manifest.platform_integrity(platform) else { + if manifest.package_integrity().is_none() + || manifest + .tool + .current_platform_package() + .is_none_or(|platform| manifest.platform_integrity(platform).is_none()) + { return Vec::new(); - }; + } let mut matching = Vec::new(); for registry in GrokNpmRegistry::ALL { - if registry_matches( - &client, - registry, - manifest.tool.package(), - manifest.version(), - expected_package, - platform, - expected_platform, - ) - .await - { + if registry_matches(&client, registry, manifest).await { matching.push(registry); } } matching } -async fn registry_matches( +async fn load_manifest_from_registry( client: &reqwest::Client, + tool: OfficialNpmTool, + platform: &str, registry: GrokNpmRegistry, +) -> Result { + let root = fetch_json(client, registry, tool.package(), "latest") + .await + .ok_or(GrokNpmPlanError::Missing)?; + let published = parse_published_root(tool.package(), platform, &root)?; + let platform_doc = fetch_json(client, registry, platform, &published.platform_version) + .await + .ok_or(GrokNpmPlanError::InvalidPlatformPackage)?; + if platform_doc.get("name").and_then(|value| value.as_str()) != Some(platform) + || platform_doc.get("version").and_then(|value| value.as_str()) + != Some(published.platform_version.as_str()) + { + return Err(GrokNpmPlanError::InvalidPlatformPackage); + } + let platform_integrity = platform_doc + .get("dist") + .and_then(|value| value.get("integrity")) + .and_then(|value| value.as_str()) + .ok_or(GrokNpmPlanError::InvalidIntegrity)?; + let mut integrity = BTreeMap::new(); + integrity.insert( + tool.package().to_string(), + decode_sha512(&published.package_integrity)?.to_string(), + ); + integrity.insert( + platform.to_string(), + decode_sha512(platform_integrity)?.to_string(), + ); + GrokNpmInstallPlan::for_execution(&published.version, GrokNpmRegistry::Npmjs, false)?; + Ok(GrokNpmManifest { + version: published.version, + platform_version: published.platform_version, + integrity, + tool, + }) +} + +struct PublishedRoot { + version: String, + package_integrity: String, + platform_version: String, +} + +fn parse_published_root( package: &str, - version: &str, - expected_package: &str, - platform_package: &str, - expected_platform: &str, + platform: &str, + json: &serde_json::Value, +) -> Result { + if json.get("name").and_then(|value| value.as_str()) != Some(package) { + return Err(GrokNpmPlanError::InvalidPlatformPackage); + } + let version = json + .get("version") + .and_then(|value| value.as_str()) + .ok_or(GrokNpmPlanError::InvalidVersion)?; + GrokNpmInstallPlan::for_execution(version, GrokNpmRegistry::Npmjs, false)?; + let package_integrity = json + .get("dist") + .and_then(|value| value.get("integrity")) + .and_then(|value| value.as_str()) + .ok_or(GrokNpmPlanError::InvalidIntegrity)?; + decode_sha512(package_integrity)?; + let platform_version = json + .get("optionalDependencies") + .and_then(|value| value.get(platform)) + .and_then(|value| value.as_str()) + .unwrap_or(version); + GrokNpmInstallPlan::for_execution(platform_version, GrokNpmRegistry::Npmjs, false)?; + Ok(PublishedRoot { + version: version.to_string(), + package_integrity: package_integrity.to_string(), + platform_version: platform_version.to_string(), + }) +} + +async fn registry_matches( + client: &reqwest::Client, + registry: GrokNpmRegistry, + manifest: &GrokNpmManifest, ) -> bool { - let package_ok = fetch_integrity(client, registry, package, version) - .await - .is_some_and(|integrity| integrity == expected_package); + let Some(expected_package) = manifest.package_integrity() else { + return false; + }; + let Some(platform) = manifest.tool.current_platform_package() else { + return false; + }; + let Some(expected_platform) = manifest.platform_integrity(platform) else { + return false; + }; + let package_ok = fetch_integrity( + client, + registry, + manifest.tool.package(), + manifest.version(), + ) + .await + .is_some_and(|integrity| integrity == expected_package); if !package_ok { return false; } - fetch_integrity(client, registry, platform_package, version) + fetch_integrity(client, registry, platform, manifest.platform_version()) .await .is_some_and(|integrity| integrity == expected_platform) } @@ -242,6 +368,24 @@ async fn fetch_integrity( if version.eq_ignore_ascii_case("latest") { return None; } + let json = fetch_json(client, registry, package, version).await?; + if json.get("version").and_then(|value| value.as_str()) != Some(version) + || json.get("name").and_then(|value| value.as_str()) != Some(package) + { + return None; + } + json.get("dist")? + .get("integrity") + .and_then(|value| value.as_str()) + .map(str::to_string) +} + +async fn fetch_json( + client: &reqwest::Client, + registry: GrokNpmRegistry, + package: &str, + version: &str, +) -> Option { let url = metadata_url(registry, package, version)?; let mut response = client .get(url) @@ -266,16 +410,7 @@ async fn fetch_integrity( if bytes.is_empty() { return None; } - let json: serde_json::Value = serde_json::from_slice(&bytes).ok()?; - if json.get("version").and_then(|value| value.as_str()) != Some(version) - || json.get("name").and_then(|value| value.as_str()) != Some(package) - { - return None; - } - json.get("dist")? - .get("integrity") - .and_then(|value| value.as_str()) - .map(str::to_string) + serde_json::from_slice(&bytes).ok() } fn metadata_url(registry: GrokNpmRegistry, package: &str, version: &str) -> Option { @@ -291,54 +426,87 @@ fn metadata_url(registry: GrokNpmRegistry, package: &str, version: &str) -> Opti mod tests { use super::*; + fn fixture_sha512() -> String { + use base64::Engine; + format!( + "sha512-{}", + base64::engine::general_purpose::STANDARD.encode([0u8; 64]) + ) + } + + fn grok_fixture_json() -> String { + let hash = fixture_sha512(); + let platform = current_platform_package().expect("current platform"); + format!( + r#"{{"channel":"stable","package":"@xai-official/grok","version":"1.2.3","integrity":{{"@xai-official/grok":"{hash}","{platform}":"{hash}"}}}}"# + ) + } + #[test] - fn claude_manifest_has_only_reviewed_product_platforms_and_rejects_other_packages() { - let manifest = claude_manifest().unwrap(); - assert_eq!(manifest.tool, OfficialNpmTool::Claude); - assert_eq!(manifest.integrity.len(), 5); - assert_eq!( - manifest - .integrity - .keys() - .map(String::as_str) - .collect::>(), - [ - "@anthropic-ai/claude-code", - "@anthropic-ai/claude-code-darwin-arm64", - "@anthropic-ai/claude-code-darwin-x64", - "@anthropic-ai/claude-code-win32-arm64", - "@anthropic-ai/claude-code-win32-x64", - ] - ); - assert!(parse_manifest_for(OfficialNpmTool::Grok, CLAUDE_MANIFEST_JSON).is_err()); - assert!(parse_manifest_for(OfficialNpmTool::Claude, MANIFEST_JSON).is_err()); - assert!(parse_manifest_for( - OfficialNpmTool::Claude, - &CLAUDE_MANIFEST_JSON.replace("sha512-", "sha256-") + fn published_root_reads_latest_document_version_not_the_tag() { + let hash = fixture_sha512(); + let platform = OfficialNpmTool::Grok + .current_platform_package() + .expect("platform"); + let json = serde_json::json!({ + "name": "@xai-official/grok", + "version": "1.0.25", + "dist": { "integrity": hash }, + }); + let published = parse_published_root("@xai-official/grok", platform, &json).unwrap(); + assert_eq!(published.version, "1.0.25"); + assert_eq!(published.platform_version, "1.0.25"); + assert!(parse_published_root( + "@xai-official/grok", + platform, + &serde_json::json!({ + "name": "@xai-official/grok", + "version": "latest", + "dist": { "integrity": hash }, + }) ) .is_err()); } #[test] - fn bundled_manifest_is_exact_and_has_current_platform() { - let manifest = bundled_manifest().expect("bundled manifest"); + fn published_root_reads_optional_package_version() { + let hash = fixture_sha512(); + let platform = OfficialNpmTool::Grok + .current_platform_package() + .expect("platform"); + let mut json = serde_json::json!({ + "name": "@xai-official/grok", + "version": "1.0.25", + "dist": { "integrity": hash }, + }); + let mut deps = serde_json::Map::new(); + deps.insert(platform.to_string(), serde_json::json!("1.0.26")); + json.as_object_mut().expect("object").insert( + "optionalDependencies".to_string(), + serde_json::Value::Object(deps), + ); + let published = parse_published_root("@xai-official/grok", platform, &json).unwrap(); + assert_eq!(published.version, "1.0.25"); + assert_eq!(published.platform_version, "1.0.26"); + } + + #[test] + fn parse_manifest_accepts_current_platform_fixture() { + let manifest = parse_manifest(&grok_fixture_json()).expect("fixture"); + assert_eq!(manifest.version(), "1.2.3"); assert_ne!(manifest.version(), "latest"); - assert!(manifest.version().chars().next().unwrap().is_ascii_digit()); let platform = current_platform_package().expect("current platform"); assert!(manifest.package_integrity().unwrap().starts_with("sha512-")); assert!(manifest .platform_integrity(platform) .unwrap() .starts_with("sha512-")); - assert!(MANIFEST_JSON.contains(manifest.version())); - assert!(!MANIFEST_JSON.contains("@latest")); } #[test] - fn default_install_command_uses_manifest_version_and_tencent() { + fn default_install_command_uses_exact_version_and_tencent() { let command = default_install_command().expect("default command"); - let version = bundled_manifest().expect("manifest").version; - assert!(command.contains(&format!("@xai-official/grok@{version}"))); + assert!(command.contains("@xai-official/grok@1.2.3")); assert!(command.contains("--registry=https://mirrors.tencent.com/npm/")); assert!(!command.contains("@latest")); assert!(!command.contains("npm config")); @@ -390,10 +558,12 @@ mod tests { } #[test] - fn metadata_url_never_asks_for_latest() { - let url = metadata_url(GrokNpmRegistry::Tencent, GROK_NPM_PACKAGE, "1.0.13").expect("url"); - assert!(url.as_str().contains("1.0.13")); - assert!(!url.as_str().contains("latest")); - assert_eq!(url.scheme(), "https"); + fn metadata_url_can_resolve_latest_tag_but_install_uses_exact_version() { + let latest = metadata_url(GrokNpmRegistry::Npmjs, GROK_NPM_PACKAGE, "latest").expect("url"); + assert!(latest.as_str().ends_with("/latest")); + let exact = metadata_url(GrokNpmRegistry::Tencent, GROK_NPM_PACKAGE, "1.2.3").expect("url"); + assert!(exact.as_str().contains("1.2.3")); + assert!(!exact.as_str().contains("latest")); + assert_eq!(exact.scheme(), "https"); } } diff --git a/src-tauri/src/services/tooling/grok_npm_manifest.json b/src-tauri/src/services/tooling/grok_npm_manifest.json deleted file mode 100644 index 856af2b30..000000000 --- a/src-tauri/src/services/tooling/grok_npm_manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "channel": "stable", - "package": "@xai-official/grok", - "version": "1.0.13", - "published_at": "2026-08-28T00:00:00Z", - "integrity": { - "@xai-official/grok": "sha512-rBMEx/7ND5DaBRGwzi6fEyf4ZWy4yStPnZ38UaIM2smZzg4E0fieDfLKPK8eRF4l2Xe4+5kSdCAVop99+whG4A==", - "@xai-official/grok-win32-x64": "sha512-IAoQ+fDQVUzwzl1gYK+CMka2cH4yN5nbcsyodyvYTvkxSPixDIzzWeRtrOESOCPlft6YYpvFfdqisJppPOHekA==", - "@xai-official/grok-win32-arm64": "sha512-J+USAEgMyy7TaDRir2HvS1rj8yG8HjjtewK0qxz1dl2E7jjdqMjaKuN6H6l563ASvXwNDx17ye+1fkwwpjyKdg==", - "@xai-official/grok-darwin-x64": "sha512-trI+fm4ZY/2skF05XLfCYCMQgk/NUj6jYVV1stL4jPafhYj41yDYxu0PHgLxEy6f8UtA8XCfZN084/KxgiYDKw==", - "@xai-official/grok-darwin-arm64": "sha512-Nctnwkzj550E512RZ+n+IUuhqGPZ2L7z/ZTIlZdVn0KqZHfzC58vIZPMBzdcOkoK42IrJlLD6GS7Eo6c1y+VGw==" - } -} diff --git a/src-tauri/src/services/tooling/versions.rs b/src-tauri/src/services/tooling/versions.rs index bfd1cea27..dff283e37 100644 --- a/src-tauri/src/services/tooling/versions.rs +++ b/src-tauri/src/services/tooling/versions.rs @@ -55,7 +55,7 @@ pub(super) async fn get_single_tool_version_impl(tool: &str) -> ToolVersion { _ => None, }; - ToolVersion { + let mapped = ToolVersion { name: tool.to_string(), version: local_version, latest_version, @@ -67,7 +67,8 @@ pub(super) async fn get_single_tool_version_impl(tool: &str) -> ToolVersion { installed_but_broken, distribution_owner, latest_source, - } + }; + mapped } pub(super) async fn fetch_grok_latest_with_owner( @@ -78,14 +79,13 @@ pub(super) async fn fetch_grok_latest_with_owner( { let observation = super::grok::observe_installed_grok_owner(); let owner = super::grok::owner_observation_wire(observation).map(str::to_string); - let _ = client; let latest = match observation { super::grok::GrokOwnerObservation::NativeInternal => { super::grok::native_latest_from_update_check(local) } super::grok::GrokOwnerObservation::OfficialNpm | super::grok::GrokOwnerObservation::Absent => { - super::grok_npm::bundled_manifest_version() + fetch_npm_latest_for_tool(client, "@xai-official/grok", "grok", local).await } super::grok::GrokOwnerObservation::Ambiguous => None, }; @@ -93,8 +93,10 @@ pub(super) async fn fetch_grok_latest_with_owner( } #[cfg(not(target_os = "macos"))] { - let _ = (client, local); - (super::grok_npm::bundled_manifest_version(), None) + ( + fetch_npm_latest_for_tool(client, "@xai-official/grok", "grok", local).await, + None, + ) } } @@ -168,14 +170,21 @@ pub(super) async fn fetch_npm_latest_for_package( fetch_npm_latest_for_tool(client, package, "", None).await } -async fn fetch_npm_latest_for_tool( +pub(super) async fn fetch_npm_latest_for_tool( client: &reqwest::Client, package: &str, tool: &str, local_version: Option<&str>, ) -> Option { - let dist_tags = fetch_npm_dist_tags(client, package).await?; - pick_latest_version(&dist_tags, npm_prerelease_tags(tool), local_version) + let from_dist_tags = fetch_npm_dist_tags(client, package) + .await + .and_then(|dist_tags| { + pick_latest_version(&dist_tags, npm_prerelease_tags(tool), local_version) + }); + match from_dist_tags { + Some(version) => Some(version), + None => super::grok_npm::fetch_published_version(package).await, + } } pub(crate) const FIXED_GITHUB_OPENCODE_REPO: &str = "anomalyco/opencode"; From b2cdc098b6a70b9429e3dfcbf6a94e23c81aefc0 Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Fri, 11 Sep 2026 17:46:52 +0800 Subject: [PATCH 02/12] fix(tooling): apply live Grok npm policy on development Windows LocalProcess still installed the 1.2.3 fixture and omitted npm 12 --allow-scripts, so a successful npm exit left PATH-default grok unchanged. Co-authored-by: Cursor --- .trellis/spec/backend/claude-code-cli.md | 11 +- .../spec/backend/external-agent-lifecycle.md | 37 +++- .../spec/backend/windows-runtime-security.md | 31 ++- src-tauri/src/services/tooling.rs | 190 +++++++++++++++++- src-tauri/src/services/tooling/claude.rs | 23 ++- src-tauri/src/services/tooling/grok.rs | 9 + src-tauri/src/services/tooling/grok_npm.rs | 61 +++++- src-tauri/src/services/tooling/lifecycle.rs | 22 ++ 8 files changed, 361 insertions(+), 23 deletions(-) diff --git a/.trellis/spec/backend/claude-code-cli.md b/.trellis/spec/backend/claude-code-cli.md index 39f9785ca..54b855973 100644 --- a/.trellis/spec/backend/claude-code-cli.md +++ b/.trellis/spec/backend/claude-code-cli.md @@ -95,8 +95,8 @@ generic command execution capability is added. point at an older release; that is why argv never uses the `latest` tag. - `default_install_command()` is a command-shape fixture (`@xai-official/grok@1.2.3` plus the Tencent registry). It is not version - authority. macOS and formal Windows resolve `resolve_published_manifest` - before any npm plan. + authority. macOS, formal Windows, and development Windows LocalProcess + resolve `resolve_published_manifest` before any npm install/update. - npm receives exact package/version, general registry and the matching `@anthropic-ai:registry` option for that invocation. Scope config must not silently redirect the request to a different registry. Global/user npmrc and @@ -184,7 +184,9 @@ reject Desktop/`managed_desktop`. Mirror smoke uses an isolated temporary home/prefix/cache and no login or inference. `grok_npm` tests must parse a `/latest` document version, reject `version=latest`, keep fixture argv free of `@latest`, and must not -`include_str!` a version/hash JSON. `default_install` tests must prefer +`include_str!` a version/hash JSON. `command_with_script_policy` must add +`--allow-scripts=@xai-official/grok` only for npm ≥ 12 and must not duplicate +an existing flag. `default_install` tests must prefer PATH default over a second copy. Windows native helper execution and real vendor login require their own matching-host evidence; macOS and portable tests do not establish it. @@ -201,9 +203,12 @@ wrong: walk ~/.mise / nvm / volta trees; treat any second copy as unsupported wrong: run user npm from the elevated desktop process wrong: Command::new("npm.cmd") as the helper application name wrong: renderer surfacesForAgent(claude-code)=desktop; sourceKind=managed_desktop +wrong: development Windows LocalProcess `npm i -g @xai-official/grok@1.2.3` +wrong: npm 12 without --allow-scripts=@xai-official/grok; exit 0 -> succeeded correct: login/process PATH + product env -> PATH-default owner correct: registry /latest -> exact version + integrity -> matching registry -> closed plan -> ordinary-user execution -> actual CLI version/owner correct: Windows .cmd shim -> cmd /D /S /C call "{quoted}" via raw_arg correct: renderer admits compact CLI readiness (cli_tooling, no surfaces array) +correct: LocalProcess npm 12 --allow-scripts=@xai-official/grok; reread grok --version ``` diff --git a/.trellis/spec/backend/external-agent-lifecycle.md b/.trellis/spec/backend/external-agent-lifecycle.md index f167adf9b..dfba9ce25 100644 --- a/.trellis/spec/backend/external-agent-lifecycle.md +++ b/.trellis/spec/backend/external-agent-lifecycle.md @@ -162,10 +162,17 @@ command, argument vector, token, hash, package format, signer or bypass flags. - Before execution, the product matches both `@xai-official/grok` and the current platform package SHA-512 against one allowed registry. Version authority is `resolve_published_manifest` (npmjs `/latest` first, then the - mainland chain). The host then sends only the compact exact-version / + mainland chain). Formal Windows sends only the compact exact-version / registry / allow-scripts control to the ordinary-user helper. The helper validates and executes that closed control; it does not resolve registry metadata, choose a platform package, or invent `@latest`. +- Development Windows LocalProcess is not a second product policy. It + composes the same live argv in-process: never execute + `default_install_command()`'s `@xai-official/grok@1.2.3` fixture, add + `--allow-scripts=@xai-official/grok` when the executing npm major is ≥ 12, + and do not report Agent success if npm blocked that postinstall or the + PATH-default grok version is still below the planned version. Formal + Windows stays on the helper; LocalProcess is development builds only. - macOS Claude/Grok discovery uses login-shell PATH, process PATH, and product env. It does not walk mise/nvm/fnm/Volta trees. `default_install` is the PATH-default copy. See [Claude Code CLI](./claude-code-cli.md). @@ -330,6 +337,9 @@ leaf: | Grok macOS/Windows architecture has no closed platform package or manifest integrity | Produce no npm plan/action; do not fall back to Linux or another product package. | | Non-macOS/non-Windows development host | No admitted CLI package; verify host compilation separately under the development-environment contract, without inventing a Linux installer or crate-wide rejection policy. | | Grok registry metadata does not match both root and current-platform SHA-512 | Skip that registry; fail with source exhaustion when none match. | +| Development Windows Grok npm argv uses `@xai-official/grok@1.2.3` or `@latest` | Contract regression; resolve live exact version first. | +| npm 12+ omits `--allow-scripts=@xai-official/grok` on a Grok install/update | postinstall blocked; treat as failure even if npm exit 0. | +| npm exit 0 but PATH-default grok is still below the planned version | Fail that registry attempt; do not report Agent `succeeded`. | | Cancel after `launching_installer`/`installing` | `operation_conflict`; do not kill external/commit operation. | | Secret/path/raw native identity reaches DTO/log/DOM | Security regression. | @@ -356,6 +366,10 @@ leaf: - **Bad:** install Grok with `@latest`, compile a reviewed npm version JSON, change the user's global npmrc, or claim mainland sign-in/inference because the CLI installed. +- **Bad:** on development Windows, run `default_install_command()`'s + `@xai-official/grok@1.2.3` fixture, omit npm 12 `--allow-scripts=@xai-official/grok`, + or treat npm exit 0 / "changed N packages" as success while PATH-default + grok remains the previous version. - **Bad:** treat GitHub latest failure as OpenCode uninstallable, freeze only the NSIS stub path `OpenCode/OpenCode.exe`, require exact Uninstall DisplayName equality, or describe Windows OpenCode as supported while @@ -407,6 +421,10 @@ Assertion points: resolution admits no Linux optional package, registry admission matches both package integrities, and the helper receives only the compact host-selected plan; +- development Windows LocalProcess Grok npm uses `resolve_published_manifest` + (not `1.2.3`), adds `--allow-scripts=@xai-official/grok` only for npm ≥ 12, + treats blocked install scripts as failure, and rereads PATH-default + `grok --version` before `succeeded`; - renderer polls until a terminal native stage and does not paint a poll cap as failure while a job remains active. Browser fixtures do not prove native inventory, installer or signing behavior. @@ -472,6 +490,23 @@ let manifest = grok_npm::resolve_published_manifest(OfficialNpmTool::Claude).awa Wrong: +```text +development Windows LocalProcess: + npm i -g @xai-official/grok@1.2.3 + npm exit 0 / "changed 3 packages" -> Agent succeeded +``` + +Correct: + +```text +development Windows LocalProcess: + resolve_published_manifest -> npm i -g @xai-official/grok@ + npm 12+ --allow-scripts=@xai-official/grok + PATH-default grok --version must reach the planned version +``` + +Wrong: + ```rust let candidate = inventory.candidates.first().unwrap(); launch(candidate.path)?; diff --git a/.trellis/spec/backend/windows-runtime-security.md b/.trellis/spec/backend/windows-runtime-security.md index ca6f32d3c..2d72bb2f4 100644 --- a/.trellis/spec/backend/windows-runtime-security.md +++ b/.trellis/spec/backend/windows-runtime-security.md @@ -577,7 +577,12 @@ run_tool_lifecycle_action(tools=["grok"], action=install|update|install_official formal Windows -> grok-tool helper; no elevated fallback default install -> official npm exact-version plan (no @latest) install_native -> official x.ai/PowerShell installer - development Windows / macOS -> existing Tooling owner, same Grok rules + development Windows LocalProcess -> same live npm plan as helper + (resolve_published_manifest; never default_install_command 1.2.3) + npm 12+ --allow-scripts=@xai-official/grok from the executing npm major + exit 0 is not success if postinstall was blocked or PATH-default grok + is still below the planned version + macOS -> existing Tooling owner, same Grok rules CLI-backed Auth observation/session -> unavailable / interactive_user_unavailable on formal Windows @@ -611,6 +616,12 @@ fyagent-user-helper.exe entry, validates root and platform SHA-512 at an allowed registry, and then sends the compact exact-version/registry/allow-scripts control. The helper must not select Darwin/Linux packages or fetch registry metadata. +- Development Windows LocalProcess must apply the same live argv and + allow-scripts policy without widening the elevated parent. Detect npm major + from the sibling or PATH-default `npm.cmd` that will actually run. npm 11 + must not receive `--allow-scripts`; npm 12+ uses only + `--allow-scripts=@xai-official/grok`. Blocked install scripts or a + PATH-default grok version below the planned version fail that attempt. - Catalog desktop EXE install uses the separate protected package bridge and closed product action; this does not authorize CLI tools. There is no generic `ShellExecute` of a renderer/download path from Bob. Launch of an @@ -624,6 +635,9 @@ fyagent-user-helper.exe | Formal elevated Windows direct CLI/Auth execution | Fail before a user process; dedicated lifecycle helper actions are separate. | | Formal elevated Windows Grok Build lifecycle | Closed `grok-tool` helper; no elevated fallback | | Grok npm helper has no plan, `@latest`, or unknown registry | Fail closed; no npm child process | +| Development Windows Grok npm uses `@xai-official/grok@1.2.3` or `@latest` | Contract regression; resolve live exact version first | +| npm 12+ LocalProcess omits `--allow-scripts=@xai-official/grok` | postinstall blocked; treat as failure even if npm exit 0 | +| npm exit 0 but PATH-default grok is still below the planned version | Fail that registry attempt; do not report Agent succeeded | | Windows product has no matching `grok-win32-*` package/integrity or no registry matches both hashes | Produce no helper plan; no npm child process | | OpenCode Windows x64 ProductName/relative EXE/signer is reviewed | Admit current-user NSIS handoff; ARM64 remains unsupported | | OpenCode Windows ProductName/relative EXE/signer is empty | `windows_exe_install_admitted` rejects download and install; do not claim supported | @@ -636,10 +650,16 @@ fyagent-user-helper.exe - Good: Grok uses generic Tooling, Claude uses its dedicated CLI owner, and OpenCode stays Desktop-only; none runs a user CLI in the elevated parent. +- Good: development Windows LocalProcess npm 12 adds + `--allow-scripts=@xai-official/grok` and rereads PATH-default `grok --version` + before reporting success. - Base: formal Windows CLI-based Auth stays unavailable, while admitted lifecycle actions can use their closed helpers when Explorer is available. - Bad: `fyagent-user-helper.exe run --cmd `. - Bad: helper npm install without a host plan, or with `@latest`. +- Bad: development Windows LocalProcess `npm i -g @xai-official/grok@1.2.3`, + or treating npm exit 0 as success while grok postinstall was blocked or the + PATH-default version did not change. - Bad: claim OpenCode Windows is supported while identity fields are empty, or treat helper product `opencode` as proof that `OpenCode/OpenCode.exe` is the installed folder. @@ -660,6 +680,10 @@ fyagent-user-helper.exe carries, besides fixed framing/version bytes, only exact package version, registry index and the allow-scripts bit. The helper does not resolve optional-package metadata. +- LocalProcess Grok tests must keep live install argv on the resolved version + (not `1.2.3`), add `--allow-scripts=@xai-official/grok` only for npm ≥ 12, + preserve an already-present allow-scripts flag, and treat the npm + "install scripts blocked" warning as failure. - Bob/Alice/UAC HIL remains unverified residual risk. ### 7. Wrong vs Correct @@ -669,6 +693,8 @@ fyagent-user-helper.exe ```text elevated parent -> helper argv includes installer URL or shell command helper -> raw child stdout back to renderer +development Windows LocalProcess -> npm i -g @xai-official/grok@1.2.3 +npm exit 0 / "changed 3 packages" -> Agent succeeded ``` #### Correct @@ -678,6 +704,9 @@ formal elevated Windows direct CLI/Auth -> interactive_user_unavailable installer helper -> exact Codex MSIX, Agent EXE, Grok tool, or Claude tool action Grok Build lifecycle -> grok-tool helper; no elevated fallback Claude lifecycle -> claude-tool helper; no Auth verbs or generic command argv +development Windows LocalProcess -> live @xai-official/grok@ + + npm 12 --allow-scripts=@xai-official/grok + + PATH-default grok --version reaches the planned version ``` ## Scenario: Inventory parent registry enumeration rights diff --git a/src-tauri/src/services/tooling.rs b/src-tauri/src/services/tooling.rs index 2892f8bae..f23ef7899 100644 --- a/src-tauri/src/services/tooling.rs +++ b/src-tauri/src/services/tooling.rs @@ -254,9 +254,13 @@ pub async fn run_tool_lifecycle_action(tools: Vec, action: String) -> Re if grok_windows_uses_ordinary_user_helper() { return grok::run_windows_grok_helper_lifecycle(action).await; } + let live_npm_commands = if matches!(action, ToolLifecycleAction::InstallNative) { + Vec::new() + } else { + grok::windows_live_npm_install_commands().await + }; tokio::task::spawn_blocking(move || { - let command_line = build_tool_lifecycle_command(&requested, action)?; - run_elevated_cli_lifecycle_whitelist(&command_line, label) + windows_local_process_lifecycle(action, &live_npm_commands, label) }) .await .map_err(|e| format!("tool lifecycle task join error: {e}"))? @@ -293,6 +297,136 @@ fn grok_windows_execution_for(formal_windows_build: bool) -> GrokWindowsExecutio } } +#[cfg(target_os = "windows")] +fn windows_local_process_lifecycle( + action: ToolLifecycleAction, + live_npm_commands: &[String], + label: &str, +) -> Result<(), String> { + if matches!(action, ToolLifecycleAction::InstallNative) { + let command_line = build_tool_lifecycle_command(&["grok"], action)?; + return run_elevated_cli_lifecycle_whitelist(&command_line, label); + } + if live_npm_commands.is_empty() + && matches!( + action, + ToolLifecycleAction::Install | ToolLifecycleAction::InstallOfficialNpm + ) + { + return Err("官方 npm 镜像都未能提供匹配的版本".to_string()); + } + if live_npm_commands.is_empty() { + let command_line = windows_live_grok_action_command(action, None)?; + return run_elevated_cli_lifecycle_whitelist(&command_line, label); + } + let npm_path = windows_npm_binary_for_grok_action(action); + let npm_major = npm_path.as_deref().and_then(windows_npm_major); + let mut last_error = None; + for npm_install in live_npm_commands { + let npm_install = grok_npm::command_with_script_policy(npm_install, npm_major); + let command_line = windows_live_grok_action_command(action, Some(&npm_install))?; + match run_elevated_cli_lifecycle_whitelist(&command_line, label) { + Ok(()) => { + if command_line.contains("@xai-official/grok@") { + if let Some(target) = grok_npm::exact_install_version(&npm_install) { + let local = windows_observed_grok_version(); + if !local + .as_deref() + .is_some_and(|local| windows_grok_version_matches(local, target)) + { + last_error = Some(format!( + "更新完成后本机版本仍为 {},目标是 {target}", + local.as_deref().unwrap_or("未知") + )); + continue; + } + } + } + return Ok(()); + } + Err(error) => last_error = Some(error), + } + } + Err(last_error.unwrap_or_else(|| "官方 npm 镜像都未能提供匹配的版本".to_string())) +} + +#[cfg(target_os = "windows")] +fn windows_npm_major(npm_path: &Path) -> Option { + let output = run_windows_tool_command(npm_path, &["--version"]).ok()?; + if !output.status.success() { + return None; + } + fyagent_user_helper::grok_npm::parse_npm_major(&decode_command_output(&output.stdout)) +} + +#[cfg(target_os = "windows")] +fn windows_npm_binary_for_grok_action(action: ToolLifecycleAction) -> Option { + if matches!(action, ToolLifecycleAction::Update) { + let installs = enumerate_tool_installations("grok"); + if let Some(install) = default_install(&installs) { + if let Some(npm) = sibling_bin_with_ext(&install.path, "npm", &["cmd", "exe"]) { + return Some(PathBuf::from(npm)); + } + } + } + resolve_path_default("npm", None).ok().flatten() +} + +#[cfg(target_os = "windows")] +fn windows_observed_grok_version() -> Option { + default_install(&enumerate_tool_installations("grok")) + .and_then(|install| install.version.clone()) +} + +#[cfg(target_os = "windows")] +fn windows_grok_version_matches(local: &str, target: &str) -> bool { + fyagent_user_helper::grok_npm::version_is_at_least(local, target) + || fyagent_user_helper::grok::parse_normalized_version(local).as_deref() == Some(target) +} + +#[cfg(target_os = "windows")] +fn npm_output_blocked_install_scripts(stderr: &str) -> bool { + let lower = stderr.to_ascii_lowercase(); + lower.contains("install scripts blocked") || lower.contains("not covered by allowscripts") +} + +#[cfg(target_os = "windows")] +fn windows_live_grok_action_command( + action: ToolLifecycleAction, + npm_install: Option<&str>, +) -> Result { + let command = match action { + ToolLifecycleAction::InstallNative => grok_install_windows_command(), + ToolLifecycleAction::Install | ToolLifecycleAction::InstallOfficialNpm => npm_install + .ok_or_else(|| "官方 npm 镜像都未能提供匹配的版本".to_string())? + .to_string(), + ToolLifecycleAction::Update => { + let installs = enumerate_tool_installations("grok"); + if let Some(inst) = default_install(&installs) { + let real = inst.real.to_string_lossy(); + if is_grok_native_install(&inst.path, &real) { + anchored_official_update_command("grok", &inst.path) + .map(grok_native_update_command) + .ok_or_else(|| "Unsupported tool action target: grok".to_string())? + } else if let Some(npm_install) = npm_install { + grok_npm_anchored_command_using(&inst.path, npm_install) + .unwrap_or_else(|| npm_install.to_string()) + } else { + return Err("官方 npm 镜像都未能提供匹配的版本".to_string()); + } + } else if let Some(npm_install) = npm_install { + npm_install.to_string() + } else { + return Err("官方 npm 镜像都未能提供匹配的版本".to_string()); + } + } + }; + if command.is_empty() { + return Err("Unsupported tool action target: grok".to_string()); + } + Ok(lifecycle::wrap_windows_lifecycle_bat(&command)) +} + #[cfg(target_os = "windows")] async fn formal_windows_grok_version() -> ToolVersion { match grok::observe_windows_grok_via_helper().await { @@ -366,8 +500,14 @@ fn run_elevated_cli_lifecycle_whitelist(command_line: &str, label: &str) -> Resu .creation_flags(CREATE_NO_WINDOW) .output(); let _ = std::fs::remove_file(&bat_file); - - finish_lifecycle_output(&output.map_err(|e| format!("启动安装进程失败: {e}"))?) + let output = output.map_err(|e| format!("启动安装进程失败: {e}"))?; + if label.starts_with("tool_") { + let stderr = decode_command_output(&output.stderr); + if output.status.success() && npm_output_blocked_install_scripts(&stderr) { + return Err(last_lines(&stderr, 8)); + } + } + finish_lifecycle_output(&output) } /// 把子进程退出结果转成 `Result`:成功返回 `Ok`;失败提取 stderr(空则回退 stdout) @@ -1822,8 +1962,12 @@ fn package_manager_anchored_command_from_paths(tool: &str, bin_path: &str) -> Op #[cfg(target_os = "windows")] fn grok_npm_anchored_command(bin_path: &str) -> Option { + grok_npm_anchored_command_using(bin_path, grok_npm::default_install_command()?.as_str()) +} + +#[cfg(target_os = "windows")] +fn grok_npm_anchored_command_using(bin_path: &str, command: &str) -> Option { let npm = sibling_bin_with_ext(bin_path, "npm", &["cmd", "exe"])?; - let command = grok_npm::default_install_command()?; let args = command.strip_prefix("npm ")?; Some(format!("{} {args}", win_quote_path_for_batch(&npm))) } @@ -4142,6 +4286,42 @@ mod tests { assert_eq!(paths, vec![PathBuf::from("/custom/toolchain").join("bin")]); } + #[cfg(target_os = "windows")] + #[test] + fn windows_live_install_command_uses_resolved_npm_version() { + let bat = windows_live_grok_action_command( + ToolLifecycleAction::Install, + Some("npm i -g @xai-official/grok@1.0.25 --registry=https://registry.npmjs.org/"), + ) + .expect("live install"); + assert!(bat.contains("@xai-official/grok@1.0.25"), "{bat}"); + assert!(!bat.contains("@xai-official/grok@1.2.3"), "{bat}"); + } + + #[cfg(target_os = "windows")] + #[test] + fn windows_live_install_command_preserves_allow_scripts_flag() { + let bat = windows_live_grok_action_command( + ToolLifecycleAction::Install, + Some( + "npm i -g @xai-official/grok@1.0.25 --registry=https://registry.npmjs.org/ --allow-scripts=@xai-official/grok", + ), + ) + .expect("live install"); + assert!(bat.contains("--allow-scripts=@xai-official/grok"), "{bat}"); + } + + #[cfg(target_os = "windows")] + #[test] + fn npm_blocked_scripts_warning_is_detected() { + assert!(npm_output_blocked_install_scripts( + "npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:\nnpm warn install-scripts @xai-official/grok@1.0.25 (postinstall: node bin/postinstall.js)" + )); + assert!(!npm_output_blocked_install_scripts( + "========== Grok Build ==========\n\nchanged 3 packages in 1s" + )); + } + #[cfg(target_os = "windows")] #[test] fn cli_path_env_skips_windows_apps_alias_dir() { diff --git a/src-tauri/src/services/tooling/claude.rs b/src-tauri/src/services/tooling/claude.rs index 3774523a3..b761f4a6e 100644 --- a/src-tauri/src/services/tooling/claude.rs +++ b/src-tauri/src/services/tooling/claude.rs @@ -95,21 +95,24 @@ pub(super) async fn version() -> ToolVersion { pub(super) async fn version() -> ToolVersion { let observed = windows_operation(fyagent_user_helper::GrokToolAction::Observe, None).await; match observed { - Ok(observed) => ToolVersion { - name: "claude".to_string(), - installed_but_broken: observed.detected && observed.normalized_version.is_none(), - error: (!observed.detected).then(|| "Claude Code is not installed".to_string()), - version: observed.normalized_version, - latest_version: super::versions::fetch_npm_latest_for_tool( + Ok(observed) => { + let latest_version = super::versions::fetch_npm_latest_for_tool( &crate::proxy::http_client::get(), fyagent_user_helper::claude::CLAUDE_NPM_PACKAGE, "claude", observed.normalized_version.as_deref(), ) - .await, - distribution_owner: observed.owner.map(|owner| owner.as_str().to_string()), - latest_source: Some("npm".to_string()), - }, + .await; + ToolVersion { + name: "claude".to_string(), + installed_but_broken: observed.detected && observed.normalized_version.is_none(), + error: (!observed.detected).then(|| "Claude Code is not installed".to_string()), + version: observed.normalized_version, + latest_version, + distribution_owner: observed.owner.map(|owner| owner.as_str().to_string()), + latest_source: Some("npm".to_string()), + } + } Err(error) => ToolVersion { name: "claude".to_string(), version: None, diff --git a/src-tauri/src/services/tooling/grok.rs b/src-tauri/src/services/tooling/grok.rs index d116350ca..3fda643c3 100644 --- a/src-tauri/src/services/tooling/grok.rs +++ b/src-tauri/src/services/tooling/grok.rs @@ -1325,6 +1325,15 @@ async fn windows_npm_plans_for_action( .collect() } +#[cfg(target_os = "windows")] +pub(super) async fn windows_live_npm_install_commands() -> Vec { + windows_npm_plans_for_action(ToolLifecycleAction::Install) + .await + .iter() + .map(super::grok_npm::command_for_plan) + .collect() +} + #[cfg(target_os = "windows")] pub(super) async fn observe_windows_grok_via_helper( ) -> Result { diff --git a/src-tauri/src/services/tooling/grok_npm.rs b/src-tauri/src/services/tooling/grok_npm.rs index f4f64accb..d6ba2114f 100644 --- a/src-tauri/src/services/tooling/grok_npm.rs +++ b/src-tauri/src/services/tooling/grok_npm.rs @@ -7,8 +7,8 @@ use std::collections::BTreeMap; use std::time::Duration; use fyagent_user_helper::grok_npm::{ - current_platform_package, GrokNpmInstallPlan, GrokNpmPlanError, GrokNpmRegistry, - OfficialNpmTool, + current_platform_package, npm_major_allows_scripts, GrokNpmInstallPlan, GrokNpmPlanError, + GrokNpmRegistry, OfficialNpmTool, GROK_NPM_ALLOW_SCRIPTS_PACKAGE, }; #[cfg(test)] use fyagent_user_helper::GROK_NPM_PACKAGE; @@ -51,7 +51,9 @@ impl GrokNpmManifest { pub(super) async fn resolve_published_manifest( tool: OfficialNpmTool, ) -> Result { - let client = metadata_client().ok_or(GrokNpmPlanError::Missing)?; + let Some(client) = metadata_client() else { + return Err(GrokNpmPlanError::Missing); + }; let platform = tool .current_platform_package() .ok_or(GrokNpmPlanError::InvalidPlatformPackage)?; @@ -96,6 +98,33 @@ pub(super) fn install_command_for_version(version: &str) -> Option { Some(format!("npm {}", plan.npm_argv().join(" "))) } +pub(super) fn command_for_plan(plan: &GrokNpmInstallPlan) -> String { + format!("npm {}", plan.npm_argv().join(" ")) +} + +/// npm 12+ blocks unlisted lifecycle scripts. Mirror the helper: add a +/// package-scoped allow-scripts flag only when the executing npm major needs it. +pub(super) fn command_with_script_policy(command: &str, npm_major: Option) -> String { + let flag = format!("--allow-scripts={GROK_NPM_ALLOW_SCRIPTS_PACKAGE}"); + if command.contains("--allow-scripts=") { + return command.to_string(); + } + if npm_major.is_some_and(npm_major_allows_scripts) { + format!("{command} {flag}") + } else { + command.to_string() + } +} + +pub(super) fn exact_install_version(command: &str) -> Option<&str> { + let marker = format!("{}@", OfficialNpmTool::Grok.package()); + command.split_whitespace().find_map(|token| { + token + .strip_prefix(marker.as_str()) + .filter(|version| !version.is_empty() && *version != "latest") + }) +} + /// Command shape for tests and generic shell fallbacks. Live Grok/Claude /// install/update resolve the published version first and do not use this. pub(super) fn default_install_command() -> Option { @@ -513,6 +542,32 @@ mod tests { assert!(!command.contains("dangerously-allow-all")); } + #[test] + fn command_for_plan_uses_the_resolved_version() { + let plan = GrokNpmInstallPlan::for_execution("1.0.25", GrokNpmRegistry::Npmjs, false) + .expect("plan"); + let command = command_for_plan(&plan); + assert!(command.contains("@xai-official/grok@1.0.25"), "{command}"); + assert!(!command.contains("@xai-official/grok@1.2.3"), "{command}"); + assert!(command.contains("registry.npmjs.org"), "{command}"); + assert!(!command.contains("@latest"), "{command}"); + } + + #[test] + fn command_with_script_policy_adds_flag_only_for_npm_12() { + let command = "npm i -g @xai-official/grok@1.0.25 --registry=https://registry.npmjs.org/"; + let with_flag = command_with_script_policy(command, Some(12)); + assert!( + with_flag.contains("--allow-scripts=@xai-official/grok"), + "{with_flag}" + ); + assert!(!command_with_script_policy(command, Some(11)).contains("--allow-scripts=")); + assert!(!command_with_script_policy(command, None).contains("--allow-scripts=")); + let already = format!("{command} --allow-scripts=@xai-official/grok"); + assert_eq!(command_with_script_policy(&already, Some(12)), already); + assert_eq!(exact_install_version(&with_flag), Some("1.0.25")); + } + #[test] fn hash_mismatch_skips_to_the_next_registry_without_downgrade() { let expected = "sha512-expected"; diff --git a/src-tauri/src/services/tooling/lifecycle.rs b/src-tauri/src/services/tooling/lifecycle.rs index 0ed0710db..3d925563e 100644 --- a/src-tauri/src/services/tooling/lifecycle.rs +++ b/src-tauri/src/services/tooling/lifecycle.rs @@ -75,6 +75,17 @@ pub(super) fn build_tool_lifecycle_command( })) } +#[cfg(target_os = "windows")] +pub(super) fn wrap_windows_lifecycle_bat(command: &str) -> String { + [ + "@echo off", + "echo ========== Grok Build ==========", + &format!("call {command}"), + "if errorlevel 1 exit /b %errorlevel%", + ] + .join("\r\n") +} + #[cfg(any(test, target_os = "windows"))] pub(super) fn tool_display_name(tool: &str) -> &'static str { match tool { @@ -253,6 +264,17 @@ mod tests { assert!(!install.contains("powershell")); } + #[test] + fn grok_windows_live_bat_uses_resolved_version_not_placeholder() { + let bat = wrap_windows_lifecycle_bat( + "npm i -g @xai-official/grok@1.0.25 --registry=https://registry.npmjs.org/", + ); + assert!(bat.contains("@xai-official/grok@1.0.25"), "{bat}"); + assert!(!bat.contains("@xai-official/grok@1.2.3"), "{bat}"); + assert!(bat.contains("@echo off"), "{bat}"); + assert!(bat.contains("call npm"), "{bat}"); + } + #[test] fn grok_windows_explicit_native_install_uses_official_powershell() { let native = static_fallback_command_for("grok", ToolLifecycleAction::InstallNative); From 379bb0d113702421779e01eb4f49f4cf6c13c0aa Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:27:46 +0800 Subject: [PATCH 03/12] refactor(ui): simplify secondary pages and close validation gaps --- .trellis/spec/backend/claude-code-cli.md | 7 + .trellis/spec/backend/proxy-runtime.md | 7 + .trellis/spec/backend/task-runner-contract.md | 7 + .trellis/spec/frontend/mcp.md | 7 + .trellis/spec/frontend/prompts-memory.md | 10 + .trellis/spec/frontend/quality-guidelines.md | 23 ++- .trellis/spec/frontend/skills.md | 8 +- .trellis/spec/frontend/surfaces-responsive.md | 13 +- .trellis/spec/frontend/user-facing-copy.md | 45 ++++- .trellis/spec/frontend/visual-language.md | 14 ++ .../tasks/09-14-concise-renderer/audit.md | 58 ++++++ .../tasks/09-14-concise-renderer/check.jsonl | 7 + .../09-14-concise-renderer/commit-plan.md | 107 ++++++++++ .../tasks/09-14-concise-renderer/design.md | 59 ++++++ .../09-14-concise-renderer/implement.jsonl | 7 + .../tasks/09-14-concise-renderer/implement.md | 33 ++++ .trellis/tasks/09-14-concise-renderer/prd.md | 48 +++++ .../tasks/09-14-concise-renderer/research.md | 18 ++ .../research/performance-instrumentation.md | 67 +++++++ .../research/validation-repairs.md | 97 +++++++++ .../tasks/09-14-concise-renderer/task.json | 26 +++ .../verification-initial.md | 164 +++++++++++++++ .../09-14-concise-renderer/verification.md | 186 ++++++++++++++++++ config/playwright.performance.config.ts | 4 +- .../supported-platform-structure-assets.json | 18 +- src-tauri/src/services/provider/mod.rs | 20 +- src-tauri/src/services/tooling/grok_npm.rs | 9 +- src-tauri/src/services/tooling/versions.rs | 5 +- src/app/styles/features.css | 62 +----- src/app/styles/tokens.css | 2 - src/pages/agents/AgentAssignmentSections.tsx | 6 +- src/pages/agents/AgentAuthStatusPanel.tsx | 15 +- src/pages/agents/AgentPromptsSection.tsx | 3 +- src/pages/agents/Page.tsx | 2 +- src/pages/auth/AccountView.tsx | 62 +++--- src/pages/auth/CodexRequestSource.tsx | 3 - src/pages/auth/ConnectionsView.tsx | 6 +- src/pages/auth/LoginDialog.tsx | 2 +- src/pages/auth/Page.tsx | 6 +- src/pages/auth/page.css | 19 +- src/pages/health/Page.tsx | 24 +-- src/pages/health/page.css | 3 + src/pages/mcp/Page.tsx | 109 +++------- src/pages/memory/Page.tsx | 21 +- src/pages/memory/page.css | 16 +- src/pages/models/ModelConnectivityTest.tsx | 2 +- src/pages/models/OpenCodeModelsPanel.tsx | 6 +- src/pages/models/Page.tsx | 4 +- src/pages/models/QoderModelsPanel.tsx | 5 +- src/pages/models/TraeModelsPanel.tsx | 5 - src/pages/models/XaiSubscriptionSection.tsx | 4 +- src/pages/prompts/Page.tsx | 21 +- src/pages/skills/Page.tsx | 104 +++------- src/shared/ui/WorkBuddyTrustDialog.tsx | 4 +- tests/architecture/rootGovernance.test.ts | 34 ++++ tests/browser/auth.spec.ts | 1 + tests/browser/responsive-density.spec.ts | 18 +- tests/browser/scroll-ownership.spec.ts | 99 ++++++++++ tests/codexWindowsUserScopeContract.test.ts | 50 ++++- tests/renderer/app/actWarningGuard.test.ts | 15 ++ tests/renderer/app/setup.ts | 7 +- tests/renderer/app/userFacingCopy.test.ts | 42 ++++ tests/renderer/features/featurePages.test.tsx | 120 ++++++----- tests/renderer/pages/agents/Page.test.tsx | 12 +- tests/renderer/pages/auth/Page.test.tsx | 21 ++ tests/renderer/pages/health/Page.test.tsx | 6 + tests/renderer/pages/memory/Page.test.tsx | 2 +- tests/renderer/pages/models/Page.test.tsx | 10 +- 68 files changed, 1573 insertions(+), 454 deletions(-) create mode 100644 .trellis/tasks/09-14-concise-renderer/audit.md create mode 100644 .trellis/tasks/09-14-concise-renderer/check.jsonl create mode 100644 .trellis/tasks/09-14-concise-renderer/commit-plan.md create mode 100644 .trellis/tasks/09-14-concise-renderer/design.md create mode 100644 .trellis/tasks/09-14-concise-renderer/implement.jsonl create mode 100644 .trellis/tasks/09-14-concise-renderer/implement.md create mode 100644 .trellis/tasks/09-14-concise-renderer/prd.md create mode 100644 .trellis/tasks/09-14-concise-renderer/research.md create mode 100644 .trellis/tasks/09-14-concise-renderer/research/performance-instrumentation.md create mode 100644 .trellis/tasks/09-14-concise-renderer/research/validation-repairs.md create mode 100644 .trellis/tasks/09-14-concise-renderer/task.json create mode 100644 .trellis/tasks/09-14-concise-renderer/verification-initial.md create mode 100644 .trellis/tasks/09-14-concise-renderer/verification.md create mode 100644 tests/renderer/app/actWarningGuard.test.ts diff --git a/.trellis/spec/backend/claude-code-cli.md b/.trellis/spec/backend/claude-code-cli.md index 54b855973..8a104063e 100644 --- a/.trellis/spec/backend/claude-code-cli.md +++ b/.trellis/spec/backend/claude-code-cli.md @@ -194,6 +194,13 @@ Helper contract tests must require `npm.cmd` discovery plus `.raw_arg(&command_line)` / `call {quoted_program}` and must not accept `Command::new(npm.cmd)`. +Host-side npm command-string adapters used only by Windows are compiled under +`#[cfg(any(target_os = "windows", test))]`, with matching conditional imports. +Pure tests still run on the development host; the unused Windows adapters do +not enter a normal macOS build. Do not add `allow(dead_code)` to silence this +ownership mismatch. Check both the normal library and all test targets with +`mise run check:backend`; portable tests do not prove Windows execution. + ## 7. Wrong vs Correct ```text diff --git a/.trellis/spec/backend/proxy-runtime.md b/.trellis/spec/backend/proxy-runtime.md index ebe722fc5..8a7dca460 100644 --- a/.trellis/spec/backend/proxy-runtime.md +++ b/.trellis/spec/backend/proxy-runtime.md @@ -208,6 +208,13 @@ backup body, or replacement routing implementation. delegation, ACL registration, and secret-negative serialization. - Service lifecycle tests cover bind failure, duplicate start, status after successful bind, plain stop, explicit stop-with-restore, and repeated stop. +- Successful listener tests use the existing `ProxyConfig.listen_port = 0` + test configuration and assert URLs against the port returned by `start()`. + Assert the returned port is nonzero and stop the listener at test completion. + Do not assume the product's default port is free, probe-and-release a port, + or stop an unrelated local listener. Fixed occupied ports belong only in + deliberate bind-failure fixtures; application test serialization is not an + OS-wide port reservation. - Takeover tests cover each supported application projection, backup reuse and mismatch, lock conflicts, write failure, readback mismatch, compensation success/failure, and active-state publication only after verification. diff --git a/.trellis/spec/backend/task-runner-contract.md b/.trellis/spec/backend/task-runner-contract.md index 7671f8e61..1a6dbfd35 100644 --- a/.trellis/spec/backend/task-runner-contract.md +++ b/.trellis/spec/backend/task-runner-contract.md @@ -351,6 +351,13 @@ rerun `supported-platform:check`; a previously green manifest does not cover later source edits. Do not bulk-refresh unreviewed entries or disable the seal to unblock the always-running CI Changes job. +The manifest's canonical ordering is `path.localeCompare(other, "en")`, not +raw byte sorting. Adding a platform guard to a previously platform-neutral +module also adds a candidate: review that file and add its identity rather +than excluding it. When repairing a stale source-text test, assert the complete +owning platform block and add negative cases for moving/widening the protected +operation; attribute/call adjacency alone is not an authority boundary. + The checker and both inventories must remain runnable from a clean checkout using only Node built-ins. The always-running CI Changes job invokes this path before dependency installation, so importing a package or a helper with a diff --git a/.trellis/spec/frontend/mcp.md b/.trellis/spec/frontend/mcp.md index 7018ecdaa..c10aa53e0 100644 --- a/.trellis/spec/frontend/mcp.md +++ b/.trellis/spec/frontend/mcp.md @@ -170,6 +170,13 @@ untrusted or versioned response must add parsing at this adapter boundary. ### Discovery, import, assignment, and trust copy +- Installed detail shows source and transport once in its header and keeps + configuration/provenance in one flat metadata section. Missing optional + descriptions and absent local directories do not produce filler rows. + Editable assignment switches replace the former read-only assignment card. +- Copy-only directory assertions target `CopyablePath` itself. An explicitly + displayed absolute command can legitimately contain the same directory; + combining metadata must not turn that fact into a false redaction failure. - Discovery uses the local reviewed catalog and builds an `McpServer` for `McpPort.upsert`; it is not a runtime/network test of the recipe. - Import delegates to `importFromApps()` and reports the returned imported diff --git a/.trellis/spec/frontend/prompts-memory.md b/.trellis/spec/frontend/prompts-memory.md index a605df7c9..4be019aab 100644 --- a/.trellis/spec/frontend/prompts-memory.md +++ b/.trellis/spec/frontend/prompts-memory.md @@ -173,6 +173,16 @@ The fixed resource mapping is: ### Query, write, and navigation behavior +- Optional list/preview descriptions are omitted when absent. Loading and + selection states do not add subtitles repeating their titles. Memory keeps + missing-file and dirty badges, not an ordinary `已读取` badge. Failure, + stale/refresh warnings, native limits and discard/delete confirmations remain. +- Memory editor titles use the shared section token. The existing flex header + wraps the toolbar as a group before compressing its buttons into unnecessary + vertical rows; truly narrow toolbars may still wrap. Content and actions stay + within their pane and the editor retains its own scrolling. The toolbar's + copy-only `.fy-feature-path` is `width:auto`; the shared metadata-row default + `width:100%` would force the surrounding actions into separate rows. - Query keys are partitioned by app, document, daily file, search string, and Hermes limits. A mutation invalidates only resources it can affect. - Each page owns a mutual-exclusion write lock. Repeated clicks while a write is diff --git a/.trellis/spec/frontend/quality-guidelines.md b/.trellis/spec/frontend/quality-guidelines.md index f2651721e..c30be32a7 100644 --- a/.trellis/spec/frontend/quality-guidelines.md +++ b/.trellis/spec/frontend/quality-guidelines.md @@ -113,6 +113,16 @@ timers. A dependency warning may be allowlisted only by one exact message and reviewed version, with an upstream reference and removal condition; broad regular-expression suppression is prohibited. +The shared `console.error` act-warning guard is installed in `beforeEach`, not +`beforeAll`: the renderer's `restoreMocks: true` restores spies before each +test, including the first. `actWarningGuard.test.ts` exercises two consecutive +tests against the real setup so a disabled guard cannot silently pass. Preserve +the normal console path for messages outside that specific fail-fast guard. +When a fixture resolves an installation job, await the resulting readiness and +inventory readback UI before ending the test. Resolving a deferred Promise is +not proof that its chained state updates have reached the DOM; do not replace +the wait with a sleep, suppressed warning, or changed product lifecycle. + Route/lifecycle tests prove both sides of lazy ownership: prefetch may request an unvisited module, but its page is not mounted and creates no queries/observers; a visited primary route stays mounted behind `PersistentSurface` with queries disabled @@ -137,12 +147,23 @@ initialization from its helpers and produce cross-chunk cycles. For navigation profiling run `mise exec -- pnpm exec playwright test --config config/playwright.performance.config.ts`. It uses a serial production server, -1232×700 viewport, 42 revisits at 1× and 4× CPU cost, CPU profiles and long-task +1232×700 viewport, six revisits per route at 1× and 4× CPU cost, CPU profiles and long-task records. The normal-speed local target is p95 ≤100ms from semantic link activation to the frame after visible destination DOM; it excludes OS input dispatch, data freshness and animation settling. Report those limits, not a claim about all native WebViews. Do not raise the existing build budgets. +The performance configuration keeps `trace: "off"`, one worker and zero retries. +Playwright `retain-on-failure` still records every run and adds screenshot/DOM +capture overhead; it is not a zero-cost recorder activated only after a failure. +Functional browser checks retain their normal failure traces. For a diagnosis, +rerun the failing case separately with `--trace on`; label those timings as +instrumented, not accepted performance measurements. CPU profiles, frame samples, +long tasks, real resize geometry and cleanup assertions remain enabled in the +normal benchmark. Do not remove animations or loosen the 33.4ms/100ms budgets. +`tests/architecture/rootGovernance.test.ts` loads both actual configs in native +Node and checks this separation while retaining all four timing suites. + The same production configuration also runs `presentation-performance.spec.ts`. It also selects `dialog-origins.spec.ts` and `mcp-followup-origins.spec.ts`: production CSS/chunks must preserve asynchronous entry timing, transient-source diff --git a/.trellis/spec/frontend/skills.md b/.trellis/spec/frontend/skills.md index 8fae396dd..4ec02a479 100644 --- a/.trellis/spec/frontend/skills.md +++ b/.trellis/spec/frontend/skills.md @@ -159,8 +159,8 @@ Port/query layer but are not the current Skill discovery UI path. dialog that does not exist. - Uninstall requires the confirmation dialog and calls `uninstall(id)`. Its result has optional `backupPath`; UI/spec logic must not treat backup creation - as guaranteed evidence. Current confirmation wording is not authority for - whether a backup was actually created. + as guaranteed evidence. Confirmation describes removal from the managed + list and enabled apps, without promising a recoverable backup. - Backup deletion requires confirmation. Restore selects one closed target via the shared radio panel and calls `restoreBackup(backupId, target)`. - Sync method is read from current settings, saved through `SettingsPort`, then @@ -172,6 +172,10 @@ Port/query layer but are not the current Skill discovery UI path. ### Paths, links, copy, and evidence +- Installed detail shows source once in a header badge, optional description + only when supplied, and one flat installation section containing repository, + directory, dates and links. The existing assignment switches are the only + detailed assignment presentation; there is no duplicate read-only app card. - Installed detail intentionally exposes `skill.path` when observed, otherwise `directory`, through `CopyablePath(revealValue=false)`. This is explicit user-initiated path UI; do not claim paths never enter the renderer. diff --git a/.trellis/spec/frontend/surfaces-responsive.md b/.trellis/spec/frontend/surfaces-responsive.md index 99411d561..4a32d883d 100644 --- a/.trellis/spec/frontend/surfaces-responsive.md +++ b/.trellis/spec/frontend/surfaces-responsive.md @@ -125,10 +125,11 @@ CSS consumes the blur/rim/sheen tokens directly, including preference changes. must not inflate a 31px row to hundreds of pixels in WebKit. Shared bulk presentation uses explicit name/action slots and one content-box breakpoint; see [Assignments](./assignments.md). No repaint timer or route remount is a fix. -- Info cards use `--fy-info-card-min:256px` and `--fy-info-card-gap` with - intrinsic Grid sizing, capped at two columns. Admission depends on the actual - grid width, not a window breakpoint; a full-span item must not keep a blank - third column. `align-items:start` preserves natural short-card height. +- Installed Skills/MCP use one full-width `.fy-feature-info-card` in a + single-column `.fy-feature-info-grid`, with a leading hairline rather than + another bordered card. The former two-column source/assignment/date cards + and their card-floor tokens are retired. Assignment switches own assignment + state; header badges own source/transport, without duplicate metadata rows. Metadata uses `fit-content(var(--fy-definition-label-cap)) minmax(0,1fr)`; the shared label cap is `min(30%,8em)`, with `--fy-definition-gap` between name and value. Caption-level definition rows span the card: values sit on @@ -152,7 +153,7 @@ CSS consumes the blur/rim/sheen tokens directly, including preference changes. | ---------------------------------------------------- | ----------------------------------------------------------------------------------- | | Pane is narrow in a wide window | Form stacks based on container; long text remains within its pane. | | Three-pane window grows after shrinking | Middle detail absorbs growth; auxiliary rails obey pixel bounds and drag choices. | -| Detail has less than two card floors plus one gap | One full-width card column, regardless of viewport width. | +| Installed Skill/MCP detail grows or shrinks | One full-width metadata section; no duplicate source or read-only assignment card. | | WebKit crosses two/three-pane admission repeatedly | Rows remain intrinsic; no accumulated height or force-remounted content. | | URL/identity has no natural breaks | Wrap in detail; no horizontal escape or lost action. | | Standard/comfortable dialog at small viewport | Body scrolls as needed; footer actions remain reachable. | @@ -175,7 +176,7 @@ glass. - `responsive-density.spec.ts` exercises Skills/MCP in both themes and engines: 1564→1232→1180→900→1181→1564, content-relative row heights, uniform atomic - bulk pairs, width growth, real drag/keyboard/reset, bounded long metadata, + bulk pairs, width growth, one full-width metadata section, real drag/keyboard/reset, bounded long metadata, 616px/font-enlargement pressure and draft/hidden-route lifetime. Check both initial and post-resize geometry; no-overflow alone misses inflated rows. - `scroll-ownership.spec.ts` covers long Skills/MCP installed lists with wheel diff --git a/.trellis/spec/frontend/user-facing-copy.md b/.trellis/spec/frontend/user-facing-copy.md index fca04d1d0..170f7ffe3 100644 --- a/.trellis/spec/frontend/user-facing-copy.md +++ b/.trellis/spec/frontend/user-facing-copy.md @@ -124,6 +124,38 @@ Examples: accessible label may match the visible sentence when needed for assistive technology. +### Concise secondary surfaces + +The object name, meaningful state and actions are the default hierarchy across +all eight routes and their details/dialogs. A heading does not require a +subtitle. Add supporting text only for a non-obvious choice, consequence, +limitation or recovery step; do not explain an already-labelled control. + +- In installed Skills/MCP details, show source/transport once in the header, + configuration facts in one metadata section, and assignment state in the + existing editable switches. Do not add a second read-only assignment card. +- Omit absent optional descriptions instead of filling every row with + `暂无说明` / `暂无描述`. An unknown operational state is different: preserve + unknown authentication, stale reads, missing files and unsupported actions. +- Ordinary document reads do not need an `已读取` badge. Missing, dirty, + failed and stale states remain visible; never remove user-authored content. +- Omit an optional target subtitle when it exactly repeats the software heading; + retain a different target/configuration name. A normal Health badge plus the + fixed local-check limitation does not also need a paragraph restating success; + non-ready causes and individual check facts remain visible. +- A removal button does not need its own `危险操作` introduction. Keep the + named destructive action and its real impact/confirmation, including affected + connections and files. Never infer a guaranteed backup from an optional result. +- Keep model costs, managed-account versus request-source distinctions, + external trust steps, credential impact and cancellation limits at their + action points. Concision is not permission to hide these in hover help. + +For example, render `` with its spinner, +not an additional `description="正在读取已安装的 Skills"`. For an error, retain +the specific safe cause/retry path rather than removing every description. +Use ordinary task-specific words; no rigid character quota, universal paragraph +ban, or punctuation-based test of authorship is part of this contract. + ### Confirmation and safety copy Claude's Agent card may offer the closed CLI installer and official login @@ -215,7 +247,8 @@ on those facts. Before merging user-visible text, verify: -- [ ] The first sentence says what happened or what the screen is for. +- [ ] The heading or first sentence identifies the task, object or state without + repeating it in a subtitle, summary card and control. - [ ] Every technical term is one the target reader must recognize or use. - [ ] Errors and uncertain states include a safe next step. - [ ] Copy does not reveal an opaque token, event sequence, adapter, projection, @@ -244,6 +277,10 @@ mise run build:renderer mise run format:check ``` -The Renderer test suite contains a focused source contract for reviewed forbidden -phrases. It is a regression guard for known implementation narration, not a -substitute for human review of meaning and context. +`tests/renderer/app/userFacingCopy.test.ts` checks all eight page families for +reviewed retired narration, not AI authorship. `featurePages.test.tsx` checks +single-owner metadata, actual switch state, paths and secrets. Browser +`responsive-density.spec.ts` and `scroll-ownership.spec.ts` check the compact +layout, readable body text, controls and document content across themes and +engines. These are regression guards, not substitutes for reviewing meaning or +native-platform acceptance. diff --git a/.trellis/spec/frontend/visual-language.md b/.trellis/spec/frontend/visual-language.md index 41ee9cf13..53287307e 100644 --- a/.trellis/spec/frontend/visual-language.md +++ b/.trellis/spec/frontend/visual-language.md @@ -44,6 +44,16 @@ Origin geometry, conditional-session keys and enter/exit timing are owned by ## 3. Contracts +- Secondary details lead with one object title and its actions. Do not stack + an introductory heading, explanatory subtitle and summary cards that all + repeat the same facts. `.fy-feature-intro` uses the 14px body token and + `--fy-line-body`, not section-title sizing. +- Installed Skills/MCP use one full-width metadata section with a leading + hairline, not nested source/assignment/install cards. Account sections use + the same flat grouping principle; actual software-connection cards retain + their object boundaries. Do not shrink controls or body text to compensate + for excessive content. Container/definition behavior remains owned by + [Surfaces and Container Response](./surfaces-responsive.md). - Dialog title and description have explicit role classes. Keep one scrolling content region (header + optional body) and a nonshrinking action footer. Long descriptions must not push actions outside the viewport. @@ -112,6 +122,10 @@ Bad: a page overrides every shared control, adds a second focus trap, or uses capped widths, visible footer, selected host/lens geometry and screenshots. - Existing multi-viewport shell, account, model/editor and keyboard tests; `mise run typecheck`, `mise run lint`, `mise run test:unit`, `mise run test:browser`. +- `scroll-ownership.spec.ts` captures populated secondary views in both themes + and checks body sizing, document content, single metadata ownership and + reachable assignment controls; screenshots are review artifacts, not new + automatically approved desktop baselines. - Browser fixture images do not prove native window chrome, platform fonts on every host, all contrast pairs, or subjective final-product acceptance. diff --git a/.trellis/tasks/09-14-concise-renderer/audit.md b/.trellis/tasks/09-14-concise-renderer/audit.md new file mode 100644 index 000000000..3dfa121ed --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/audit.md @@ -0,0 +1,58 @@ +# Renderer audit + +## Scope and decisions + +All eight production route families, their secondary panels and shared dialogs +were reviewed. This is a presentation change, not an authentication, installation, +configuration or storage redesign. + +| Surface | Removed or consolidated | Deliberately retained | +| --- | --- | --- | +| Agents | Duplicate loading/error narration; missing-description filler; verbose CLI login confirmation | Product sections, actual assignments, retry actions, install-source/authorization distinctions, credential ownership and cancellation limits | +| Auth | Generic page/selection introductions; repeated connection count and identical target name; dedicated danger-introduction card; nested section boxes | Account identity versus model/request source, actual software cards, distinct target labels, removal preview, official login/credential/file impact | +| Health | Page/sort/loading instructions; duplicate unchecked count; routine success paragraph repeating the status and remote-service limitation | All 12 checks and 13 timestamps, non-ready reasons, stale/read failures, explicit remote-service/usage limitation, check/stop/retry actions | +| Models | Duplicate Qoder/TRAE unsupported notices; repeated overwrite confirmation; long selection/usage narration | Unsupported/read-only behavior, actual selected IDs, unsaved state, irreversible deletion, billed connectivity test, experimental subscription and background-process limits | +| Skills | Three competing source/assignment/date cards become one flat installation section; absent descriptions and repeated loading text omitted | Source badge once, repo/branch/path/dates/docs, copy-only directory, real seven-target switches, uninstall impact; no guarantee of an optional backup | +| MCP | Same one-section detail; no empty directory row, duplicate transport/source or assignment summary | Commands and redacted arguments/URLs, env/header counts, ID/provenance, editor secrets confined to their existing owner, real assignments and WorkBuddy trust instructions | +| Prompts | Loading/selection subtitles, absent description filler and verbose discard text | User content, actual enable controls, native-only limits, current-file inspector, dirty/discard/delete checks and navigation authority | +| Memory | Normal `已读取` badges, redundant loading/selection narration and wordy discard text; header uses section token and toolbar avoids unnecessary vertical stacking | Missing/dirty/error/stale states, four fixed resources, full user text, per-resource limits, file paths, daily search and real actions | + +## Shared presentation + +The existing role scale is retained: 14px body, 13px control, 12px metadata and +16px section headings. Skills/MCP descriptions are ordinary body text, not +another heading. Metadata uses its existing definition/copy layout and narrow +container behavior, with a leading divider instead of nested summary cards. +Real assignment controls remain visible; removing a duplicate read-only summary +does not remove supported or hidden native assignment flags. + +Do not interpret this task as a prohibition on all help, all subtitles or all +repeated strings. A selected object must remain identifiable in master and detail; +a warning may need to appear at both its summary and action point. Distinct +connection targets, failure causes and native-safety facts are not filler. + +## Review findings corrected + +- Merging metadata exposed an imprecise test: an absolute MCP command legitimately + contains the installation directory. Copy-only path assertions now scope the + actual path control, while command rendering and secret redaction remain tested. +- A normal Auth fixture includes an outstanding official login. Screenshot + readiness waits for actual page content and excludes loading empty states, + not all spinners; product session behavior was not changed to satisfy a test. +- Visual review found identical `Codex` heading/subtitle and an unfiltered + `40 / 40` account count. Those repetitions were removed; a distinct target + label and meaningful filtered count are still displayed. +- Visual review found normal Health status followed by another success paragraph + repeating the remote-test limitation. The normal paragraph is omitted while + the scope warning, non-ready reasons and all check rows remain. +- Memory actions were shrinking into a tall stack despite sufficient total pane + width. The copy-only path control inherited the metadata-row `width:100%`. + It is now auto-width in the toolbar, and the existing flex header wraps the + intact toolbar as a group; controls still wrap inside a truly narrow pane. + +## Evidence limits + +Browser fixtures exercise renderer DOM, layout, actions and accessibility; they +are not native macOS/Windows acceptance or live-account tests. No real login, +installation, credential change, model request or native configuration write was +performed. No release, approved visual baseline or remote branch was published. diff --git a/.trellis/tasks/09-14-concise-renderer/check.jsonl b/.trellis/tasks/09-14-concise-renderer/check.jsonl new file mode 100644 index 000000000..b2df11bfe --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/check.jsonl @@ -0,0 +1,7 @@ +{"file": ".trellis/spec/frontend/user-facing-copy.md", "reason": "Concise copy, actionable states and safety meaning"} +{"file": ".trellis/spec/frontend/visual-language.md", "reason": "Shared typography and dialog hierarchy"} +{"file": ".trellis/spec/frontend/surfaces-responsive.md", "reason": "Container sizing, readable metadata and scroll ownership"} +{"file": ".trellis/tasks/09-14-concise-renderer/research.md", "reason": "Community feedback and primary writing guidance"} +{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Full renderer gate and fail-fast warning guard lifecycle"} +{"file": ".trellis/spec/backend/claude-code-cli.md", "reason": "Native npm adapter scope and unchanged execution authority"} +{"file": ".trellis/tasks/09-14-concise-renderer/research/validation-repairs.md", "reason": "Reviewed native identity changes and reproduced test guard failure"} diff --git a/.trellis/tasks/09-14-concise-renderer/commit-plan.md b/.trellis/tasks/09-14-concise-renderer/commit-plan.md new file mode 100644 index 000000000..5cc45385d --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/commit-plan.md @@ -0,0 +1,107 @@ +# Local commit and archive plan + +Authorization: the user's subsequent request to resolve all remaining work +approves the local work commit and archive, including the reported baseline +contract and test-warning repairs. No remote publication is authorized. + +## Work commit + +Proposed message: + +```text +refactor(ui): simplify secondary pages and close validation gaps +``` + +Include the original 41 tracked UI/SPEC/test paths below, the fourteen explicitly +listed completion-repair paths (including performance and native-test isolation), +and this task's planning/research/review artifacts. +The previous UI modifications were reviewed as this same task; the completion +repairs are described separately in research/validation-repairs.md. No unrelated +dirty paths were found. Recheck before staging and stop for newly appearing, +unrecognized changes. + +```text +.trellis/spec/frontend/mcp.md +.trellis/spec/frontend/prompts-memory.md +.trellis/spec/frontend/skills.md +.trellis/spec/frontend/surfaces-responsive.md +.trellis/spec/frontend/user-facing-copy.md +.trellis/spec/frontend/visual-language.md +src/app/styles/features.css +src/app/styles/tokens.css +src/pages/agents/AgentAssignmentSections.tsx +src/pages/agents/AgentAuthStatusPanel.tsx +src/pages/agents/AgentPromptsSection.tsx +src/pages/agents/Page.tsx +src/pages/auth/AccountView.tsx +src/pages/auth/CodexRequestSource.tsx +src/pages/auth/ConnectionsView.tsx +src/pages/auth/LoginDialog.tsx +src/pages/auth/Page.tsx +src/pages/auth/page.css +src/pages/health/Page.tsx +src/pages/health/page.css +src/pages/mcp/Page.tsx +src/pages/memory/Page.tsx +src/pages/memory/page.css +src/pages/models/ModelConnectivityTest.tsx +src/pages/models/OpenCodeModelsPanel.tsx +src/pages/models/Page.tsx +src/pages/models/QoderModelsPanel.tsx +src/pages/models/TraeModelsPanel.tsx +src/pages/models/XaiSubscriptionSection.tsx +src/pages/prompts/Page.tsx +src/pages/skills/Page.tsx +src/shared/ui/WorkBuddyTrustDialog.tsx +tests/browser/auth.spec.ts +tests/browser/responsive-density.spec.ts +tests/browser/scroll-ownership.spec.ts +tests/renderer/app/userFacingCopy.test.ts +tests/renderer/features/featurePages.test.tsx +tests/renderer/pages/auth/Page.test.tsx +tests/renderer/pages/health/Page.test.tsx +tests/renderer/pages/memory/Page.test.tsx +tests/renderer/pages/models/Page.test.tsx +.trellis/tasks/09-14-concise-renderer/ +``` + +Completion-repair additions: + +```text +.trellis/spec/backend/claude-code-cli.md +.trellis/spec/backend/task-runner-contract.md +.trellis/spec/frontend/quality-guidelines.md +scripts/tasks/supported-platform-structure-assets.json +src-tauri/src/services/tooling/grok_npm.rs +src-tauri/src/services/tooling/versions.rs +tests/codexWindowsUserScopeContract.test.ts +tests/renderer/app/setup.ts +tests/renderer/app/actWarningGuard.test.ts +tests/renderer/pages/agents/Page.test.tsx +config/playwright.performance.config.ts +tests/architecture/rootGovernance.test.ts +.trellis/spec/backend/proxy-runtime.md +src-tauri/src/services/provider/mod.rs +``` + +Do not include generated logs, screenshots, `node_modules`, dependency/API/data +changes or unrelated work. The two mechanical native changes above are the +entire native behavior-preserving compile patch. The final native gate also +required a test-only correction in provider/mod.rs: use the existing ephemeral +listener configuration, assert the returned port, and stop that test listener. +No production Provider behavior is modified. + +## Bookkeeping after the work commit + +Use the existing `fyagent-concise-renderer-20260914` context identity. Preserve +work-commit → archive-commit → journal-commit order; no amend and no push. +The installed archive command moves the directory but does not rewrite its +JSONL references, so use its supported `--no-commit` option, repair only this +task's relocated context paths, stage the move, and run canonical postarchive +checks without an exclusion before making the archive commit. Record the work +commit in task metadata and pass only work-commit hashes to the journal command. + +The previous proposal to archive with known failing aggregates is superseded. +Only actual final gate results in verification.md authorize completion; native +Windows/live-account/installer/signing evidence is still not inferred from +portable or host-only tests. diff --git a/.trellis/tasks/09-14-concise-renderer/design.md b/.trellis/tasks/09-14-concise-renderer/design.md new file mode 100644 index 000000000..8cc855156 --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/design.md @@ -0,0 +1,59 @@ +# Concise renderer design + +## Gap and owners + +The renderer already has shared layout and copy contracts, but secondary views repeat those facts: Skills/MCP display source badges, source prose and source rows together, plus a read-only assignment card beside assignment switches. Auth and Health introduce already-labelled sections in prose. Loading states often narrate the same query twice. + +Presentation lives in `src/pages/**`, feature-aware controls in `src/shared/features/**`, and role styling in `src/app/styles/**` / existing page CSS. Those are the change boundary; hooks, native ports, DTOs and persisted configuration remain unchanged. + +## Design + +- Keep each page's recognizable title, toolbar, selected object and actionable status. Remove subtitles that repeat the heading or describe obvious controls. +- Skills/MCP retain one source/configuration metadata owner and the existing assignment panel, not a duplicate assignment summary. Keep copy-only paths, redaction and provenance intact. Use a flat, compact detail section instead of three competing summary cards. +- Auth/Agent/model/editor sections retain labels and meaningful distinctions; put explanations beside the actual exceptional action rather than on every normal screen. +- Preserve direct visible warnings for deletion, credential changes, billed connectivity checks, stale/unknown results and external trust steps. Never move safety-critical content into hover-only help. +- Reuse existing Collapsible only where a genuinely optional information section needs disclosure. Avoid new shared state or universal page abstractions. +- Keep body/control/caption sizes readable, use existing spacing/radius roles and maintain natural-height rows, bounded panes and reachable footers. + +## Compatibility and rollback + +No dependencies, transport/command changes, route changes or persisted-state changes. Existing selection, dirty blockers, focus lifecycle, action callbacks, secret clearing and native authority remain the owners. The completion repair additionally scopes Windows-only native command adapters to Windows/test builds and removes a redundant return binding; it does not change native execution authority. Review the diff for accidental logic edits. Reverting the presentation/test/SPEC patch restores the old appearance without a data migration. + +## Verification boundaries + +### Completion repair boundary (user-authorized 2026-09-14) + +The reported baseline failures are part of this task's completion scope. Inspect +the native diff that invalidated the source seal before updating any individual +digest. The stale Codex contract currently assumes the macOS PATH helper call is +immediately after its cfg attribute; production now places both login-shell and +process PATH calls inside a macOS-only block. Test the actual block boundary and +Windows exclusion, not an unrelated adjacency fragment. Native behavior is not +changed merely to satisfy a source-text assertion. + +Renderer warnings originate in asynchronous fixture completion and potentially +the test guard lifecycle. Await meaningful terminal/readback states, not arbitrary +delays. Keep the console guard live with the project's restoreMocks policy and +add regression coverage for its lifetime. Existing native commands, product state +ownership, credentials and approved visual baselines remain unchanged. + +### Performance measurement follow-up + +The complete traced performance suite reproduced an additional frame-budget +failure. Two untraced repetitions of the same production-build resize test meet +the unchanged threshold. Following Playwright's documented recording costs, +separate timed measurement from trace diagnostics at the existing performance +configuration owner. Keep functional traces, real animations, CPU metrics, sample +counts, geometry/cleanup checks, one worker and zero retries. No product motion +or sampling/assertion code is changed. An executable configuration contract +prevents tracing from silently returning to the timing gate. + +### Native listener fixture isolation + +The last aggregate repeat exposed a pre-existing native test that binds the +product's fixed default port and assumes it is available. Use the neighboring +tests' existing ephemeral-port configuration, derive its expected profile URL +from the real `start()` result and stop the test listener. This is confined to +the existing provider unit test; no production listener or takeover code changes. + +Behavioral unit tests protect workflows and important warnings; source contracts prevent known repeated narration, not all bad writing. Browser tests exercise actual geometry, keyboard/focus, themes and scrolling with fixtures. Screenshots are renderer evidence only, not native macOS/Windows or real-account acceptance. diff --git a/.trellis/tasks/09-14-concise-renderer/implement.jsonl b/.trellis/tasks/09-14-concise-renderer/implement.jsonl new file mode 100644 index 000000000..b2df11bfe --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/implement.jsonl @@ -0,0 +1,7 @@ +{"file": ".trellis/spec/frontend/user-facing-copy.md", "reason": "Concise copy, actionable states and safety meaning"} +{"file": ".trellis/spec/frontend/visual-language.md", "reason": "Shared typography and dialog hierarchy"} +{"file": ".trellis/spec/frontend/surfaces-responsive.md", "reason": "Container sizing, readable metadata and scroll ownership"} +{"file": ".trellis/tasks/09-14-concise-renderer/research.md", "reason": "Community feedback and primary writing guidance"} +{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Full renderer gate and fail-fast warning guard lifecycle"} +{"file": ".trellis/spec/backend/claude-code-cli.md", "reason": "Native npm adapter scope and unchanged execution authority"} +{"file": ".trellis/tasks/09-14-concise-renderer/research/validation-repairs.md", "reason": "Reviewed native identity changes and reproduced test guard failure"} diff --git a/.trellis/tasks/09-14-concise-renderer/implement.md b/.trellis/tasks/09-14-concise-renderer/implement.md new file mode 100644 index 000000000..e913ef352 --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/implement.md @@ -0,0 +1,33 @@ +# Execution and verification + +## Plan + +- [x] Inspect clean checkout, Trellis workflow and frontend owner contracts; research community feedback and primary writing guidance. +- [x] Audit eight routes and shared secondary controls; record keep/remove/consolidate decisions. +- [x] Simplify Skills/MCP detail hierarchy and redundant assignment metadata using existing owners. +- [x] Simplify Agent/Auth/Health/model/prompt/memory panels, dialogs and empty/loading states without changing workflow authority. +- [x] Adjust existing role CSS only where removal exposes unnecessary space or hierarchy. +- [x] Add focused copy/layout regressions and update retired text assertions without dropping behavior/safety assertions. +- [x] Run initial frontend/browser checks and review captured layouts. Their initial results and baseline failures are retained in `verification-initial.md`; the authorized complete repair and final results are in `verification.md`. +- [x] Update frontend copy/visual/feature SPEC owners and validate task context; subsequently resolve the reproduced baseline failures under the user's completion authorization. +- [x] Record final results and limitations, review the full diff and prepare `commit-plan.md`. +- [x] Obtain local work-commit/archive approval through the user's request to complete all remaining work. +- [ ] Commit the reviewed task scope, archive the task, validate archived references and record the session. Do not push. + +## Gates + +### Remaining completion work (authorized) + +- [x] Review native changes since the platform seal, repair the stale Windows + source contract with boundary-sensitive negative coverage, update only reviewed + individual source digests and rerun the real scanner. +- [x] Reproduce React warnings, fix asynchronous test completion and guard + lifetime, and verify repeated full renderer tests with no unexpected warnings. +- [x] Execute full local prearchive, release, browser and serial production + performance checks; fix actionable failures without lowering thresholds. +- [ ] Update backend/frontend SPEC and final verification/commit plan, commit + task-scoped work locally, archive, journal and run canonical postarchive checks. + +Use repository-owned `mise` commands. Run compilation/unit gates separately from performance profiling. Use targeted runs while iterating, then the complete frontend and browser gates. Do not update approved visual baselines automatically or weaken budgets/assertions to pass. + +Before archive, review the complete diff for functionality, safety warnings, accessible names, secret/redaction boundaries, source attribution and unchanged native authority. A failed or unexecuted check must remain explicitly recorded. diff --git a/.trellis/tasks/09-14-concise-renderer/prd.md b/.trellis/tasks/09-14-concise-renderer/prd.md new file mode 100644 index 000000000..5ddbd751f --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/prd.md @@ -0,0 +1,48 @@ +# Simplify renderer copy and secondary-page layouts + +## Goal + +Audit all eight renderer routes and secondary views; remove repetitive UI narration and metadata, simplify hierarchy without hiding safety or actions, verify behavior and layout, then update SPEC before archiving. + +## Requirements + +- Review all eight product routes (Agents, Auth, Health, Models, Skills, MCP, Prompts, Memory), their secondary panels, dialogs and shared chrome. +- Lead with the object, its state and available actions. Remove repeated introductions, implementation narration, placeholder descriptions and decorative metadata cards. +- Use concise Chinese labels and task-specific messages, not slogans, generic reassurance or repeated instructions. +- Preserve decision-relevant warnings, source/provenance, configuration impact, errors, unknown/stale states, accessibility, keyboard access and existing workflows. +- Keep the existing design system, navigation, theme and motion owners. Do not introduce dependencies, native behavior changes or data migrations. The approved completion scope below includes mechanical native compile repairs. +- Update the owning SPEC before archiving this task, with reproducible verification and explicit evidence limits. + +## Acceptance Criteria + +- [x] Every product route and secondary-view family has an audit disposition recorded. +- [x] Skills/MCP details no longer duplicate the editable assignment state or repeat source/transport facts across badges and cards. +- [x] Account, Agent, model, prompt and memory secondary views use compact headings and only relevant help; safety and recovery information remains available at the point of action. +- [x] Loading/empty states do not repeat the same message in a subtitle. +- [x] Shared layout stays readable and usable in narrow/wide containers, both themes and Chromium/WebKit; no hidden controls or smaller body text used to fake simplicity. +- [x] Renderer-specific checks pass with the repaired warning guard; the previous full functional browser run passed 586 tests. Final repeat results are recorded in `verification.md`. +- [x] Affected tests, frontend gate, browser checks, build and task/SPEC contracts pass; remaining native/manual limitations are stated. +- [x] Owning frontend SPEC updates are prepared and reviewed before task archive. +- [ ] Archive the task after the authorized local work commit and successful full verification. + +## Non-goals + +No new features, API or storage changes, dependency updates, route redesign, native installs/logins, release or remote publication. + +## Authorized completion scope + +On 2026-09-14 the user requested that all reported remaining problems be resolved, +not archived under the previously proposed aggregate-gate exception. The user also +authorized the proposed local commit and archive; remote publication remains out +of scope. Continue this existing task rather than create a competing active task. + +- Resolve the three baseline contract failures after reviewing their actual source + changes; preserve Windows Shell-user authority and strict platform inventories. +- Resolve React `act(...)` warnings at their test lifecycle owner and ensure the + warning guard remains active across tests. No warning suppression or assertion + weakening is acceptable. +- Run the full local prearchive check, release contracts, complete functional + browser matrix and serial production performance suite, then update SPEC, + commit locally, archive and validate without an active-task exclusion. +- Keep real-account, Windows/macOS installer/signing and live service acceptance + distinct from local source, native-host and fixture evidence. diff --git a/.trellis/tasks/09-14-concise-renderer/research.md b/.trellis/tasks/09-14-concise-renderer/research.md new file mode 100644 index 000000000..22e646b19 --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/research.md @@ -0,0 +1,18 @@ +# Copy and hierarchy research + +Reviewed 2026-09-14. Community comments are qualitative feedback, not representative user research or an AI-authorship detector. + +## Evidence + +- Microsoft, Use simple words, concise sentences: https://learn.microsoft.com/en-us/style-guide/word-choice/use-simple-words-concise-sentences — remove words without substance; preserve precise meaning, ordinary verbs and a natural tone. +- Microsoft, Scannable content: https://learn.microsoft.com/en-us/style-guide/scannable-content/ — prioritize important information and use short headings and consistent patterns. +- Microsoft, Style and Tone: https://learn.microsoft.com/en-us/windows/win32/uxguide/text-style-tone — concrete familiar words, precise meaning and no needless explanation. This is explicitly a Windows 7-era guide; only its writing principles inform this task, not its legacy visual examples or current Windows behavior. +- Community discussion of repetitive short-form copy: https://www.reddit.com/r/content_marketing/comments/1n28hql/what_are_the_biggest_shortcomings_of_ai_generated/ — one complaint is the same point restated several ways. Ignore promotional product recommendations in the thread. +- Community discussion of interface judgment: https://www.reddit.com/r/UXDesign/comments/1tvt03p/aigenerated_ui_proves_people_value_design_but_not/ — visually polished output can still lack hierarchy, restraint, context and intent. +- Community discussion of overused phrases: https://www.reddit.com/r/copywriting/comments/1f500ys/do_you_think_the_average_person_can_spot_all_of/ — formulaic phrases predate AI; do not turn punctuation or isolated vocabulary into an authorship test. + +## Applied review questions + +Does this text identify an object, explain a non-obvious choice, report a meaningful state or help with the next action? Is the same fact already visible in a label/control/status? Would deleting it hide a cost, risk, limitation or recovery step? Can ordinary layout carry the distinction without an explanatory card? + +Retain the project's current Chinese terminology and safety contracts. No new UI dependency or copied community skill is required. diff --git a/.trellis/tasks/09-14-concise-renderer/research/performance-instrumentation.md b/.trellis/tasks/09-14-concise-renderer/research/performance-instrumentation.md new file mode 100644 index 000000000..b928a9d40 --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/research/performance-instrumentation.md @@ -0,0 +1,67 @@ +# Production timing and trace-recorder isolation + +Reviewed 2026-09-14. This is a measurement repair, not a product-motion change. + +## Observed failures + +The first performance run failed the normal-CPU content-resize budget at 33.5ms +p95. A port-conflicting invocation also truncated its shared log while the first +process continued writing; the mixed log remains in `performance-overlap.log`, +not clean acceptance evidence. A subsequent exclusive complete run still failed: +`performance-final.log` records 34 passes and one failure, content-resize p95 +50ms at 1x CPU, with no observed main-thread long tasks. The port collision alone +therefore did not explain the budget failure. + +## Discriminating experiment + +The performance config used `retain-on-failure`. This records each test and only +discards successful traces afterward; it is not a recorder started after failure. +Playwright documents recording costs and screenshot/DOM snapshot capture: + +- https://playwright.dev/docs/trace-viewer#recording-a-trace +- https://playwright.dev/docs/api/class-tracing#tracing-start +- https://github.com/microsoft/playwright/blob/main/docs/src/trace-viewer.md +- https://playwright.dev/docs/api/class-testoptions#test-options-trace — explicitly + distinguishes recording every run from retaining only failures. +- https://playwright.dev/docs/best-practices#debugging-on-ci — warns that tracing + every test adds substantial recording cost. + +Keep the same production build, real animation, viewport, real clock, twenty warm +cycles and 33.4ms assertion. Change only the trace recorder for diagnostics: + +| Evidence log under node_modules/.cache/concise-renderer | Result | +| --- | --- | +| `performance-no-trace-ab.log` | Two serial 1x repetitions pass; both p95 33.4ms | +| `resize-without-trace.cYn7Fw` | Independent 1x and 4x cases pass; both p95 33.4ms | + +Neither diagnostic changes product code, sample counts or budgets. These results +support trace instrumentation as the cause of the observed budget failures on +this host, not a claim that every native WebView has identical timing. + +## Repair and protection + +Use Playwright's existing `trace: off` in the dedicated performance config. +Functional tests retain their normal failure traces. An explicit `--trace on` +run remains available for diagnosis, with its timings labelled instrumented. +CPU profiles, frame/long-task reports, real intermediate-size and cleanup +assertions, one worker and zero retries are unchanged. + +`tests/architecture/rootGovernance.test.ts` imports both real configs in native +Node and checks this separation and all four timing suites. The new assertion +failed before the config repair (`performance-config-red.log`) and all five +ownership tests pass afterward (`performance-config-green.log`). + +The final gate is the entire canonical `mise run test:performance`, not the +diagnostic CLI override. A distinct log is used for each new run so a repeated +invocation cannot overwrite in-flight evidence. Final results belong in +`verification.md`; failed and diagnostic runs are retained separately. + +## Final canonical result + +`performance-accepted.gRDHIN` records all 35 production tests passing in 2.7 minutes. +No diagnostic CLI overrides were used. Normal CPU navigation p95 is 31.5ms; +presentation and content-resize frame p95 are 33.4ms, with theme segments at most +16.8ms. At 4x CPU cost navigation p95 is 55.3ms and both animation frame p95 +measurements remain 33.4ms. Existing rounding only removes sub-nanosecond +floating-point subtraction noise; all budgets and samples remain unchanged. +This repairs benchmark workload isolation, not the product's animation code. diff --git a/.trellis/tasks/09-14-concise-renderer/research/validation-repairs.md b/.trellis/tasks/09-14-concise-renderer/research/validation-repairs.md new file mode 100644 index 000000000..6e45a4636 --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/research/validation-repairs.md @@ -0,0 +1,97 @@ +# Completion-gate repair review + +Reviewed 2026-09-14 after the user's request to resolve every reported blocker. +This supersedes the earlier proposed baseline-failure exception. No remote write, +real login, CLI installation, live inference or visual-baseline approval is part +of this work. + +## Test lifecycle: reproduced failure and fix + +The renderer configuration has `restoreMocks: true`; its console guard was +installed in `beforeAll`. Two new guard tests both failed before any repair: +Vitest restored the spy before the tests and the warning was merely printed. +Moving installation to `beforeEach` retains the same exact warning rejection, +while reinstalling it after the runner's mock restore. Other console messages +still go to the original console. The install-target test also ended immediately +after resolving an action-job Promise. It now awaits the next installation-target +button after readiness/inventory readback and verifies configuration stays +disabled because the fixture still reports not-installed. No product timing or +state machine was modified. + +Primary references (consulted in the adopted tool's version where available): + +- https://v3.vitest.dev/config/#restoremocks — restores spies before each test; + this repository uses Vitest 3.2.7 rather than inferring behavior from newer docs. +- https://react.dev/reference/react/act — flush pending updates; prefer awaited + asynchronous interactions. React Testing Library wraps its helpers with act. +- https://testing-library.com/docs/dom-testing-library/api-async/ — await the + actual DOM condition with findBy/waitFor rather than assuming Promise completion. + +The guard's negative run is `node_modules/.cache/concise-renderer/guard-red.log`; +the corrected complete renderer run is `renderer-guard-first.log` (680 tests, +no unexpected warning). These are local generated logs, not published artifacts. + +## Reviewed platform source identity + +`git diff 38b4a986..b2cdc098` proves the five stale inventory entries came from +the two already-committed CLI changes, not the UI patch. Each changed source +was reviewed before changing its individual digest: + +| File under src-tauri/src/services | Reviewed change and preserved boundary | +| --- | --- | +| tooling.rs | macOS login/process PATH reads moved into one macOS cfg block; Windows still uses the frozen Shell-user paths and local-path filtering. Development Grok npm commands use live resolved plans; formal builds still return through the ordinary-user helper before that branch. | +| tooling/claude.rs | npm metadata replaces bundled version data; selected installation follows PATH default; Windows execution still uses the established helper, closed plan and reobservation. | +| tooling/grok.rs | PATH-default owner determines distribution; resolved exact npm manifests replace compiled versions; Windows command formatting consumes reviewed plans rather than renderer strings. | +| tooling/lifecycle.rs | Windows-only batch wrapper preserves call and nonzero exit propagation; no new renderer command or scope was introduced. | +| tooling/versions.rs | existing macOS/native versus non-macOS dispatch remains; registry fallback replaces compiled versions. The unnecessary let-and-return introduced by that commit is removed without changing the expression. | + +The stale Codex Windows test required a macOS cfg directly adjacent to a helper +call. The actual helper calls now live inside the same macOS-only block. The +replacement assertion checks that complete formatted function/block, both PATH +sources and no matching call/read in other branches. Mutating the guard to admit +Windows, moving or copying the process PATH read outside must fail. The scanner itself, +all candidate/mode/digest checks and its negative fixtures remain unchanged. + +## Additional native build findings + +The first full backend run failed Clippy on three Windows-only npm command +adapters compiled into the ordinary macOS library, plus a redundant let-return. +The adapters and their two exclusive imports now have matching explicit +`cfg(any(target_os = "windows", test))`. Their existing pure unit tests remain +available on the host; no warning allowance was added. This makes grok_npm.rs a +new platform-sensitive candidate, so its fully reviewed source is added to the +identity inventory. Entries use the existing English locale comparator and +SHA-256 of final rustfmt-checked bytes. No bulk refresh or checker exemption. +The modified Codex Windows contract test is itself a sealed source; its reviewed +negative-case additions are included with the final formatted test digest. + +Owning contracts: frontend/quality-guidelines.md, backend/claude-code-cli.md, +and backend/task-runner-contract.md (identity seals and exact active-task gate). +The latter is read directly; it is not injected as an oversized context file. + +The later production-frame measurement failure and controlled trace-recorder +experiments are documented in `performance-instrumentation.md` alongside this +file. No product motion or benchmark threshold was changed to resolve it. + +## Native fixture port conflict + +The next complete aggregate repeat (`prearchive-final.log`) exposed another +pre-existing fixture defect: `update_current_claude_desktop_provider_syncs_profile_when_proxy_takeover_is_active` +failed with `Address already in use` while starting the default 15721 listener. +The specific competing listener was no longer present when inspected; its origin +is not inferred. Tests immediately beside it already use `listen_port: 0`. + +Only that existing test now uses the same `ProxyConfig` ephemeral-port input, +asserts a nonzero port from the real start result, compares the complete native +profile gateway URL to that result, and explicitly stops its listener afterward. +Backup sentinel, auth scheme and model-routing assertions are unchanged. This +removes the fixed-port assumption rather than retrying, probing/releasing a free +port, skipping the test or terminating an unrelated process. The source file's +reviewed identity seal is updated for these test-only bytes; production Provider +behavior and the scanner are unchanged. + +Primary semantics: https://docs.rs/tokio/latest/tokio/net/struct.TcpListener.html +and https://doc.rust-lang.org/std/net/struct.TcpListener.html specify that binding +port 0 lets the OS allocate the bound port, retrievable via `local_addr`. FyAgent's +existing proxy owner already exposes it in its start result; no new helper is +needed. The prevention rule is in backend/proxy-runtime.md, Tests Required. diff --git a/.trellis/tasks/09-14-concise-renderer/task.json b/.trellis/tasks/09-14-concise-renderer/task.json new file mode 100644 index 000000000..b17cc14d0 --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/task.json @@ -0,0 +1,26 @@ +{ + "id": "concise-renderer", + "name": "concise-renderer", + "title": "Simplify renderer copy and secondary-page layouts", + "description": "Audit all eight renderer routes and secondary views; remove repetitive UI narration and metadata, simplify hierarchy without hiding safety or actions, verify behavior and layout, then update SPEC before archiving.", + "status": "in_progress", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P1", + "creator": "pythonrust", + "assignee": "pythonrust", + "createdAt": "2026-09-14", + "completedAt": null, + "branch": "dev/laiyongjie", + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/.trellis/tasks/09-14-concise-renderer/verification-initial.md b/.trellis/tasks/09-14-concise-renderer/verification-initial.md new file mode 100644 index 000000000..84e014dc4 --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/verification-initial.md @@ -0,0 +1,164 @@ +# Initial verification — superseded by verification.md + +This records the first implementation and its blocked continuation. The user +subsequently required all reported blockers to be resolved. The current results, +expanded repair boundary and final acceptance are in `verification.md`; the +baseline exception and pending-confirmation state below are historical only. + +## Checkout and scope + +Base: `b2cdc098b6a70b9429e3dfcbf6a94e23c81aefc0`, branch `dev/laiyongjie`. +The checkout was clean before this task. Source changes are limited to renderer +presentation and its tests; there are no native, port, domain, dependency, +configuration-format, route, CI or persisted-data changes. + +## Completed focused checks + +| Check | Result | +| --- | --- | +| `mise run typecheck` | Pass | +| `mise run lint` | Pass | +| `mise run format:check` | Pass | +| `mise run test:unit tests/renderer src/domain` | 110 files, 758 tests pass | +| `mise run test:desktop:mock` | 7 tests pass; mock contract verified, not native evidence | +| `mise run test:desktop:visual:preflight` | Pass; read-only, no approved baseline updated | +| `mise run build:renderer` | Pass; all eight route chunks verified | +| Production boot tests in the existing performance configuration | 2 pass; startup only, not a full performance profile | +| Concise-secondary browser tests, all configured projects | 10 pass; four Chromium sizes plus WebKit, both themes, eight populated routes | +| Full functional browser matrix | 586 pass in 6.5 minutes; final log completed, no failed/skipped tests reported | +| Trellis context validation | Pass; implement/check context entries resolve | +| Task validation and task documentation inside prearchive aggregate | Pass | +| Python lock / version contracts | Pass | +| `git diff --check` | Pass | + +Final `mise run check:frontend`: typecheck, lint and format pass; the aggregate +unit run has 184 passing files and 2 failing files, with **1,640 passing tests, +3 baseline failures and 1 skipped test**. The three failures are listed below; +the full gate is not reported as passing. + +The renderer build, desktop mock/preflight and production-boot checks were rerun +separately after that aggregate stopped. The final full-browser run completed +with **586 passing tests**; the command was: + +```sh +mise exec -- pnpm exec playwright test --config config/playwright.config.ts --workers=4 +``` + +No production source was changed while that final run was executing. Its local +log is `node_modules/.cache/concise-renderer/browser-full-final.log`; the final +summary is on line 635. It is functional browser evidence, not a performance +profile or native-platform acceptance. + +## Continuation review + +The continuation reused the existing task and reviewed all 41 tracked dirty +paths against its audit and design, rather than resetting or replacing the +previous work. No additional production source or test changes were needed. + +The following commands were rerun against the current working tree: + +```sh +mise run typecheck +mise run lint +mise run format:check +mise run test:unit tests/renderer src/domain +``` + +All commands exited 0; the unit result remains 110 files / 758 tests passed. +The local combined log is +`node_modules/.cache/concise-renderer/resume-renderer-checks.log`. +The unit run did emit React `act(...)` warnings from Agent directory tests; +the earlier aggregate log contains the same warning categories. This is not +claimed as a warning-free run, and warnings were not filtered or suppressed. +Unlike the three native assertion failures below, these warnings were not +independently reproduced in an unchanged HEAD worktree during this continuation. + +The source/test diff identity at this review is: + +```text +git diff --binary -- src tests/renderer tests/browser | shasum -a 256 +ba47fce85a2e289115c8bf3cba7b7d20f1c039b3525c38da34479262a853759d +``` + +`git diff --exit-code -- src-tauri src/domain src/platform package.json +pnpm-lock.yaml config` exited 0. The existing narrow MCP and Memory captures +were inspected again: metadata stays within the detail pane and the Memory +toolbar does not collapse into unnecessary one-button rows. This spot check +does not expand the earlier manual-review claim to every capture. + +SPEC changes are present before archival. The task remains `in_progress` until +the local work-commit plan is confirmed; no commit, archive or push has been +performed in this continuation. See `commit-plan.md` for the exact file scope. + +After the closeout documents were updated, `git diff --check` and task context +validation passed again. The prearchive aggregate was also rerun: all 80 mise +tasks, task/document contracts and lockfile checks passed; the aggregate still +exited 1 on the same `src-tauri/src/services/tooling.rs` supported-platform +identity drift. The new log is +`node_modules/.cache/concise-renderer/resume-prearchive-contracts.log`. + +## Independently reproduced baseline failures + +`mise run check:frontend` includes repository native contracts, not only renderer +tests. The following failures were reproduced with the same command/filter in an +unmodified detached worktree at the base commit: + +1. `tests/codexWindowsUserScopeContract.test.ts`: the test named + `does not consume elevated-process user path environment on Windows` expects + a source-text fragment containing a macOS `cfg`/`extend_from_cli_path_env` sequence that is not + present in the checked-in native file. +2. `tests/remainingPlatformSurface.test.ts`: current repository scanner fails + with `Supported-platform structure identity drifted: src-tauri/src/services/tooling.rs`. +3. The same file's source-seal test fails on that same native inventory drift. + +Detached baseline command: + +```sh +mise run test:unit tests/codexWindowsUserScopeContract.test.ts tests/remainingPlatformSurface.test.ts +``` + +Result: 3 failures, 32 passes. Both the checked-out `tooling.rs` and its HEAD blob +were `f23ef78992b4eb3c4b848ab527f6133b5ab352b2`. The temporary worktree was removed +after confirming it had no tracked changes; its dependency symlink alone was +unlinked. Other existing worktrees were not touched. + +Prearchive command (the context ID is required): + +```sh +TRELLIS_CONTEXT_ID=fyagent-concise-renderer-20260914 mise run check:contracts:prearchive --exclude-active-task .trellis/tasks/09-14-concise-renderer +``` + +Its task/doc checks pass before the same supported-platform check fails. +`mise run release:check` also reports the existing supported-platform failure and +the same Codex Windows source-text contract, not an additional renderer failure. +No native inventory was resealed and no assertion was suppressed to report green. + +## Browser and visual review + +Existing functional tests retain assignment changes, secret/redaction boundaries, +copy actions, confirmation, focus/origin lifetimes, dirty editors, native-only +limits, row heights and scroll reachability. New regressions verify single-owner +metadata, actual switch state, optional descriptions, distinct account targets, +normal versus non-ready health copy, and compact memory toolbars. + +Screenshots are generated for eight populated routes × two themes × five +configured projects. Chromium uses 900×600, 1152×640, 1232×700 and 1440×900; +WebKit uses 1232×700. Existing responsive tests also resize the content panes and +apply long strings and enlarged text. Representative captures were visually +reviewed across all eight route families, including the narrow Memory correction; +automated assertions cover the whole configured matrix. This does not claim +manual inspection of every pixel in every capture. + +Local generated evidence lives under +`node_modules/.cache/concise-renderer/` (ignored): logs, `screenshots/` and +downsampled WebKit copies in `review/`. Browser `screenshot`/trace-on-failure +artifacts use the repository's normal temporary Playwright artifact directory. +These are review captures, not approved desktop baselines. + +## Limits + +No live account, network inference, native install, credential replacement, +Windows/macOS bundle acceptance, full performance profile or release candidate +was exercised. No benchmark budget, accessibility threshold or approved visual +baseline was relaxed. The repository-wide aggregate remains blocked as described +above even when the renderer-specific checks pass. diff --git a/.trellis/tasks/09-14-concise-renderer/verification.md b/.trellis/tasks/09-14-concise-renderer/verification.md new file mode 100644 index 000000000..b554ad08e --- /dev/null +++ b/.trellis/tasks/09-14-concise-renderer/verification.md @@ -0,0 +1,186 @@ +# Verification + +## Scope and current result + +Base: `b2cdc098b6a70b9429e3dfcbf6a94e23c81aefc0`, branch `dev/laiyongjie`. +The checkout was clean before the task. All eight renderer route families and +their secondary surfaces were audited; see `audit.md` for keep/remove decisions. + +The user's final completion request supersedes the earlier proposal to archive +with known baseline failures. The three baseline assertions, React warning +guard/lifecycle issue and four additional Clippy errors have been resolved. +The full current-host prearchive check, functional browser repeat and corrected +canonical production performance suite pass. The final aggregate repeat after +the timing-harness and native test-port repairs also exits 0. Postarchive +verification is recorded below after the task move. + +Production native changes are limited to two mechanical repairs: conditional +compilation of Windows-only npm string adapters (with host unit tests retained), +and removal of a redundant return binding. One existing native test additionally +uses an OS-assigned port and explicit listener teardown instead of assuming the +product default port is free. The reviewed platform identity manifest and source +contract tests are synchronized. No native operation, API/DTO, dependency, +configuration format, route, credential or stored user data was changed. + +## Final checks + +| Check | Result | +| --- | --- | +| Full `check:prearchive` with direct session identity | Pass; frontend, backend, environment and contract aggregates all exit 0 | +| Typecheck, ESLint, Prettier | Pass | +| Full unit suite | 187 files; 1,649 pass, 1 existing Windows-only skip, 0 failures | +| Renderer with restored act-warning guard | 104 files; 680 pass; no unexpected React warnings | +| Desktop mock | 7 pass; mock-only evidence | +| Visual preflight | Pass; no approved baseline changed | +| Rust formatting, check and all-target Clippy | Pass; 0 compiler warnings/errors, no allow/warning suppression added | +| Current-host Rust tests | 3,565 pass, 6 pre-existing explicit ignores, 0 failures across 19 result blocks | +| Task/docs/platform/Python lock/version contracts | Pass | +| Release contract aggregate | Pass; includes 619 passing contract tests, 1 host skip and 4 native-fetch tests | +| Renderer build / full functional browser repeat | Pass; all eight route chunks, 2 production boot cases and 586 functional browser cases | +| Serial production performance suite | 35 pass in 2.7m, with the corrected dedicated timing configuration; no CLI override | +| Trellis context | Both manifests resolve all 7 entries within injection limits | +| Canonical postarchive `mise run check` | Pending archive | + +The one unit skip is the existing `it.runIf(process.platform === "win32")` +host test. Rust's six explicit ignores are two backup performance diagnostics, +two live S3 tests, a real Codex corpus replay, and matching-host OS credential-store +HIL. None was newly skipped or counted as passing. These require their own inputs +or authority and are not reasons to enable real external writes during UI work. + +## Reproduction and logs + +Use the repository toolchain, not the ambient system Node: + +```sh +TRELLIS_CONTEXT_ID=fyagent-concise-renderer-20260914 \ + mise run check:prearchive --exclude-active-task .trellis/tasks/09-14-concise-renderer +mise run test:browser +mise run test:performance +``` + +The last two run sequentially after the first, so compilation and parallel +functional tests do not contaminate the serial production performance sample. +Final commands use existing thresholds, zero performance retries and the normal +project/browser configuration. No product source changes occur during these runs. + +The first browser repeat was externally terminated mid-run (107 test completions, +no failed assertion or final suite summary). Its termination at 13:49:28 coincides +with the new DevSpace service process start time and loss of the original tool +session; the exact stop trigger was not established. Preserve that log as +`browser-interrupted.log`, not passing evidence. Only its identified orphan Vite +test-server process was stopped after verifying its PID, project path and port. +The entire canonical browser command was restarted with no source or threshold +changes and completed with 2 production boot cases and 586 functional cases +passing (6.7 minutes for the functional matrix). Its process exited before the +canonical serial performance command was started; the suites did not overlap. + +Local generated evidence is under `node_modules/.cache/concise-renderer/`: +`prearchive-complete.log`, `browser-complete.log`, `performance-accepted.gRDHIN`. +The final aggregate repeat after the native test-port repair exited 0 and is +recorded as `prearchive-port-isolated.3aiKDV`: 1,649 unit passes and 3,565 Rust +passes. The earlier timing-config repeat is `prearchive-final.VdiXBx`. +The complete prearchive log records 0 React act warnings and 0 Rust compiler +warnings/errors. Generated logs/screenshots are ignored, not committed binaries. + +## Repaired failures and prevention + +The initial 3 native-source contract failures were independently reproduced at +unmodified HEAD (3 failed, 32 passed). They came from the two previous tooling +commits, not the UI patch. The five stale native identities were reviewed before +their individual hashes changed. The newly platform-scoped npm adapter and the +modified Windows contract test are themselves sealed as well. No scanner or +negative inventory assertion was relaxed. + +The old Windows assertion assumed cfg/call adjacency. The replacement checks +the complete macOS PATH block, both login/process sources and absence of those +reads in other branches. Three mutation cases reject moving or copying the PATH +read outside, or admitting Windows through a widened guard. + +Two new warning-guard tests failed before repair (`guard-red.log`): Vitest's +per-test mock restore removed the beforeAll guard. Installing it in beforeEach +restores the intended failure behavior. The Agent install-target test now awaits +readiness/inventory readback before teardown, instead of ending at deferred job +resolution. Its actual disabled configuration state is still asserted. The full +renderer passes with that guard enabled (`renderer-guard-first.log`). + +The full native gate additionally exposed three Windows-only functions unused in +the normal macOS library and a redundant let-return. The exact platform/test cfg +and direct return repairs keep native semantics and all existing pure tests. +Detailed source review, primary references and SPEC owners are in +`research/validation-repairs.md`. The initial blocked review is retained in +`verification-initial.md` as history, not current acceptance. + +Another complete repeat (`prearchive-final.log`, distinct from the successful +`prearchive-final.VdiXBx`) exposed a fixed-port fixture collision in the Claude +Desktop provider takeover test: its proxy could not bind the product default +port. The test now reuses the existing `listen_port: 0` support, asserts a nonzero +bound port in the generated profile URL and explicitly stops its own listener. +No unrelated local process was stopped. The focused native test and subsequent +complete gate both pass. Details and +the owning SPEC are in `research/validation-repairs.md`. + +### Performance measurement repair + +The traced performance configuration reproduced content-resize p95 above the +existing 33.4ms budget (33.5ms, then 50ms in an exclusive complete repeat). +`performance-final.log` is that failed instrumented run, not final acceptance. +Two unchanged-test repetitions with trace recording off passed; an independent +1x/4x diagnostic also passed. The actual configuration now separates untraced +timing from traced diagnosis, with a regression test importing both real configs. +All five configuration-ownership tests pass after the new test failed before +the repair. Functional browser failure tracing remains enabled. + +The canonical 35-test performance suite then passed with no CLI override. At +1232×700, using the serial production Chromium runner: + +| Metric | 1x CPU cost | 4x CPU cost | +| --- | --- | --- | +| Navigation p95 | 31.5ms | 55.3ms | +| Presentation frame p95 | 33.4ms | 33.4ms | +| Content-resize frame p95 | 33.4ms | 33.4ms | +| Maximum of theme-segment p95 values | 16.8ms | 16.7ms | + +Normal navigation budget remains 100ms and normal frame budget 33.4ms. The +4x values are additional pressure measurements, not expanded normal budgets. +CPU profiles, real clocks, sample counts, geometry and teardown assertions remain +enabled. No product motion or benchmark assertion was changed. See +`research/performance-instrumentation.md` for the failed evidence, official +recording semantics, controlled comparison and logging incident. + +The normal navigation/presentation/resize samples record no long tasks. At 4x, +navigation records one 53ms long task and presentation one 50ms long task; these +pressure observations are retained. Navigation uses 48 return samples per run; +content resize uses 20 warm cycles and 511/503 frame intervals at 1x/4x. The +accepted performance log has no NUL bytes and belongs to one exclusive run. + +## Browser and visual evidence + +Existing regressions retain assignment changes, copy/redaction boundaries, +confirmation, focus/origin lifetimes, dirty editors, native-only limits, intrinsic +row heights and scroll reachability. New assertions cover single-owner metadata, +actual switches, optional descriptions, distinct account targets, normal versus +non-ready Health copy and compact Memory toolbars. User-authored content remains. + +The final complete functional run passed 586 tests. Captures cover eight +populated routes × two themes × five projects (80 images): Chromium 900×600, +1152×640, 1232×700 and 1440×900; WebKit 1232×700. Existing pressure tests resize +panes continuously and use long strings/enlarged text. Representative captures +were manually reviewed across all eight families, including the corrected narrow +Memory toolbar; automated assertions cover the complete matrix. This does not +claim pixel-by-pixel manual review of every capture. + +These are renderer review captures in `screenshots/` and `review/`, not newly +approved desktop baselines. Playwright trace/attachment evidence uses the normal +temporary artifact directories. + +## Delivery boundary + +SPEC updates are prepared before the work commit and archive. The user's local +commit/archive authorization is recorded in `commit-plan.md`; no remote push, +release or publishing action is performed. Archive completion and the canonical +postarchive result will be recorded after execution. + +Current-host native tests do not prove Windows runtime, installer/signing, +real-account login, credential replacement, live model inference or packaged +WebView behavior. No such user-data operation was performed. Performance numbers +describe this host's production Chromium run, not universal native latency. diff --git a/config/playwright.performance.config.ts b/config/playwright.performance.config.ts index 09e030620..1c25b60c7 100644 --- a/config/playwright.performance.config.ts +++ b/config/playwright.performance.config.ts @@ -26,7 +26,9 @@ export default defineConfig({ ...devices["Desktop Chrome"], baseURL: "http://127.0.0.1:4175", viewport: { width: 1232, height: 700 }, - trace: "retain-on-failure", + // Tracing records every run, even when only failures are retained, and + // perturbs frame timing. Use --trace on for a separate diagnostic run. + trace: "off", }, webServer: { cwd: repositoryRoot, diff --git a/scripts/tasks/supported-platform-structure-assets.json b/scripts/tasks/supported-platform-structure-assets.json index 7c5f78cbb..3a0c4a034 100644 --- a/scripts/tasks/supported-platform-structure-assets.json +++ b/scripts/tasks/supported-platform-structure-assets.json @@ -235,7 +235,7 @@ ], [ "src-tauri/src/services/provider/mod.rs", - "62b8a26c833e6e930aac2f29dc0c418123da276f8e6e62b5208df118f121d272" + "1b33761c1ea02ba7b51fad259809c84461d98a6706131adbf348834a681d232a" ], [ "src-tauri/src/services/qoderwork.rs", @@ -267,19 +267,23 @@ ], [ "src-tauri/src/services/tooling.rs", - "d9511c27bac9a80f61c117bc4db402f214f5b94b04956f9e0deb4cda1bfa7829" + "bac1492eb395bb9a06fb5ddc3e60e504b89dec46253ba9f2aa8b652962ed5eb2" ], [ "src-tauri/src/services/tooling/claude.rs", - "a4d9c9265eca9d7ecab6d320b6494dadbfb6c42f5a39b27c17788cb84eef5d26" + "606c939420f54b3bd4904a2532933b58c3dc77f0b457e63f5e19a0943f30e38c" ], [ "src-tauri/src/services/tooling/discovery.rs", "5a21aa858566373dddfaf5972391ea0b93746e140d5c557793c5ab277e0250fa" ], + [ + "src-tauri/src/services/tooling/grok_npm.rs", + "a37f390cca12fc938aaf3746bb93221f74002523f7f880168b310e16e692a9f7" + ], [ "src-tauri/src/services/tooling/grok.rs", - "e05136958c382c3835fdf2ec755594419494843c69385b826c39f36cea26f4a4" + "60b52043f9a4849dea78852a284dacb4d552c3ee76ec62c48f617276f530e182" ], [ "src-tauri/src/services/tooling/health.rs", @@ -287,7 +291,7 @@ ], [ "src-tauri/src/services/tooling/lifecycle.rs", - "5394ab12acb1ea5738f62aa994774afd55b11b026a865d5c61d424da8f02924a" + "7357a45dcc7d3b413cc9f783cac8d272870407d36b8eed8c7370448bd724b9ec" ], [ "src-tauri/src/services/tooling/terminal.rs", @@ -295,7 +299,7 @@ ], [ "src-tauri/src/services/tooling/versions.rs", - "2347eeef354cd9a09008646fc635151de9ececeacbad2c981d89a5970cca2e48" + "a8ed5bc583906bb24df881e4e399d91f42401d32401f6dc9ba8a546a9c6c14e1" ], [ "src-tauri/src/services/traework_models.rs", @@ -391,7 +395,7 @@ ], [ "tests/codexWindowsUserScopeContract.test.ts", - "e4e120372e13ce0b34728f14506fc0b6d4629a53265aec16ec73fa4b27e58559" + "dd312ed28285c0aa56e7f998b3c3f64095b71c4df0bac4a3e4db7d57c49869cd" ], [ "tests/hdiutilRetry.test.ts", diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index 8dee209d1..35f335294 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -3537,6 +3537,13 @@ requires_openai_auth = true crate::settings::set_current_provider(&AppType::ClaudeDesktop, Some("p1")) .expect("set local current provider"); + db.update_proxy_config(ProxyConfig { + listen_port: 0, + ..Default::default() + }) + .await + .expect("use an OS-assigned test proxy port"); + // Claude Desktop keeps backup state from takeover startup; this sentinel only // marks takeover as active so provider updates rewrite the 3P profile. db.save_live_backup("claude-desktop", "{}") @@ -3553,11 +3560,12 @@ requires_openai_auth = true .expect("update app proxy config"); } - state + let proxy_info = state .proxy_service .start() .await .expect("start proxy service"); + assert_ne!(proxy_info.port, 0, "listener must expose its bound port"); let mut updated = Provider::with_id( "p1".into(), @@ -3601,7 +3609,10 @@ requires_openai_auth = true let profile: Value = read_json_file(&profile_path).expect("read desktop profile"); assert_eq!( profile["inferenceGatewayBaseUrl"], - json!("http://127.0.0.1:15721/claude-desktop"), + json!(format!( + "http://127.0.0.1:{}/claude-desktop", + proxy_info.port + )), "desktop profile should stay pointed at the local gateway during takeover" ); assert_eq!(profile["inferenceGatewayAuthScheme"], json!("bearer")); @@ -3610,6 +3621,11 @@ requires_openai_auth = true json!([{ "name": "claude-sonnet-4-6", "labelOverride": "DeepSeek V4 Flash Updated", "supports1m": true }]), "provider edits should propagate into the Claude Desktop 3P profile during takeover" ); + state + .proxy_service + .stop() + .await + .expect("stop test proxy service"); } #[test] diff --git a/src-tauri/src/services/tooling/grok_npm.rs b/src-tauri/src/services/tooling/grok_npm.rs index d6ba2114f..21799392b 100644 --- a/src-tauri/src/services/tooling/grok_npm.rs +++ b/src-tauri/src/services/tooling/grok_npm.rs @@ -7,9 +7,11 @@ use std::collections::BTreeMap; use std::time::Duration; use fyagent_user_helper::grok_npm::{ - current_platform_package, npm_major_allows_scripts, GrokNpmInstallPlan, GrokNpmPlanError, - GrokNpmRegistry, OfficialNpmTool, GROK_NPM_ALLOW_SCRIPTS_PACKAGE, + current_platform_package, GrokNpmInstallPlan, GrokNpmPlanError, GrokNpmRegistry, + OfficialNpmTool, }; +#[cfg(any(target_os = "windows", test))] +use fyagent_user_helper::grok_npm::{npm_major_allows_scripts, GROK_NPM_ALLOW_SCRIPTS_PACKAGE}; #[cfg(test)] use fyagent_user_helper::GROK_NPM_PACKAGE; @@ -98,12 +100,14 @@ pub(super) fn install_command_for_version(version: &str) -> Option { Some(format!("npm {}", plan.npm_argv().join(" "))) } +#[cfg(any(target_os = "windows", test))] pub(super) fn command_for_plan(plan: &GrokNpmInstallPlan) -> String { format!("npm {}", plan.npm_argv().join(" ")) } /// npm 12+ blocks unlisted lifecycle scripts. Mirror the helper: add a /// package-scoped allow-scripts flag only when the executing npm major needs it. +#[cfg(any(target_os = "windows", test))] pub(super) fn command_with_script_policy(command: &str, npm_major: Option) -> String { let flag = format!("--allow-scripts={GROK_NPM_ALLOW_SCRIPTS_PACKAGE}"); if command.contains("--allow-scripts=") { @@ -116,6 +120,7 @@ pub(super) fn command_with_script_policy(command: &str, npm_major: Option) } } +#[cfg(any(target_os = "windows", test))] pub(super) fn exact_install_version(command: &str) -> Option<&str> { let marker = format!("{}@", OfficialNpmTool::Grok.package()); command.split_whitespace().find_map(|token| { diff --git a/src-tauri/src/services/tooling/versions.rs b/src-tauri/src/services/tooling/versions.rs index dff283e37..173e78364 100644 --- a/src-tauri/src/services/tooling/versions.rs +++ b/src-tauri/src/services/tooling/versions.rs @@ -55,7 +55,7 @@ pub(super) async fn get_single_tool_version_impl(tool: &str) -> ToolVersion { _ => None, }; - let mapped = ToolVersion { + ToolVersion { name: tool.to_string(), version: local_version, latest_version, @@ -67,8 +67,7 @@ pub(super) async fn get_single_tool_version_impl(tool: &str) -> ToolVersion { installed_but_broken, distribution_owner, latest_source, - }; - mapped + } } pub(super) async fn fetch_grok_latest_with_owner( diff --git a/src/app/styles/features.css b/src/app/styles/features.css index 099d3f26e..008b010ea 100644 --- a/src/app/styles/features.css +++ b/src/app/styles/features.css @@ -423,52 +423,28 @@ } .fy-feature-intro { margin: 0; - color: var(--fy-text); - font-size: 16px; - line-height: 1.7; + color: var(--fy-text-secondary); + font-size: var(--fy-font-body); + line-height: var(--fy-line-body); overflow-wrap: anywhere; } .fy-feature-info-grid { display: grid; - /* Fill the real detail box, with a meaningful floor and no phantom third column. */ - grid-template-columns: repeat( - auto-fit, - minmax( - min( - 100%, - max(var(--fy-info-card-min), calc((100% - var(--fy-info-card-gap)) / 2)) - ), - 1fr - ) - ); - gap: var(--fy-info-card-gap); + grid-template-columns: minmax(0, 1fr); align-content: start; - align-items: start; -} -.fy-feature-info-span { - grid-column: 1 / -1; } .fy-feature-info-card { container: fy-info-card / inline-size; display: grid; align-content: start; - gap: 10px; + gap: var(--fy-space-md); min-width: 0; - padding: 16px; - border: 1px solid var(--fy-border); - border-radius: var(--fy-radius-panel); - background: rgba(229, 246, 255, 0.06); + padding-top: var(--fy-space-md); + border-top: 1px solid var(--fy-divider); } .fy-feature-info-card h3 { - font-size: 13px; - font-weight: 650; - letter-spacing: 0.02em; -} -.fy-feature-info-lead { - margin: 0; - color: var(--fy-text-secondary); - font-size: 13px; - line-height: 1.65; + font-size: var(--fy-font-control); + font-weight: var(--fy-weight-semibold); } .fy-feature-info-card .fy-feature-definition { font-size: 13px; @@ -492,26 +468,6 @@ text-align: end; } } -.fy-feature-app-chips { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 0; - padding: 0; - list-style: none; -} -.fy-feature-app-chip { - display: inline-flex; - align-items: center; - gap: 7px; - min-height: 32px; - padding: 4px 10px 4px 7px; - border: 1px solid var(--fy-border); - border-radius: var(--fy-radius-pill); - background: var(--fy-surface-hover); - color: var(--fy-text); - font-size: 13px; -} .fy-feature-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); diff --git a/src/app/styles/tokens.css b/src/app/styles/tokens.css index 82b0663fc..b0d0b78cc 100644 --- a/src/app/styles/tokens.css +++ b/src/app/styles/tokens.css @@ -86,8 +86,6 @@ --fy-space-md: 12px; --fy-space-lg: 16px; --fy-space-xl: 24px; - --fy-info-card-min: 256px; - --fy-info-card-gap: var(--fy-space-md); --fy-definition-label-cap: min(30%, 8em); --fy-definition-gap: var(--fy-space-md); --fy-assignment-row-gap: var(--fy-space-sm); diff --git a/src/pages/agents/AgentAssignmentSections.tsx b/src/pages/agents/AgentAssignmentSections.tsx index 3929c5971..b758b4371 100644 --- a/src/pages/agents/AgentAssignmentSections.tsx +++ b/src/pages/agents/AgentAssignmentSections.tsx @@ -118,7 +118,6 @@ export function AgentSkillsSection({ ) : query.isError && query.data === undefined ? ( void query.refetch()}>重试} /> ) : skills.length === 0 ? ( @@ -156,7 +155,7 @@ export function AgentSkillsSection({ ) : null} -

{skill.description ?? "暂无说明"}

+ {skill.description &&

{skill.description}

}
void query.refetch()}>重试} /> ) : servers.length === 0 ? ( @@ -309,7 +307,7 @@ export function AgentMcpSection({ ) : null}
-

{server.description ?? "暂无说明"}

+ {server.description &&

{server.description}

}

- Claude Code 将更新自己的登录凭据;macOS 使用系统钥匙串。FyAgent - 不读取、复制或替换这些凭据,也不会借此更改你的模型来源、MCP - 或其他配置。 -

-

- 官方登录或退出不是 FyAgent - 配置文件替换,不能通过文件备份撤销。需要恢复账号使用时,请重新运行官方登录。 -

-

- 关闭此窗口或停止等待只会结束 FyAgent - 的监测,不会取消已经打开的官方登录流程。 + Claude Code 自行更新登录凭据(macOS 使用系统钥匙串)。FyAgent + 不读取、复制或替换凭据,也不改动模型来源、MCP 或其他配置。

+

登录或退出不能通过文件备份撤销,恢复账号需重新运行官方登录。

+

关闭窗口或停止等待只会结束监测,不会取消官方登录流程。

); diff --git a/src/pages/agents/AgentPromptsSection.tsx b/src/pages/agents/AgentPromptsSection.tsx index e487389b5..c4c124b47 100644 --- a/src/pages/agents/AgentPromptsSection.tsx +++ b/src/pages/agents/AgentPromptsSection.tsx @@ -94,7 +94,6 @@ function SupportedPromptProjection({ ) : query.isError && query.data === undefined ? ( void query.refetch()}>重试} /> ) : prompts.length === 0 ? ( @@ -130,7 +129,7 @@ function SupportedPromptProjection({

{selected.name}

-

{selected.description ?? "暂无说明。"}

+ {selected.description &&

{selected.description}

}
- ) : null} + {canRemove ? ( +
+
- + ) : null} ); } @@ -423,7 +410,11 @@ export function AccountView({
@@ -503,10 +494,7 @@ export function AccountView({ /> ) : ( - + )} diff --git a/src/pages/auth/CodexRequestSource.tsx b/src/pages/auth/CodexRequestSource.tsx index c9e793ee9..8f0747a97 100644 --- a/src/pages/auth/CodexRequestSource.tsx +++ b/src/pages/auth/CodexRequestSource.tsx @@ -57,9 +57,6 @@ export function CodexRequestSource({ onTerminal={reconcile} /> ) : null} -

- 需要添加服务地址、API Key 或修改模型参数? -

diff --git a/src/pages/auth/ConnectionsView.tsx b/src/pages/auth/ConnectionsView.tsx index 14213f7bd..db65f4ace 100644 --- a/src/pages/auth/ConnectionsView.tsx +++ b/src/pages/auth/ConnectionsView.tsx @@ -263,7 +263,6 @@ export function ConnectionsView({

{managedAuthConsumerLabel(selectedConsumer)}

-

选择登录账号,并管理软件当前使用的模型来源。

@@ -288,10 +287,7 @@ export function ConnectionsView({ )} ) : ( - + )} {codexSourceControls} diff --git a/src/pages/auth/LoginDialog.tsx b/src/pages/auth/LoginDialog.tsx index f56c68965..d81a7a7dd 100644 --- a/src/pages/auth/LoginDialog.tsx +++ b/src/pages/auth/LoginDialog.tsx @@ -286,7 +286,7 @@ function LoginDialogContent({ description={ session ? "登录由官方服务完成;FyAgent 保存账号后,不会自动替换软件的认证文件。连接软件需要另行确认。" - : "选择账号类型和这次登录的用途。" + : undefined } actions={actions} size="comfortable" diff --git a/src/pages/auth/Page.tsx b/src/pages/auth/Page.tsx index cd92e1d46..4337f79d6 100644 --- a/src/pages/auth/Page.tsx +++ b/src/pages/auth/Page.tsx @@ -361,10 +361,7 @@ export function AuthPage() { aria-label="账号与认证" data-testid="auth-page" > - +
@@ -421,7 +418,6 @@ export function AuthPage() {

账号与认证

-

登录官方账号,切换软件账号与模型来源。

运行状态

-

查看本机安装、账号与配置,找到需要处理的地方。

{progress?.state === "running" @@ -244,14 +242,10 @@ export function HealthPage() { ? `检查已停止 · 已完成 ${progress.completed} / ${progress.total}${progress.failed ? `,${progress.failed} 个读取失败` : ""}` : progress?.state === "complete" ? `检查完成 · ${progress.completed - progress.failed} 个已更新${progress.failed ? `,${progress.failed} 个读取失败,可单独重试` : ""}` - : "选择软件即可检查,也可以检查全部软件。"} + : null}

- +
{ @@ -356,9 +349,11 @@ export function HealthPage() { : "检查结果已超过 5 分钟,请重新检查后再判断。"} ) : null} -

- {healthPrimaryReason(snapshot)} -

+ {selectedStatus !== "ready" && ( +

+ {healthPrimaryReason(snapshot)} +

+ )}

最后检查{" "}

-

{description}

+ {description &&

{description}

}
-
-

安装来源

-

- {catalogItem - ? "来自内置精选目录。" - : "手动添加或从现有 Agent 配置导入。"} -

+
+

安装信息

-
来源类型
-
{sourceLabel}
{catalogItem && ( <>
发布方
@@ -146,55 +132,14 @@ function ServerDetail({
{server.id}
-
安装目录
-
- {installDirectory ? ( - - ) : ( - "无本地安装目录" - )} -
-
- {(homepage || docs) && ( -
- {homepage && ( - 主页 - )} - {docs && 说明} -
- )} -
-
-

当前分配

-

- {assigned.length > 0 - ? `已启用 ${assigned.length} 个应用。` - : "尚未分配到任何应用。"} -

- {assigned.length > 0 && ( -
    - {assigned.map((app) => ( -
  • - - {app.label} -
  • - ))} -
- )} -
-
-

安装信息

-
-
传输类型
-
{transport}
+ {installDirectory && ( + <> +
安装目录
+
+ +
+ + )} {spec.command && ( <>
命令
@@ -249,6 +194,14 @@ function ServerDetail({ )}
+ {(homepage || docs) && ( +
+ {homepage && ( + 主页 + )} + {docs && 说明} +
+ )}
{showAssignment && ( @@ -434,7 +387,7 @@ export function McpPage() { unmountOnExit > {query.isLoading ? ( - + ) : ( @@ -472,7 +425,7 @@ export function McpPage() { unmountOnExit > {query.isLoading ? ( - + ) : query.error && query.data === undefined ? ( @@ -533,15 +486,13 @@ export function McpPage() { onSelect={() => setSelectedId(server.id)} > - {server.description || - server.tags?.join(" · ") || - "暂无说明"}{" "} - · {transportOf(server)} ·{" "} - { - MCP_TARGETS.filter((app) => server.apps[app.id]) - .length - }{" "} - Agent + {[ + server.description || server.tags?.join(" · "), + transportOf(server), + `${MCP_TARGETS.filter((app) => server.apps[app.id]).length} Agent`, + ] + .filter(Boolean) + .join(" · ")} ))} @@ -836,7 +787,7 @@ function McpEditor({ open onOpenChange={(next) => !next && !busy && onClose()} title={initial ? `编辑 ${initial.name}` : "添加 MCP"} - description="可使用表单或 JSON 编辑服务配置。敏感信息仅在此窗口显示。" + description="密钥和请求头仅在此窗口显示。" size="wide" actions={ <> diff --git a/src/pages/memory/Page.tsx b/src/pages/memory/Page.tsx index 465ca159e..302d8fc19 100644 --- a/src/pages/memory/Page.tsx +++ b/src/pages/memory/Page.tsx @@ -203,7 +203,7 @@ export function MemoryPage() { aria-label="记忆" >
-

记忆模块

+

记忆

@@ -372,7 +372,7 @@ function LongTermView({ if (documentQuery.isLoading) { return ( - + ); @@ -504,9 +504,7 @@ function LongTermEditor({

{resource.title}

- - {missing ? "尚未创建" : "已读取"} - + {missing && 尚未创建} {dirty && 未保存}
@@ -757,7 +755,7 @@ function DailyView({ if (listQuery.isLoading) { return ( - + ); @@ -902,10 +900,7 @@ function DailyView({ ) ) : (
- +
)} @@ -955,9 +950,7 @@ function DailyEditor({

{filename}

- - {missing ? "尚未创建" : "已读取"} - + {missing && 尚未创建} {dirty && 未保存}
diff --git a/src/pages/memory/page.css b/src/pages/memory/page.css index 62c7b470a..7cf83303a 100644 --- a/src/pages/memory/page.css +++ b/src/pages/memory/page.css @@ -86,9 +86,9 @@ .fy-memory-editor-head .fy-feature-detail-title h2 { margin: 0; - font-size: 18px; - font-weight: 700; - line-height: 1.3; + font-size: var(--fy-font-section-title); + font-weight: var(--fy-weight-semibold); + line-height: var(--fy-line-heading); } .fy-memory-editor-meta-inline { @@ -105,10 +105,12 @@ .fy-memory-editor-tools-wrapper { display: flex; + flex-shrink: 0; flex-direction: column; align-items: flex-end; gap: 6px; margin-left: auto; + max-width: 100%; } .fy-memory-editor-tools { @@ -119,6 +121,10 @@ gap: 8px 10px; } +.fy-memory-editor-tools > .fy-feature-path { + width: auto; +} + .fy-memory-char-count { margin: 0; color: var(--fy-text-secondary); @@ -217,10 +223,6 @@ align-items: flex-start; } - .fy-memory-editor-head .fy-feature-detail-title h2 { - font-size: 16px; - } - .fy-memory-editor-tools-wrapper { align-items: flex-start; margin-left: 0; diff --git a/src/pages/models/ModelConnectivityTest.tsx b/src/pages/models/ModelConnectivityTest.tsx index 33a77bcf1..d863e6e51 100644 --- a/src/pages/models/ModelConnectivityTest.tsx +++ b/src/pages/models/ModelConnectivityTest.tsx @@ -121,7 +121,7 @@ export function ModelConnectivityTest({ onOpenChange={closeDialog} size="wide" title="选择要测试的模型" - description="测试会向所选模型发送一条简短请求,可能产生少量用量。完成后会显示响应或错误。" + description="将向所选模型发送测试请求,可能产生少量用量。" actions={ <> } - > -

确认后将使用当前选择覆盖已有模型。

- + />
diff --git a/src/pages/prompts/Page.tsx b/src/pages/prompts/Page.tsx index 32aad9ae0..ea5fa325a 100644 --- a/src/pages/prompts/Page.tsx +++ b/src/pages/prompts/Page.tsx @@ -431,10 +431,7 @@ export function PromptsPage() { description="网页版不能管理提示词。" /> ) : promptsQuery.isPending && promptsQuery.data === undefined ? ( - + ) : readFailed ? ( @@ -468,7 +465,6 @@ export function PromptsPage() { ) : filtered.length === 0 && activeEditor?.mode !== "new" && !selected ? ( setSearch("")}>清空搜索} /> ) : ( @@ -505,9 +501,11 @@ export function PromptsPage() { title={prompt.name} onSelect={() => requestSelect(prompt.id)} > - - {prompt.description || "暂无描述"} - + {prompt.description && ( + + {prompt.description} + + )} ))} @@ -539,10 +537,7 @@ export function PromptsPage() { className="fy-feature-panel fy-prompts-editor-pane" aria-label="提示词详情" > - + )} @@ -663,7 +658,7 @@ export function PromptsPage() { } open={activeDiscardIntent !== null} title="放弃未保存的提示词更改" - description="当前编辑内容尚未保存。确认放弃后再继续切换或离开页面。" + description="未保存的更改将丢失。" pending={busy} onCancel={cancelDiscard} onConfirm={confirmDiscard} diff --git a/src/pages/skills/Page.tsx b/src/pages/skills/Page.tsx index 9f695c7bc..ed60c4a8d 100644 --- a/src/pages/skills/Page.tsx +++ b/src/pages/skills/Page.tsx @@ -1,7 +1,6 @@ import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useRef, useState } from "react"; -import { getSkillTargetIcon } from "../../shared/assets/apps"; import { buildSkillSearchText, convergeSelection, @@ -103,10 +102,6 @@ function skillCardBody(skill: DiscoverableSkill): string { return skillDirectoryNote(skill); } -function skillDetailBody(skill: DiscoverableSkill): string { - return skillCardBody(skill) || "暂无说明"; -} - type DiscoverySkill = DiscoverableSkill & Partial< Pick< @@ -163,10 +158,6 @@ function skillDocsAction( return repoUrl ? { url: repoUrl, label: "仓库" } : null; } -function assignedSkillTargets(skill: InstalledSkill) { - return SKILL_TARGETS.filter((target) => Boolean(skill.apps[target.id])); -} - const INSTALLED_SPLIT_LABELS = ["调整列表与详情的宽度", "调整详情与分配的宽度"]; const invalidations = [ @@ -195,7 +186,6 @@ function Detail({ onUninstall: () => void; showAssignment: boolean; }) { - const assigned = assignedSkillTargets(skill); const repo = skill.repoOwner && skill.repoName ? `${skill.repoOwner}/${skill.repoName}` @@ -205,17 +195,8 @@ function Detail({ skill.repoOwner && skill.repoName ? githubRepoUrl(skill.repoOwner, skill.repoName) : null; - const sourceLabel = market - ? "从 Skill 市场安装" - : repo - ? "GitHub 仓库" - : "本地导入"; - const sourceLead = market - ? "从 Skill 市场安装,保存在本地目录。" - : repo - ? "来自 GitHub 仓库,保存在本地目录。" - : "来自本地导入或 ZIP 安装。"; - const description = skill.description?.trim() || "暂无说明"; + const sourceLabel = market ? "Skill 市场" : repo ? "GitHub 仓库" : "本地导入"; + const description = skill.description?.trim(); return (
有更新} {sourceLabel}
-

{description}

+ {description &&

{description}

}
{update && (
-
-

下载来源

-

{sourceLead}

+
+

安装信息

-
来源类型
-
{sourceLabel}
{repo && !market && ( <>
仓库
@@ -275,6 +253,14 @@ function Detail({ value={skillInstallPath(skill)} /> + {skill.installedAt > 0 && ( + <> +
安装时间
+
{formatSkillTimestamp(skill.installedAt)}
+ + )} +
最近更新
+
{formatSkillTimestamp(skill.updatedAt)}
{(repoUrl || skill.readmeUrl) && (
@@ -289,45 +275,6 @@ function Detail({
)}
-
-

当前分配

-

- {assigned.length > 0 - ? `已启用 ${assigned.length} 个应用。` - : "尚未分配到任何应用。"} -

- {assigned.length > 0 && ( -
    - {assigned.map((app) => ( -
  • - - {app.label} -
  • - ))} -
- )} -
-
-

安装信息

-
- {skill.installedAt > 0 && ( - <> -
安装时间
-
{formatSkillTimestamp(skill.installedAt)}
- - )} -
最近更新
-
{formatSkillTimestamp(skill.updatedAt)}
-
-
{showAssignment && (
@@ -607,10 +554,7 @@ export function SkillsPage() { )} {installedQuery.isLoading ? ( - + ) : installedQuery.error && installedQuery.data === undefined ? ( @@ -669,7 +613,9 @@ export function SkillsPage() { title={skill.name} onSelect={() => setSelectedId(skill.id)} > - {skill.description || "暂无说明"} + {skill.description && ( + {skill.description} + )} ))} @@ -787,7 +733,7 @@ export function SkillsPage() { } description={ confirm?.kind === "uninstall" - ? "将从管理列表及已启用的应用中移除,并创建可恢复备份。" + ? "将从管理列表及已启用的应用中移除。" : "删除后无法从该备份恢复。" } pending={busy} @@ -958,11 +904,11 @@ function Discovery({ /> ) : (installed.data === undefined && installed.isPending) || (market.data === undefined && market.isPending) ? ( - + ) : skills.length === 0 ? ( - + ) : (
@@ -995,13 +941,15 @@ function Discovery({ originRef={originRef} open title={detailSkill.name} - description={skillDetailMeta(detailSkill) || "Skill 详情"} + description={skillDetailMeta(detailSkill) || undefined} onOpenChange={(open) => { if (!open) setDetailSkill(null); }} actions={} > -

{skillDetailBody(detailSkill)}

+ {skillCardBody(detailSkill) && ( +

{skillCardBody(detailSkill)}

+ )} ) : null} @@ -1088,7 +1036,7 @@ function AuxiliaryDialogs({ originRef={originRef} open title="导入本地 Skills" - description="选择要管理的 Skills。系统会根据支持情况预设可用应用,你仍可逐项调整。" + description="选择 Skills 和要启用的软件。" onOpenChange={(open) => !open && !busy && close()} actions={ <> @@ -1189,7 +1137,6 @@ function AuxiliaryDialogs({ originRef={originRef} open title="备份恢复" - description="选择要恢复到的应用。" onOpenChange={(open) => !open && !busy && close()} actions={ } - > -

WorkBuddy 官方限制第三方 MCP 必须在安装后手动信任授权才能正常使用。

- + /> ); } diff --git a/tests/architecture/rootGovernance.test.ts b/tests/architecture/rootGovernance.test.ts index 2cd0a8485..de783d146 100644 --- a/tests/architecture/rootGovernance.test.ts +++ b/tests/architecture/rootGovernance.test.ts @@ -122,4 +122,38 @@ describe("repository configuration ownership", () => { "vite preview --config config/vite.config.ts", ); }); + + it("separates serial frame measurement from functional trace recording", () => { + // Load the actual exported configs in Node, not the jsdom test realm. + const settings = JSON.parse( + execFileSync( + process.execPath, + [ + "--input-type=module", + "-e", + ` + import performance from './config/playwright.performance.config.ts'; + import functional from './config/playwright.config.ts'; + console.log(JSON.stringify({performance, functional})); + `, + ], + { cwd: root, encoding: "utf8" }, + ), + ); + expect(settings.performance).toMatchObject({ + workers: 1, + retries: 0, + use: { trace: "off", viewport: { width: 1232, height: 700 } }, + webServer: { reuseExistingServer: false }, + }); + expect(settings.performance.testMatch).toEqual( + expect.arrayContaining([ + "navigation-performance.spec.ts", + "presentation-performance.spec.ts", + "state-performance.spec.ts", + "theme-performance.spec.ts", + ]), + ); + expect(settings.functional.use.trace).toBe("retain-on-failure"); + }); }); diff --git a/tests/browser/auth.spec.ts b/tests/browser/auth.spec.ts index 42806016a..b0bd71e70 100644 --- a/tests/browser/auth.spec.ts +++ b/tests/browser/auth.spec.ts @@ -30,6 +30,7 @@ test("renders account identity, software connection and current request source a .getByRole("heading", { name: "Codex", exact: true }) .locator("xpath=ancestor::article[1]"); await expect(codexCard).toContainText("DeepSeek API"); + await expect(codexCard.getByText("Codex", { exact: true })).toHaveCount(1); await expect(codexCard).toContainText("已保留"); await expect(codexCard).toContainText("由 Codex 自动续期"); await expect(page.getByText(/access[_ ]?token/iu)).toHaveCount(0); diff --git a/tests/browser/responsive-density.spec.ts b/tests/browser/responsive-density.spec.ts index b545fb0cd..5e88c847c 100644 --- a/tests/browser/responsive-density.spec.ts +++ b/tests/browser/responsive-density.spec.ts @@ -130,8 +130,14 @@ for (const route of ["skills", "mcp"] as const) { (node) => node.getBoundingClientRect().width, ), })); - for (const card of cards.children) - expect(card).toBeGreaterThanOrEqual(Math.min(cards.width, 256) - 1); + expect(cards.children).toHaveLength(1); + expect(Math.abs(cards.children[0] - cards.width)).toBeLessThanOrEqual(1); + await expect(page.getByRole("region", { name: "当前分配" })).toHaveCount( + 0, + ); + await expect( + page.locator(".fy-feature-assignments:visible").getByRole("switch"), + ).toHaveCount(7); } await expectNoHorizontalOverflow(page); await expectHealthyPage(page, health); @@ -224,8 +230,12 @@ test("local Skill metadata, long content and enlarged text use bounded natural s const health = monitorPageHealth(page); await resize(page, 1564, 991); await openRendererPage(page, "/skills"); - const sourceCard = page.getByRole("region", { name: "下载来源" }); - await expect(sourceCard).toContainText("本地导入"); + const sourceCard = page.getByRole("region", { name: "安装信息" }); + await expect( + page + .getByRole("region", { name: "Skill 详情" }) + .getByText("本地导入", { exact: true }), + ).toHaveCount(1); await expect(sourceCard).not.toContainText("/fixture/private-location/"); await expect( sourceCard.getByRole("button", { name: "复制安装目录", exact: true }), diff --git a/tests/browser/scroll-ownership.spec.ts b/tests/browser/scroll-ownership.spec.ts index 8c3a48b42..80e8fffc2 100644 --- a/tests/browser/scroll-ownership.spec.ts +++ b/tests/browser/scroll-ownership.spec.ts @@ -2,10 +2,109 @@ import { expect, test, type Locator, type Page } from "@playwright/test"; import { installRichTauriFeatureFixture } from "./support/features"; import { expectHealthyPage, + expectNoHorizontalOverflow, monitorPageHealth, openRendererPage, } from "./support"; +const conciseViews = [ + { + id: "agents", + route: "/agents?target=codex§ion=skills", + ready: ".fy-agent-resource-full-list", + }, + { id: "auth", route: "/auth", ready: ".fy-auth-detail-header" }, + { id: "health", route: "/health", ready: ".fy-health-group" }, + { + id: "models", + route: "/models?target=codex", + ready: ".fy-models-config-panel", + }, + { id: "skills", route: "/skills", ready: ".fy-feature-detail-header" }, + { id: "mcp", route: "/mcp", ready: ".fy-feature-detail-header" }, + { id: "prompts", route: "/prompts", ready: ".fy-prompts-editor-content" }, + { id: "memory", route: "/memory", ready: ".fy-memory-editor-textarea" }, +] as const; + +for (const theme of ["light", "dark"] as const) { + test(`${theme} concise secondary views preserve content, controls and readable type`, async ({ + page, + }, info) => { + test.setTimeout(60_000); + await page.emulateMedia({ reducedMotion: "reduce" }); + await page.addInitScript( + (theme) => localStorage.setItem("fyagent-theme", theme), + theme, + ); + await installLongFixture(page); + const health = monitorPageHealth(page); + for (const view of conciseViews) { + await openRendererPage(page, view.route); + const scope = page.getByTestId(`${view.id}-page`); + await expect(scope.locator(view.ready).first()).toBeVisible(); + // A pending official login is valid fixture state, not page loading. + await expect( + scope.locator(".fy-control-empty .fy-control-spinner"), + ).toHaveCount(0); + await expect(scope.getByText("暂无说明", { exact: true })).toHaveCount(0); + await expect(scope.getByRole("region", { name: "当前分配" })).toHaveCount( + 0, + ); + if (view.id === "skills" || view.id === "mcp") { + await expect(scope.locator(".fy-feature-intro")).toHaveCSS( + "font-size", + "14px", + ); + await expect(scope.locator(".fy-feature-info-card")).toHaveCount(1); + await expect( + scope.locator(".fy-feature-assignments:visible").getByRole("switch"), + ).toHaveCount(7); + } + if (view.id === "memory") { + await expect(scope.getByText("已读取", { exact: true })).toHaveCount(0); + await expect(scope.locator(".fy-memory-editor-head h2")).toHaveCSS( + "font-size", + "16px", + ); + const tools = scope.locator(".fy-memory-editor-tools"); + const panelWidth = await scope + .locator(".fy-memory-editor-panel") + .evaluate((node) => node.getBoundingClientRect().width); + if (panelWidth >= 350) { + const tops = await tools + .getByRole("button") + .evaluateAll((nodes) => + nodes.map((node) => node.getBoundingClientRect().top), + ); + expect(tops.length).toBeGreaterThan(1); + expect(Math.max(...tops) - Math.min(...tops)).toBeLessThanOrEqual(1); + } + await expect( + scope.getByRole("textbox", { name: "记忆内容", exact: true }), + ).toHaveValue(/Long memory fixture line/); + } + if (view.id === "health") { + await expect(scope.locator(".fy-health-summary")).toHaveCount(0); + await expect(scope.locator(".fy-health-scope")).toContainText( + "不会测试远端服务或额度", + ); + } + if (view.id === "prompts") { + await expect( + scope.getByRole("textbox", { name: "内容", exact: true }), + ).toHaveValue(/Long prompt line/); + } + await expectNoHorizontalOverflow(page); + await page.screenshot({ + path: `node_modules/.cache/concise-renderer/screenshots/${info.project.name}-${theme}-${view.id}.jpg`, + type: "jpeg", + quality: 75, + }); + } + await expectHealthyPage(page, health); + }); +} + async function installLongFixture(page: Page) { await installRichTauriFeatureFixture(page); await page.addInitScript(() => { diff --git a/tests/codexWindowsUserScopeContract.test.ts b/tests/codexWindowsUserScopeContract.test.ts index 2f92df7ac..d580beeaf 100644 --- a/tests/codexWindowsUserScopeContract.test.ts +++ b/tests/codexWindowsUserScopeContract.test.ts @@ -72,7 +72,53 @@ function rustFilesUnder(relativeDirectory: string): string[] { return files.sort(); } +function assertMacosPathEnvironmentBoundary(source: string) { + // Match rustfmt's complete function/block boundaries, not an attribute next + // to a call: the login-shell and ambient PATH reads share one macOS block. + const searchFunction = source.match( + /^fn build_tool_search_paths\(tool: &str\) -> Vec \{[\s\S]*?^\}/mu, + )?.[0]; + expect(searchFunction).toBeDefined(); + const macosBlock = searchFunction?.match( + /^ #\[cfg\(target_os = "macos"\)\]\s*\n \{[\s\S]*?^ \}/mu, + )?.[0]; + expect(macosBlock).toBeDefined(); + expect(macosBlock).toContain("login_shell_path()"); + expect(macosBlock).toContain('std::env::var_os("PATH")'); + expect(macosBlock?.match(/extend_from_cli_path_env\s*\(/gu)).toHaveLength(2); + const otherBranches = searchFunction?.replace(macosBlock ?? "", ""); + expect(otherBranches).not.toContain("extend_from_cli_path_env"); + expect(otherBranches).not.toContain("login_shell_path()"); + expect(otherBranches).not.toMatch(/std::env::var(?:_os)?\("PATH"\)/u); +} + describe("Codex Windows interactive-user contract", () => { + it.each(["moved", "copied"])( + "rejects ambient PATH discovery %s outside the macOS-only block", + (placement) => { + const call = + 'extend_from_cli_path_env(&mut search_paths, std::env::var_os("PATH"));'; + expect(commandHost).toContain(call); + const originalBlock = + placement === "moved" ? commandHost.replace(call, "") : commandHost; + const movedOutside = originalBlock.replace( + / search_paths\n\}/u, + ` ${call}\n search_paths\n}`, + ); + expect(movedOutside).not.toBe(commandHost); + expect(() => assertMacosPathEnvironmentBoundary(movedOutside)).toThrow(); + }, + ); + + it("rejects a widened macOS PATH block even when its body is unchanged", () => { + const widened = commandHost.replace( + /#\[cfg\(target_os = "macos"\)\]\n \{\n if tool == "hermes"/u, + '#[cfg(any(target_os = "macos", target_os = "windows"))]\n {\n if tool == "hermes"', + ); + expect(widened).not.toBe(commandHost); + expect(() => assertMacosPathEnvironmentBoundary(widened)).toThrow(); + }); + it("uses the Shell process as the sole ordinary startup identity proof", () => { expect(startup).not.toContain("WTSQueryUserToken"); expect(startup).toContain("GetShellWindow"); @@ -355,9 +401,7 @@ describe("Codex Windows interactive-user contract", () => { ); expect(windowsManagerPaths).toContain("safe_command_search_paths"); expect(windowsManagerPaths).not.toContain("std::env"); - expect(commandHost).toContain( - '#[cfg(target_os = "macos")]\n extend_from_cli_path_env', - ); + assertMacosPathEnvironmentBoundary(commandHost); expect(commandHost).toContain("configure_shell_user_command"); expect( commandHost.match(/configure_shell_user_command/g)?.length ?? 0, diff --git a/tests/renderer/app/actWarningGuard.test.ts b/tests/renderer/app/actWarningGuard.test.ts new file mode 100644 index 000000000..7e2896699 --- /dev/null +++ b/tests/renderer/app/actWarningGuard.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; + +describe("renderer act-warning guard lifetime", () => { + it.each([1, 2])( + "rejects an act warning after per-test mock restore (test %i)", + () => { + expect(() => + console.error( + "Warning: An update to %s inside a test was not wrapped in act(...).", + "GuardProbe", + ), + ).toThrow("Unexpected React act warning:"); + }, + ); +}); diff --git a/tests/renderer/app/setup.ts b/tests/renderer/app/setup.ts index 672749ecc..af8ed5fa0 100644 --- a/tests/renderer/app/setup.ts +++ b/tests/renderer/app/setup.ts @@ -1,7 +1,7 @@ import { transferableAbortController } from "node:util"; import "@testing-library/jest-dom/vitest"; import { cleanup } from "@testing-library/react"; -import { afterAll, afterEach, beforeAll, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest"; const nodeAbortController = transferableAbortController(); const DOMAbortController = window.AbortController; @@ -16,6 +16,11 @@ beforeAll(() => { // restores scroll after measurement; record that call without claiming a // real viewport moved. Browser regressions retain the actual scrolling API. window.scrollTo = vi.fn(); +}); + +beforeEach(() => { + // restoreMocks runs before each test, so a beforeAll spy would be removed + // before the very first assertion. Reinstall this guard for every test. const originalConsoleError = console.error.bind(console); consoleErrorGuard = vi .spyOn(console, "error") diff --git a/tests/renderer/app/userFacingCopy.test.ts b/tests/renderer/app/userFacingCopy.test.ts index e748ab38e..df98b6828 100644 --- a/tests/renderer/app/userFacingCopy.test.ts +++ b/tests/renderer/app/userFacingCopy.test.ts @@ -96,6 +96,48 @@ function collectCopy(file: string): CopyOccurrence[] { } describe("FyAgent user-facing copy contract", () => { + it("keeps reviewed secondary-page narration out of all eight route families", () => { + // These are concrete retired strings, not an AI-authorship detector or + // a length limit for useful explanations, warnings or user-authored text. + const retiredCopy = [ + "正在读取已安装的 Skills", + "正在读取该应用的提示词", + "正在获取应用信息", + "从左侧打开提示词后即可直接阅读和编辑正文。", + "查看登录状态、软件连接和账号操作。", + "查看账号连接、当前模型来源和需要处理的状态。", + "登录状态与软件连接分别管理。", + "当前搜索条件下没有结果", + "并创建可恢复备份", + ]; + const pagesRoot = path.join(rendererRoot, "pages"); + const files = listSourceFiles(pagesRoot); + expect( + [ + ...new Set( + files.map( + (file) => path.relative(pagesRoot, file).split(path.sep)[0], + ), + ), + ].sort(), + ).toEqual([ + "agents", + "auth", + "health", + "mcp", + "memory", + "models", + "prompts", + "skills", + ]); + const violations = files + .flatMap(collectCopy) + .filter((item) => + retiredCopy.some((fragment) => item.text.includes(fragment)), + ); + expect(violations).toEqual([]); + }); + it("does not expose reviewed implementation narration", () => { const violations = listSourceFiles(rendererRoot) .flatMap(collectCopy) diff --git a/tests/renderer/features/featurePages.test.tsx b/tests/renderer/features/featurePages.test.tsx index ccd53c038..070ce7d47 100644 --- a/tests/renderer/features/featurePages.test.tsx +++ b/tests/renderer/features/featurePages.test.tsx @@ -154,21 +154,27 @@ describe("MCP management", () => { .map((node) => node.getAttribute("aria-label")), ).toEqual(MCP_TARGETS.map((app) => `${app.label} MCP 分配`)); expect(screen.getByText(/stdio · 1 Agent/)).toBeVisible(); - expect(screen.getByRole("region", { name: "安装来源" })).toHaveTextContent( - "手动添加", - ); - expect(screen.getByRole("region", { name: "安装来源" })).toHaveTextContent( - "无本地安装目录", - ); - expect(screen.getByRole("region", { name: "当前分配" })).toHaveTextContent( - "Claude Code", + const detail = screen.getByRole("region", { name: "MCP 详情" }); + expect( + within(detail).getAllByText("手动添加", { exact: true }), + ).toHaveLength(1); + expect(within(detail).getAllByText("stdio", { exact: true })).toHaveLength( + 1, ); + expect(screen.queryByText("无本地安装目录")).not.toBeInTheDocument(); + expect( + screen.queryByRole("region", { name: "当前分配" }), + ).not.toBeInTheDocument(); + expect( + screen.getByRole("switch", { name: "Claude Code MCP 分配" }), + ).toBeChecked(); + expect(detail.querySelectorAll(".fy-feature-info-card")).toHaveLength(1); expect(screen.getByRole("region", { name: "安装信息" })).toHaveTextContent( - "stdio", + "npx", ); appearsBefore( screen.getByRole("button", { name: "编辑" }), - screen.getByRole("region", { name: "安装来源" }), + screen.getByRole("region", { name: "安装信息" }), ); appearsBefore( screen.getByRole("button", { name: "删除" }), @@ -335,7 +341,7 @@ describe("MCP management", () => { }); expect(trust).toHaveTextContent("连接器 → 自定义连接器"); expect(trust).toHaveTextContent( - "WorkBuddy 官方限制第三方 MCP 必须在安装后手动信任授权才能正常使用。", + "请到「连接器 → 自定义连接器」中信任该 MCP 后才能使用。", ); await user.click(within(trust).getByRole("button", { name: "知道了" })); await waitFor(() => @@ -443,18 +449,22 @@ describe("MCP management", () => { expect(document.body).toHaveTextContent( "https://mcp.amap.com/mcp?key=••••••", ); - expect(screen.getByRole("region", { name: "安装来源" })).toHaveTextContent( - "精选目录", - ); - expect(screen.getByRole("region", { name: "安装来源" })).toHaveTextContent( - "无本地安装目录", - ); - expect(screen.getByRole("region", { name: "当前分配" })).toHaveTextContent( - "Claude Code", - ); + expect( + within(screen.getByRole("region", { name: "MCP 详情" })).getAllByText( + "精选目录", + { exact: true }, + ), + ).toHaveLength(1); + expect(screen.queryByText("无本地安装目录")).not.toBeInTheDocument(); + expect( + screen.queryByRole("region", { name: "当前分配" }), + ).not.toBeInTheDocument(); + expect( + screen.getByRole("switch", { name: "Claude Code MCP 分配" }), + ).toBeChecked(); appearsBefore( screen.getByRole("button", { name: "编辑" }), - screen.getByRole("region", { name: "安装来源" }), + screen.getByRole("region", { name: "安装信息" }), ); }); @@ -482,11 +492,16 @@ describe("MCP management", () => { expect( await screen.findByRole("heading", { name: "node_repl" }), ).toBeVisible(); - expect( - screen.getByRole("region", { name: "安装来源" }), - ).not.toHaveTextContent(directory); + const copyButton = screen.getByRole("button", { name: "复制安装目录" }); + const pathControl = copyButton.closest(".fy-feature-path"); + expect(pathControl).not.toBeNull(); + expect(pathControl).not.toHaveTextContent(directory); + expect(pathControl?.querySelector(".fy-feature-path-value")).toBeNull(); + expect(screen.getByRole("region", { name: "安装信息" })).toHaveTextContent( + command, + ); expect(screen.queryByText("无本地安装目录")).not.toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: "复制安装目录" })); + await user.click(copyButton); expect(writeText).toHaveBeenCalledWith(directory); }); @@ -907,7 +922,7 @@ describe("Skills management", () => { expect(screen.getByText("1 项失败,1 项成功")).toBeVisible(); }); - it("shows download source and assigned apps in installed skill details", async () => { + it("shows source once, compact metadata and editable assignments in skill details", async () => { const user = userEvent.setup(); const remote: InstalledSkill = { ...installedSkill("review-skill", "Review Skill"), @@ -936,10 +951,12 @@ describe("Skills management", () => { renderFeature(, ports); + const detail = await screen.findByRole("region", { name: "Skill 详情" }); expect( - await screen.findByRole("region", { name: "下载来源" }), - ).toHaveTextContent("GitHub 仓库"); - expect(screen.getByRole("region", { name: "下载来源" })).toHaveTextContent( + within(detail).getAllByText("GitHub 仓库", { exact: true }), + ).toHaveLength(1); + expect(detail.querySelectorAll(".fy-feature-info-card")).toHaveLength(1); + expect(screen.getByRole("region", { name: "安装信息" })).toHaveTextContent( "acme/skills", ); expect( @@ -947,10 +964,18 @@ describe("Skills management", () => { "Review changes in pull requests", ), ).toBeVisible(); - const assignment = screen.getByRole("region", { name: "当前分配" }); - expect(assignment).toHaveTextContent("Claude Code"); - expect(assignment).toHaveTextContent("Codex"); - expect(assignment).not.toHaveTextContent("Gemini"); + expect( + screen.queryByRole("region", { name: "当前分配" }), + ).not.toBeInTheDocument(); + expect( + screen.getByRole("switch", { name: "Claude Code Skill 分配" }), + ).toBeChecked(); + expect( + screen.getByRole("switch", { name: "Codex Skill 分配" }), + ).toBeChecked(); + expect( + screen.queryByRole("switch", { name: /Gemini/ }), + ).not.toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "打开仓库" })); expect(openExternal).toHaveBeenCalledWith("https://github.com/acme/skills"); @@ -958,7 +983,7 @@ describe("Skills management", () => { const installPath = "C:\\Users\\xk\\AppData\\Roaming\\fyagent\\skills\\review-skill"; expect( - screen.getByRole("region", { name: "下载来源" }), + screen.getByRole("region", { name: "安装信息" }), ).not.toHaveTextContent(installPath); const writeText = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { @@ -975,29 +1000,32 @@ describe("Skills management", () => { ).not.toBeInTheDocument(); await user.click(screen.getByRole("button", { name: /Local Notes/ })); - expect(screen.getByRole("region", { name: "下载来源" })).toHaveTextContent( - "本地导入", - ); - expect(screen.getByRole("region", { name: "当前分配" })).toHaveTextContent( - "尚未分配到任何应用", - ); + expect( + screen.getByRole("region", { name: "Skill 详情" }), + ).toHaveTextContent("本地导入"); + for (const assignment of screen.getAllByRole("switch")) { + expect(assignment).not.toBeChecked(); + } expect( screen.getByRole("region", { name: "安装信息" }), ).not.toHaveTextContent("安装时间"); appearsBefore( screen.getByRole("button", { name: "卸载" }), - screen.getByRole("region", { name: "下载来源" }), + screen.getByRole("region", { name: "安装信息" }), ); await user.click(screen.getByRole("button", { name: /Market Review/ })); - expect(screen.getByRole("region", { name: "下载来源" })).toHaveTextContent( - "从 Skill 市场安装", - ); expect( - screen.getByRole("region", { name: "下载来源" }), + within(screen.getByRole("region", { name: "Skill 详情" })).getAllByText( + "Skill 市场", + { exact: true }, + ), + ).toHaveLength(1); + expect( + screen.getByRole("region", { name: "安装信息" }), ).not.toHaveTextContent("GitHub 仓库"); expect( - screen.getByRole("region", { name: "下载来源" }), + screen.getByRole("region", { name: "安装信息" }), ).not.toHaveTextContent(`${SKILLHUB_MARKET_OWNER}/review-skill`); expect( screen.queryByRole("button", { name: "打开仓库" }), diff --git a/tests/renderer/pages/agents/Page.test.tsx b/tests/renderer/pages/agents/Page.test.tsx index d00c9ef78..f07fb9e38 100644 --- a/tests/renderer/pages/agents/Page.test.tsx +++ b/tests/renderer/pages/agents/Page.test.tsx @@ -783,9 +783,7 @@ describe("V3 Agent directory and configuration shell", () => { ).not.toBeInTheDocument(), ); expect( - await within(directoryArticle("QoderWork CN")).findByText( - "正在检查来源", - ), + await within(directoryArticle("QoderWork CN")).findByText("正在检查来源"), ).toBeVisible(); await waitFor(() => expect(ports.agentInstallReadiness.startAction).toHaveBeenCalledWith({ @@ -807,6 +805,14 @@ describe("V3 Agent directory and configuration shell", () => { reasonCode: null, transfer: null, }); + // The job completion triggers readiness and inventory readback. Await the + // resulting action, not just the deferred promise, before test cleanup. + expect( + await within(directoryArticle("QoderWork CN")).findByRole("button", { + name: "选择安装目标", + }), + ).toBeEnabled(); + expect(configureButton("QoderWork CN")).toBeDisabled(); }); it("does not enable configure after a succeeded job until readback proves installation", async () => { diff --git a/tests/renderer/pages/auth/Page.test.tsx b/tests/renderer/pages/auth/Page.test.tsx index feb83f028..a0f233f90 100644 --- a/tests/renderer/pages/auth/Page.test.tsx +++ b/tests/renderer/pages/auth/Page.test.tsx @@ -106,6 +106,9 @@ describe("AuthPage", () => { expect( within(connectedSection!).getByRole("heading", { name: "Codex" }), ).toBeVisible(); + expect( + within(connectedSection!).getAllByText("Codex", { exact: true }), + ).toHaveLength(1); expect(within(connectedSection!).getByText("DeepSeek API")).toBeVisible(); expect(within(connectedSection!).getByText("已保留")).toBeVisible(); expect( @@ -119,6 +122,24 @@ describe("AuthPage", () => { ); }); + it("retains a connection target that differs from the software name", async () => { + const overview = managedAuthOverviewFixture(); + overview.connections[0] = { + ...overview.connections[0], + targetLabel: "Codex · 工作配置", + }; + renderPage(managedPorts({ getOverview: vi.fn(async () => overview) })); + const detail = await screen.findByRole("region", { + name: "person@example.com 账号详情", + }); + expect( + within(detail).getByText("Codex · 工作配置", { exact: true }), + ).toBeVisible(); + expect( + within(detail).getByRole("heading", { name: "Codex" }), + ).toBeVisible(); + }); + it("connects matching software from the account detail with this account preselected", async () => { const user = userEvent.setup(); const overview = managedAuthOverviewFixture(); diff --git a/tests/renderer/pages/health/Page.test.tsx b/tests/renderer/pages/health/Page.test.tsx index 260358663..36c23d24b 100644 --- a/tests/renderer/pages/health/Page.test.tsx +++ b/tests/renderer/pages/health/Page.test.tsx @@ -49,6 +49,12 @@ describe("HealthPage", () => { await within(detail).findByText("本机检查正常"); expect(detail.querySelectorAll("[data-check-id]")).toHaveLength(12); expect(detail.querySelectorAll("time")).toHaveLength(13); + expect(detail.querySelector(".fy-health-summary")).toBeNull(); + expect( + within(detail).getByText( + "本机检查不会测试远端服务或额度。检查结果超过 5 分钟后需要重新检查。", + ), + ).toBeVisible(); expect( within(detail).getByText(/尚无此软件的本机代理请求记录/), ).toBeVisible(); diff --git a/tests/renderer/pages/memory/Page.test.tsx b/tests/renderer/pages/memory/Page.test.tsx index 3dad4981e..b58d4505c 100644 --- a/tests/renderer/pages/memory/Page.test.tsx +++ b/tests/renderer/pages/memory/Page.test.tsx @@ -225,7 +225,7 @@ describe("MemoryPage native business management", () => { expect( within(pageHeader!).getByRole("heading", { level: 1, - name: "记忆模块", + name: "记忆", }), ).toBeVisible(); const resources = await screen.findByRole("region", { diff --git a/tests/renderer/pages/models/Page.test.tsx b/tests/renderer/pages/models/Page.test.tsx index baac7168a..4a341b496 100644 --- a/tests/renderer/pages/models/Page.test.tsx +++ b/tests/renderer/pages/models/Page.test.tsx @@ -392,7 +392,11 @@ describe("Models page", () => { "自定义模型需在 TRAE Work CN 中添加。FyAgent 不会写入其本地模型配置。", ), ).toBeVisible(); - expect(screen.getByText(/以云端模型列表为准/)).toBeVisible(); + expect( + screen.getAllByText( + "自定义模型需在 TRAE Work CN 中添加。FyAgent 不会写入其本地模型配置。", + ), + ).toHaveLength(1); expect(screen.queryByLabelText("服务地址")).not.toBeInTheDocument(); expect(screen.queryByLabelText("API Key")).not.toBeInTheDocument(); expect( @@ -928,9 +932,7 @@ describe("Models page", () => { ); expect(ports.workbuddy.saveModels).not.toHaveBeenCalled(); const dialog = await screen.findByRole("dialog", { name: "确认删除模型" }); - expect(dialog).toHaveTextContent( - "此操作将会删除该模型配置,不可恢复,是否确认删除", - ); + expect(dialog).toHaveTextContent("模型配置删除后无法恢复。"); expect(within(dialog).getByText("existing-model")).toBeVisible(); await user.click(within(dialog).getByRole("button", { name: "取消" })); expect( From ee475a784f6995ecfd933b3a6b78fe1ae8950193 Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:33:08 +0800 Subject: [PATCH 04/12] test(contracts): stabilize full-repository snapshot validation --- .trellis/spec/backend/task-runner-contract.md | 8 ++++++++ .../tasks/supported-platform-structure-assets.json | 2 +- tests/remainingPlatformSurface.test.ts | 11 ++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.trellis/spec/backend/task-runner-contract.md b/.trellis/spec/backend/task-runner-contract.md index 1a6dbfd35..5641c128f 100644 --- a/.trellis/spec/backend/task-runner-contract.md +++ b/.trellis/spec/backend/task-runner-contract.md @@ -333,6 +333,14 @@ tracked raster assets. These inventories are fail-closed review authorities, not content exclusions. Every listed file still passes the normal path, text, and structure scanners. +The whole-repository integration test captures its Git file list once, then +runs every scanner against that same snapshot. It has a bounded 15-second test +watchdog because filesystem enumeration and parsing more than 1,000 files is +not a product latency benchmark; other tests keep their default timeout. +Its zero-findings and inspected-file-count assertions remain mandatory, as do +all negative inventory fixtures. Do not change the production scanner or a +performance budget to accommodate integration-runner scheduling. + The source inventory is recomputed bidirectionally from all tracked Cargo manifests and build scripts plus executable/configuration files containing platform selectors. The candidate set, canonical paths, reviewed Git index diff --git a/scripts/tasks/supported-platform-structure-assets.json b/scripts/tasks/supported-platform-structure-assets.json index 3a0c4a034..77743103c 100644 --- a/scripts/tasks/supported-platform-structure-assets.json +++ b/scripts/tasks/supported-platform-structure-assets.json @@ -411,7 +411,7 @@ ], [ "tests/remainingPlatformSurface.test.ts", - "2ede6086570af653dd9725f5c8092f7644a33a081b4dd45ba7b15b4a38457941" + "b2b6c810ea4deb76c2efa5727b7c62e98eb14c2ab57095bc2e190f795a654855" ], [ "tests/singleInstanceActivationContract.test.ts", diff --git a/tests/remainingPlatformSurface.test.ts b/tests/remainingPlatformSurface.test.ts index 16d4ff0b8..b78c04bbd 100644 --- a/tests/remainingPlatformSurface.test.ts +++ b/tests/remainingPlatformSurface.test.ts @@ -1161,6 +1161,9 @@ describe("durable supported-platform surface contract", () => { }); it("runs every production scanner against the current repository snapshot without lifecycle exclusions", () => { + // This is a full-repository integration scan, not a product latency test. + // Enumerate one coherent snapshot instead of repeatedly invoking Git. + const currentFiles = checker.listCurrentFiles(ROOT); const indexModes = checker.listCurrentIndexModes(ROOT); const runner = ( _command: unknown, @@ -1183,12 +1186,10 @@ describe("durable supported-platform surface contract", () => { ), }; } - return checker.listCurrentFiles(ROOT).length > 0 + return currentFiles.length > 0 ? { status: 0, - stdout: Buffer.from( - `${checker.listCurrentFiles(ROOT).join("\0")}\0`, - ), + stdout: Buffer.from(`${currentFiles.join("\0")}\0`), } : { status: 1, stdout: Buffer.alloc(0) }; }; @@ -1201,7 +1202,7 @@ describe("durable supported-platform surface contract", () => { }); expect(report.findings).toEqual([]); expect(report.inspectedFiles).toBeGreaterThan(1_000); - }); + }, 15_000); it("fails closed when Git enumeration or file reads fail", () => { expect(() => From 9a6ceab48cd0448c2d5fc649efe5c8fedb0349c1 Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:35:31 +0800 Subject: [PATCH 05/12] chore(task): archive 09-14-concise-renderer --- .../2026-09}/09-14-concise-renderer/audit.md | 0 .../09-14-concise-renderer/check.jsonl | 4 +- .../09-14-concise-renderer/commit-plan.md | 11 +++++- .../2026-09}/09-14-concise-renderer/design.md | 0 .../09-14-concise-renderer/implement.jsonl | 4 +- .../09-14-concise-renderer/implement.md | 9 +++-- .../2026-09}/09-14-concise-renderer/prd.md | 2 +- .../09-14-concise-renderer/research.md | 0 .../research/performance-instrumentation.md | 0 .../research/validation-repairs.md | 37 ++++++++++++++++++ .../2026-09}/09-14-concise-renderer/task.json | 10 ++--- .../verification-initial.md | 0 .../09-14-concise-renderer/verification.md | 38 +++++++++++++++---- 13 files changed, 92 insertions(+), 23 deletions(-) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/audit.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/check.jsonl (65%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/commit-plan.md (88%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/design.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/implement.jsonl (65%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/implement.md (86%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/prd.md (95%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/research.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/research/performance-instrumentation.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/research/validation-repairs.md (74%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/task.json (70%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/verification-initial.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-concise-renderer/verification.md (83%) diff --git a/.trellis/tasks/09-14-concise-renderer/audit.md b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/audit.md similarity index 100% rename from .trellis/tasks/09-14-concise-renderer/audit.md rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/audit.md diff --git a/.trellis/tasks/09-14-concise-renderer/check.jsonl b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/check.jsonl similarity index 65% rename from .trellis/tasks/09-14-concise-renderer/check.jsonl rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/check.jsonl index b2df11bfe..2a86780a6 100644 --- a/.trellis/tasks/09-14-concise-renderer/check.jsonl +++ b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/check.jsonl @@ -1,7 +1,7 @@ {"file": ".trellis/spec/frontend/user-facing-copy.md", "reason": "Concise copy, actionable states and safety meaning"} {"file": ".trellis/spec/frontend/visual-language.md", "reason": "Shared typography and dialog hierarchy"} {"file": ".trellis/spec/frontend/surfaces-responsive.md", "reason": "Container sizing, readable metadata and scroll ownership"} -{"file": ".trellis/tasks/09-14-concise-renderer/research.md", "reason": "Community feedback and primary writing guidance"} +{"file": ".trellis/tasks/archive/2026-09/09-14-concise-renderer/research.md", "reason": "Community feedback and primary writing guidance"} {"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Full renderer gate and fail-fast warning guard lifecycle"} {"file": ".trellis/spec/backend/claude-code-cli.md", "reason": "Native npm adapter scope and unchanged execution authority"} -{"file": ".trellis/tasks/09-14-concise-renderer/research/validation-repairs.md", "reason": "Reviewed native identity changes and reproduced test guard failure"} +{"file": ".trellis/tasks/archive/2026-09/09-14-concise-renderer/research/validation-repairs.md", "reason": "Reviewed native identity changes and reproduced test guard failure"} diff --git a/.trellis/tasks/09-14-concise-renderer/commit-plan.md b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/commit-plan.md similarity index 88% rename from .trellis/tasks/09-14-concise-renderer/commit-plan.md rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/commit-plan.md index 5cc45385d..a7b68c312 100644 --- a/.trellis/tasks/09-14-concise-renderer/commit-plan.md +++ b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/commit-plan.md @@ -6,13 +6,13 @@ contract and test-warning repairs. No remote publication is authorized. ## Work commit -Proposed message: +Primary work commit `379bb0d1`: ```text refactor(ui): simplify secondary pages and close validation gaps ``` -Include the original 41 tracked UI/SPEC/test paths below, the fourteen explicitly +Include the original 41 tracked UI/SPEC/test paths below, the fifteen explicitly listed completion-repair paths (including performance and native-test isolation), and this task's planning/research/review artifacts. The previous UI modifications were reviewed as this same task; the completion @@ -82,6 +82,7 @@ config/playwright.performance.config.ts tests/architecture/rootGovernance.test.ts .trellis/spec/backend/proxy-runtime.md src-tauri/src/services/provider/mod.rs +tests/remainingPlatformSurface.test.ts ``` Do not include generated logs, screenshots, `node_modules`, dependency/API/data @@ -93,6 +94,12 @@ No production Provider behavior is modified. ## Bookkeeping after the work commit +Supplemental work commit `ee475a78`, before archive bookkeeping: +`test(contracts): stabilize full-repository snapshot validation`. +It contains the final scanner test, its reviewed source seal and the owning +task-runner SPEC. The complete canonical postarchive check passes with this fix; +see verification.md for the explicit test-watchdog versus performance distinction. + Use the existing `fyagent-concise-renderer-20260914` context identity. Preserve work-commit → archive-commit → journal-commit order; no amend and no push. The installed archive command moves the directory but does not rewrite its diff --git a/.trellis/tasks/09-14-concise-renderer/design.md b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/design.md similarity index 100% rename from .trellis/tasks/09-14-concise-renderer/design.md rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/design.md diff --git a/.trellis/tasks/09-14-concise-renderer/implement.jsonl b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/implement.jsonl similarity index 65% rename from .trellis/tasks/09-14-concise-renderer/implement.jsonl rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/implement.jsonl index b2df11bfe..2a86780a6 100644 --- a/.trellis/tasks/09-14-concise-renderer/implement.jsonl +++ b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/implement.jsonl @@ -1,7 +1,7 @@ {"file": ".trellis/spec/frontend/user-facing-copy.md", "reason": "Concise copy, actionable states and safety meaning"} {"file": ".trellis/spec/frontend/visual-language.md", "reason": "Shared typography and dialog hierarchy"} {"file": ".trellis/spec/frontend/surfaces-responsive.md", "reason": "Container sizing, readable metadata and scroll ownership"} -{"file": ".trellis/tasks/09-14-concise-renderer/research.md", "reason": "Community feedback and primary writing guidance"} +{"file": ".trellis/tasks/archive/2026-09/09-14-concise-renderer/research.md", "reason": "Community feedback and primary writing guidance"} {"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Full renderer gate and fail-fast warning guard lifecycle"} {"file": ".trellis/spec/backend/claude-code-cli.md", "reason": "Native npm adapter scope and unchanged execution authority"} -{"file": ".trellis/tasks/09-14-concise-renderer/research/validation-repairs.md", "reason": "Reviewed native identity changes and reproduced test guard failure"} +{"file": ".trellis/tasks/archive/2026-09/09-14-concise-renderer/research/validation-repairs.md", "reason": "Reviewed native identity changes and reproduced test guard failure"} diff --git a/.trellis/tasks/09-14-concise-renderer/implement.md b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/implement.md similarity index 86% rename from .trellis/tasks/09-14-concise-renderer/implement.md rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/implement.md index e913ef352..0e40592cb 100644 --- a/.trellis/tasks/09-14-concise-renderer/implement.md +++ b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/implement.md @@ -12,7 +12,7 @@ - [x] Update frontend copy/visual/feature SPEC owners and validate task context; subsequently resolve the reproduced baseline failures under the user's completion authorization. - [x] Record final results and limitations, review the full diff and prepare `commit-plan.md`. - [x] Obtain local work-commit/archive approval through the user's request to complete all remaining work. -- [ ] Commit the reviewed task scope, archive the task, validate archived references and record the session. Do not push. +- [x] Commit the reviewed task scope, archive the task and validate archived references. No push. ## Gates @@ -25,8 +25,11 @@ lifetime, and verify repeated full renderer tests with no unexpected warnings. - [x] Execute full local prearchive, release, browser and serial production performance checks; fix actionable failures without lowering thresholds. -- [ ] Update backend/frontend SPEC and final verification/commit plan, commit - task-scoped work locally, archive, journal and run canonical postarchive checks. +- [x] Update backend/frontend SPEC and final verification/commit plan, commit + task-scoped work locally, archive and pass canonical postarchive checks. + +The developer session is recorded through `add_session.py` after the archive +bookkeeping commit, using both work-commit hashes. No remote publication occurs. Use repository-owned `mise` commands. Run compilation/unit gates separately from performance profiling. Use targeted runs while iterating, then the complete frontend and browser gates. Do not update approved visual baselines automatically or weaken budgets/assertions to pass. diff --git a/.trellis/tasks/09-14-concise-renderer/prd.md b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/prd.md similarity index 95% rename from .trellis/tasks/09-14-concise-renderer/prd.md rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/prd.md index 5ddbd751f..b1e11ddc6 100644 --- a/.trellis/tasks/09-14-concise-renderer/prd.md +++ b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/prd.md @@ -23,7 +23,7 @@ Audit all eight renderer routes and secondary views; remove repetitive UI narrat - [x] Renderer-specific checks pass with the repaired warning guard; the previous full functional browser run passed 586 tests. Final repeat results are recorded in `verification.md`. - [x] Affected tests, frontend gate, browser checks, build and task/SPEC contracts pass; remaining native/manual limitations are stated. - [x] Owning frontend SPEC updates are prepared and reviewed before task archive. -- [ ] Archive the task after the authorized local work commit and successful full verification. +- [x] Archive the task after the authorized local work commits; validate the relocated context and pass the canonical full check without exclusions. ## Non-goals diff --git a/.trellis/tasks/09-14-concise-renderer/research.md b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/research.md similarity index 100% rename from .trellis/tasks/09-14-concise-renderer/research.md rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/research.md diff --git a/.trellis/tasks/09-14-concise-renderer/research/performance-instrumentation.md b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/research/performance-instrumentation.md similarity index 100% rename from .trellis/tasks/09-14-concise-renderer/research/performance-instrumentation.md rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/research/performance-instrumentation.md diff --git a/.trellis/tasks/09-14-concise-renderer/research/validation-repairs.md b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/research/validation-repairs.md similarity index 74% rename from .trellis/tasks/09-14-concise-renderer/research/validation-repairs.md rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/research/validation-repairs.md index 6e45a4636..b76fd5c31 100644 --- a/.trellis/tasks/09-14-concise-renderer/research/validation-repairs.md +++ b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/research/validation-repairs.md @@ -1,5 +1,20 @@ # Completion-gate repair review +## Final scanner integration follow-up + +The separate `prearchive-port-repair.log` run recorded a 5-second timeout in +`remainingPlatformSurface.test.ts`'s full-repository scanner case; it was not a +reported scanner finding. The test repeatedly enumerated Git in its runner +adapter. Its reviewed follow-up now enumerates one coherent current-file snapshot +and uses a case-local bounded 15-second allowance for the repository I/O. All +production scanners, zero findings, the inspected-file floor, identity validation +and negative tests remain unchanged. No global timeout or product frame/latency +budget changes. The modified sealed test identity is updated after source review. + +This change appeared after the initial work commit and is recorded as a separate +work commit before archive bookkeeping, never folded into an amend. The canonical +postarchive check is the acceptance gate for the final combined source. + Reviewed 2026-09-14 after the user's request to resolve every reported blocker. This supersedes the earlier proposed baseline-failure exception. No remote write, real login, CLI installation, live inference or visual-baseline approval is part @@ -95,3 +110,25 @@ and https://doc.rust-lang.org/std/net/struct.TcpListener.html specify that bindi port 0 lets the OS allocate the bound port, retrievable via `local_addr`. FyAgent's existing proxy owner already exposes it in its start result; no new helper is needed. The prevention rule is in backend/proxy-runtime.md, Tests Required. + +## Whole-repository test watchdog + +The `prearchive-port-repair.log` repeat stopped on Vitest's default 5-second +watchdog in the full-repository scanner test, not on a scanner finding. All +other 1,648 tests passed and one pre-existing host test was skipped. The native +port repair's focused test had already passed; this aggregate did not reach +the native suite and is not recorded as a full success. + +The test now captures the real current Git file list once and reuses it for +its snapshot runner, instead of invoking Git repeatedly while constructing the +same snapshot. Only this full-tree integration test gets a finite 15-second +watchdog. It still executes every scanner, requires zero findings and more than +1,000 inspected files, and retains all negative fixture tests and strict seals. +The production scanner and all product performance thresholds are unchanged. +This is an explicit test-harness deadline adjustment, not a performance claim. +Vitest documents the default at https://v3.vitest.dev/config/#testtimeout. + +The UI/native work commit and archive move had already completed while these +final scanner changes remained uncommitted. They therefore land in a separate +work commit before the archive bookkeeping commit; the full canonical postarchive +check is the final acceptance, without a lifecycle exclusion. diff --git a/.trellis/tasks/09-14-concise-renderer/task.json b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/task.json similarity index 70% rename from .trellis/tasks/09-14-concise-renderer/task.json rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/task.json index b17cc14d0..f1239922f 100644 --- a/.trellis/tasks/09-14-concise-renderer/task.json +++ b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/task.json @@ -3,7 +3,7 @@ "name": "concise-renderer", "title": "Simplify renderer copy and secondary-page layouts", "description": "Audit all eight renderer routes and secondary views; remove repetitive UI narration and metadata, simplify hierarchy without hiding safety or actions, verify behavior and layout, then update SPEC before archiving.", - "status": "in_progress", + "status": "completed", "dev_type": null, "scope": null, "package": null, @@ -11,16 +11,16 @@ "creator": "pythonrust", "assignee": "pythonrust", "createdAt": "2026-09-14", - "completedAt": null, + "completedAt": "2026-09-14", "branch": "dev/laiyongjie", "base_branch": "main", "worktree_path": null, - "commit": null, + "commit": "379bb0d113702421779e01eb4f49f4cf6c13c0aa", "pr_url": null, "subtasks": [], "children": [], "parent": null, "relatedFiles": [], - "notes": "", + "notes": "Local work commits: 379bb0d1 and ee475a78. Canonical postarchive check passed without exclusions; verification.md records evidence and native/manual limits. No remote push.", "meta": {} -} \ No newline at end of file +} diff --git a/.trellis/tasks/09-14-concise-renderer/verification-initial.md b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/verification-initial.md similarity index 100% rename from .trellis/tasks/09-14-concise-renderer/verification-initial.md rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/verification-initial.md diff --git a/.trellis/tasks/09-14-concise-renderer/verification.md b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/verification.md similarity index 83% rename from .trellis/tasks/09-14-concise-renderer/verification.md rename to .trellis/tasks/archive/2026-09/09-14-concise-renderer/verification.md index b554ad08e..6630edb3a 100644 --- a/.trellis/tasks/09-14-concise-renderer/verification.md +++ b/.trellis/tasks/archive/2026-09/09-14-concise-renderer/verification.md @@ -10,9 +10,9 @@ The user's final completion request supersedes the earlier proposal to archive with known baseline failures. The three baseline assertions, React warning guard/lifecycle issue and four additional Clippy errors have been resolved. The full current-host prearchive check, functional browser repeat and corrected -canonical production performance suite pass. The final aggregate repeat after -the timing-harness and native test-port repairs also exits 0. Postarchive -verification is recorded below after the task move. +canonical production performance suite pass. The final canonical postarchive +`mise run check` also passes, including the later scanner-harness correction, +without an active-task exclusion. Production native changes are limited to two mechanical repairs: conditional compilation of Windows-only npm string adapters (with host unit tests retained), @@ -39,7 +39,7 @@ configuration format, route, credential or stored user data was changed. | Renderer build / full functional browser repeat | Pass; all eight route chunks, 2 production boot cases and 586 functional browser cases | | Serial production performance suite | 35 pass in 2.7m, with the corrected dedicated timing configuration; no CLI override | | Trellis context | Both manifests resolve all 7 entries within injection limits | -| Canonical postarchive `mise run check` | Pending archive | +| Canonical postarchive `mise run check` | Pass; 187 unit files / 1,649 passes, 3,565 native passes, all release/task/source contracts; no task exclusion | The one unit skip is the existing `it.runIf(process.platform === "win32")` host test. Rust's six explicit ignores are two backup performance diagnostics, @@ -58,6 +58,10 @@ mise run test:browser mise run test:performance ``` +The prearchive command above records the task's historical active location. +After relocation, use `mise run check` without any exclusion and validate +`.trellis/tasks/archive/2026-09/09-14-concise-renderer` for the current context. + The last two run sequentially after the first, so compilation and parallel functional tests do not contaminate the serial production performance sample. Final commands use existing thresholds, zero performance retries and the normal @@ -82,6 +86,11 @@ passes. The earlier timing-config repeat is `prearchive-final.VdiXBx`. The complete prearchive log records 0 React act warnings and 0 Rust compiler warnings/errors. Generated logs/screenshots are ignored, not committed binaries. +Final postarchive evidence is `postarchive.pf7DQ1`: the complete canonical check +passes without exclusions, with 1,649 unit passes / 1 existing skip and 3,565 +native passes / 6 existing ignores. Its full log contains no failed-test, +compiler-warning or unexpected React act-warning markers. + ## Repaired failures and prevention The initial 3 native-source contract failures were independently reproduced at @@ -119,6 +128,16 @@ No unrelated local process was stopped. The focused native test and subsequent complete gate both pass. Details and the owning SPEC are in `research/validation-repairs.md`. +### Full-tree scanner test harness + +`prearchive-port-repair.log` separately records a 5-second Vitest timeout in +the integration test that scans the whole repository. The test now enumerates +one coherent Git snapshot rather than repeatedly enumerating it, and receives +a bounded 15-second watchdog. This is a test execution deadline adjustment; +all zero-findings/file-count/negative-case assertions and product performance +budgets remain unchanged. The fix and owning SPEC are included in the second +work commit and verified by the canonical postarchive run above. + ### Performance measurement repair The traced performance configuration reproduced content-resize p95 above the @@ -175,10 +194,13 @@ temporary artifact directories. ## Delivery boundary -SPEC updates are prepared before the work commit and archive. The user's local -commit/archive authorization is recorded in `commit-plan.md`; no remote push, -release or publishing action is performed. Archive completion and the canonical -postarchive result will be recorded after execution. +Ten owning SPEC files were updated before closeout. Local work commits are +`379bb0d1` (UI and validation repairs) and `ee475a78` (scanner test harness). +The task is at `.trellis/tasks/archive/2026-09/09-14-concise-renderer`, marked +completed, with both seven-entry manifests resolving their relocated references. +There is no active-task pointer. Archive bookkeeping and the developer journal +follow the work commits; no amend, remote push, release or publishing action +is performed. The canonical postarchive check above verifies this relocated tree. Current-host native tests do not prove Windows runtime, installer/signing, real-account login, credential replacement, live model inference or packaged From ce6e49161de9c6a417ed0193b7a94acb8c3c323a Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:36:00 +0800 Subject: [PATCH 06/12] chore: record journal --- .trellis/workspace/pythonrust/index.md | 7 +++-- .trellis/workspace/pythonrust/journal-2.md | 34 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.trellis/workspace/pythonrust/index.md b/.trellis/workspace/pythonrust/index.md index 2f32a0096..2171dd7e7 100644 --- a/.trellis/workspace/pythonrust/index.md +++ b/.trellis/workspace/pythonrust/index.md @@ -8,8 +8,8 @@ - **Active File**: `journal-2.md` -- **Total Sessions**: 87 -- **Last Active**: 2026-09-09 +- **Total Sessions**: 88 +- **Last Active**: 2026-09-14 --- @@ -19,7 +19,7 @@ | File | Lines | Status | |------|-------|--------| -| `journal-2.md` | ~771 | Active | +| `journal-2.md` | ~805 | Active | | `journal-1.md` | ~1987 | Archived | @@ -30,6 +30,7 @@ | # | Date | Title | Commits | Branch | |---|------|-------|---------|--------| +| 88 | 2026-09-14 | Simplify secondary pages and close validation gaps | `379bb0d113702421779e01eb4f49f4cf6c13c0aa`, `ee475a784f6995ecfd933b3a6b78fe1ae8950193` | `dev/laiyongjie` | | 87 | 2026-09-09 | Repair Grok PR 185 CI and archive superseded plans | `46e378e8`, `ac458fcb`, `98d57692` | `fix/grok-pr185-ci-closeout` | | 86 | 2026-09-07 | Prepare FyAgent 0.4.4 release | `9c058cee8ae8e29614be14fd3438662b3ff9a521` | `dev/laiyongjie` | | 85 | 2026-09-07 | Verify final contracts and clarify Windows batch evidence | `6efaff0b4aab8659f52e570654fa5c1882657cbb` | `dev/laiyongjie` | diff --git a/.trellis/workspace/pythonrust/journal-2.md b/.trellis/workspace/pythonrust/journal-2.md index 71f10a8bb..1a768803a 100644 --- a/.trellis/workspace/pythonrust/journal-2.md +++ b/.trellis/workspace/pythonrust/journal-2.md @@ -769,3 +769,37 @@ Update canonical and helper package versions, preserve the client compatibility ### Status [OK] **Completed** + + +## Session 88: Simplify secondary pages and close validation gaps + + +**Date**: 2026-09-14 +**Task**: Simplify secondary pages and close validation gaps +**Branch**: `dev/laiyongjie` + +### Summary + +Simplified all eight renderer route families and secondary surfaces, repaired baseline contracts and test warnings, validated the full local/browser/performance gates, and archived the task after updating ten SPEC owners. No remote push or live-account/native installer acceptance claimed. + +### Main Changes + +- Consolidated redundant detail metadata, removed repeated UI narration, preserved destructive-action, credential and source boundaries. +- Repaired act-warning guard lifetime, reviewed native source seals, platform-only helpers, test-port isolation and full-tree scanner snapshot handling. +- Separated production timing from trace recording without changing product animations, sample counts or performance budgets. + +### Git Commits + +| Hash | Message | +|------|---------| +| `379bb0d113702421779e01eb4f49f4cf6c13c0aa` | refactor(ui): simplify secondary pages and close validation gaps | +| `ee475a784f6995ecfd933b3a6b78fe1ae8950193` | test(contracts): stabilize full-repository snapshot validation | + +### Testing + +- [OK] Canonical postarchive mise run check passed without exclusions: 1649 unit passes, 3565 Rust passes, zero unexpected React or compiler warnings; existing host/live-only skips recorded. +- [OK] Canonical browser gate: 2 production boot cases and 586 functional cases passed; serial production performance: 35 passed. + +### Status + +[OK] **Completed** From 20dd65377db52f00708f453feaef99af9312b131 Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:35:48 +0800 Subject: [PATCH 07/12] docs(spec): split oversized contract owners --- .trellis/spec/backend/claude-code-cli.md | 97 ++-- .../spec/backend/external-agent-lifecycle.md | 358 ++---------- .trellis/spec/backend/external-agent-p0.md | 4 +- .../spec/backend/external-agent-sources.md | 252 +++++++++ .trellis/spec/backend/github-ci-workflow.md | 89 +-- .../spec/backend/github-push-commit-policy.md | 133 +++++ .trellis/spec/backend/index.md | 15 +- .trellis/spec/backend/native-task-runner.md | 268 +++++++++ .../backend/supported-platform-governance.md | 164 ++++++ .trellis/spec/backend/task-runner-contract.md | 529 ++---------------- .../spec/backend/trellis-prearchive-gate.md | 130 +++++ .../backend/windows-agent-runtime-security.md | 225 ++++++++ .../backend/windows-msvc-cross-diagnostic.md | 181 ++++++ .../spec/backend/windows-runtime-security.md | 515 ++--------------- .../tasks/09-14-audit-ahead-main/check.jsonl | 20 + .../tasks/09-14-audit-ahead-main/design.md | 72 +++ .../09-14-audit-ahead-main/implement.jsonl | 20 + .../tasks/09-14-audit-ahead-main/implement.md | 49 ++ .trellis/tasks/09-14-audit-ahead-main/prd.md | 36 ++ .../research/commit-audit.md | 85 +++ .../research/merge-handoff.md | 52 ++ .../research/spec-audit.md | 80 +++ .../tasks/09-14-audit-ahead-main/task.json | 26 + 23 files changed, 1971 insertions(+), 1429 deletions(-) create mode 100644 .trellis/spec/backend/external-agent-sources.md create mode 100644 .trellis/spec/backend/github-push-commit-policy.md create mode 100644 .trellis/spec/backend/native-task-runner.md create mode 100644 .trellis/spec/backend/supported-platform-governance.md create mode 100644 .trellis/spec/backend/trellis-prearchive-gate.md create mode 100644 .trellis/spec/backend/windows-agent-runtime-security.md create mode 100644 .trellis/spec/backend/windows-msvc-cross-diagnostic.md create mode 100644 .trellis/tasks/09-14-audit-ahead-main/check.jsonl create mode 100644 .trellis/tasks/09-14-audit-ahead-main/design.md create mode 100644 .trellis/tasks/09-14-audit-ahead-main/implement.jsonl create mode 100644 .trellis/tasks/09-14-audit-ahead-main/implement.md create mode 100644 .trellis/tasks/09-14-audit-ahead-main/prd.md create mode 100644 .trellis/tasks/09-14-audit-ahead-main/research/commit-audit.md create mode 100644 .trellis/tasks/09-14-audit-ahead-main/research/merge-handoff.md create mode 100644 .trellis/tasks/09-14-audit-ahead-main/research/spec-audit.md create mode 100644 .trellis/tasks/09-14-audit-ahead-main/task.json diff --git a/.trellis/spec/backend/claude-code-cli.md b/.trellis/spec/backend/claude-code-cli.md index 8a104063e..e0d20a69e 100644 --- a/.trellis/spec/backend/claude-code-cli.md +++ b/.trellis/spec/backend/claude-code-cli.md @@ -4,14 +4,16 @@ Read before changing Claude CLI detection, installation, updates, Agent surface policy or the ordinary-user Windows helper. Main owners are -`services/tooling/claude.rs`, shared `tooling/grok_npm.rs`, macOS -`tooling/npm_runtime.rs`, and `user-helper/src/{claude,grok_npm,cli,windows}.rs` -with the Windows Claude adapter at `user-helper/src/windows/claude.rs`. +`services/tooling/claude.rs`, macOS `tooling/npm_runtime.rs`, +`user-helper/src/{claude,cli,windows}.rs`, and the Windows Claude adapter at +`user-helper/src/windows/claude.rs`. [External Agent Lifecycle](./external-agent-lifecycle.md) owns inventory, jobs and IPC. [External Agent Auth](./external-agent-auth.md) owns official -login/logout observation. No new OAuth implementation or package dependency is -needed for this lifecycle. +login/logout observation. [External Agent Product Sources and Desktop +Identity](./external-agent-sources.md) owns shared Claude/Grok live npm +metadata, mirror/integrity admission and compact exact-version plans. No new +OAuth implementation or package dependency is needed for this lifecycle. ## 2. Signatures @@ -22,8 +24,11 @@ get_tool_versions(["claude"]) -> ToolVersion OfficialNpmTool = Grok | Claude GrokNpmInstallPlan::npm_argv_for(tool) -> closed exact-version argv -grok_npm::resolve_published_manifest(tool) -> live /latest + platform integrity -grok_npm::registries_matching_manifest(manifest) -> registries whose hashes match +external source owner: + grok_npm::resolve_published_manifest(Claude) + -> live root/current-platform manifest + grok_npm::registries_matching_manifest(manifest) + -> admitted registries fetch_npm_latest_for_tool(package, tool, local) -> dist-tags.latest or /latest version build_tool_search_paths(tool) -> login PATH + process PATH + product env default_install(installs) -> PATH default, else the sole entry @@ -72,46 +77,30 @@ generic command execution capability is added. - Windows still uses the existing manager search plus the ordinary-user helper; it does not copy the macOS env-only rule onto Alice's PATH. -### Live package and shared mirror policy - -- Exact version and SHA-512 authority is the current npm `latest` document, - resolved at runtime. Prefer `registry.npmjs.org`; if that host is - unreachable, try the shared mainland chain (Tencent, Huawei, npmmirror). - The document supplies `@anthropic-ai/claude-code` version, `dist.integrity`, - and the current Darwin/Windows x64/arm64 optional package version. Do not - compile a reviewed version/hash JSON into the product, and do not repeat - version/hash literals in generic specs. -- Directory `latest_version` display uses `fetch_npm_latest_for_tool`: npmjs - packument `dist-tags` first, then the same `/latest` document fallback. Claude - may consider the `next` tag only when the local version is already newer - than `latest`. Display and install may race; both must resolve live, never - a compiled pin. -- After the published version is known, each candidate in Tencent, Huawei, - npmmirror, npmjs must return matching root and current-platform package name, - exact version and SHA-512. HTTPS-only, no redirects, per-request timeout and - a streaming 1 MiB metadata limit apply. Missing/mismatching sources fail - closed. npm argv is always `package@`; never - `package@latest`. A mainland `dist-tags.latest` (for example npmmirror) may - point at an older release; that is why argv never uses the `latest` tag. -- `default_install_command()` is a command-shape fixture - (`@xai-official/grok@1.2.3` plus the Tencent registry). It is not version - authority. macOS, formal Windows, and development Windows LocalProcess - resolve `resolve_published_manifest` before any npm install/update. -- npm receives exact package/version, general registry and the matching - `@anthropic-ai:registry` option for that invocation. Scope config must not - silently redirect the request to a different registry. Global/user npmrc and - shell profiles are not rewritten. Grok uses the same scoped-registry owner. -- Claude retains optional dependencies. Newer npm requires a narrow - `--allow-scripts=@anthropic-ai/claude-code` allowance; never allow arbitrary - scripts. The reviewed root installer links/copies its matching native - optional package; a stub or process exit alone is not install success. +### Claude projection of the shared npm source + +- Source resolution consumes the exact manifest and admitted registries from + [External Agent Product Sources and Desktop Identity](./external-agent-sources.md). + Claude lifecycle code must not reimplement mirror ordering, metadata bounds, + current-platform package selection or root/platform integrity comparison. +- Directory `latest_version` uses `fetch_npm_latest_for_tool`: npmjs packument + `dist-tags` first, then the shared bounded `/latest` fallback. Claude may + consider `next` only when the local version is already newer than `latest`. + Display and install may race; install resolves a fresh exact manifest. +- Claude npm argv contains `@anthropic-ai/claude-code@`, the + admitted general registry and the matching `@anthropic-ai:registry` option + for that invocation. Global/user npmrc and shell profiles are not rewritten. +- Claude retains optional dependencies. npm versions requiring script policy + receive only `--allow-scripts=@anthropic-ai/claude-code`; arbitrary script + allowance is forbidden. The root installer must produce the matching native + optional package; process exit or a stub alone is not success. - Require the reviewed Node major floor and Node architecture matching the - product. Do not automatically install Node, use sudo, switch architecture, - or edit PATH. macOS checks an already-discoverable global bin directory; - update checks that npm's prefix matches the selected installation. -- Root/optional metadata comparison and npm's package integrity checks are - the inherited distribution contract, not a claim of registry-independent - signed artifact verification or of login/inference availability. + product. Do not install Node, use sudo, switch architecture or edit PATH. + macOS checks an already-discoverable global bin directory; update checks that + npm's prefix matches the selected installation. +- Shared root/optional metadata comparison and npm package-integrity checks are + distribution admission, not proof of registry-independent artifact signing, + successful login or inference availability. ### Native execution boundaries @@ -182,12 +171,12 @@ owner/prefix rejection, CLI-only policy and post-install observation. Renderer tests must parse compact `claude-code` readiness as `cli` / `cli_tooling` and reject Desktop/`managed_desktop`. Mirror smoke uses an isolated temporary home/prefix/cache and no login or inference. -`grok_npm` tests must parse a `/latest` document version, reject -`version=latest`, keep fixture argv free of `@latest`, and must not -`include_str!` a version/hash JSON. `command_with_script_policy` must add -`--allow-scripts=@xai-official/grok` only for npm ≥ 12 and must not duplicate -an existing flag. `default_install` tests must prefer -PATH default over a second copy. +The shared source-owner tests must parse a `/latest` document version, reject +`version=latest`, keep executable argv free of `@latest`, match root and +current-platform integrity and prove no compiled version/hash JSON exists. +Claude tests additionally require its exact scoped-registry option and narrow +script allowance. `default_install` tests prefer the PATH default over a +second copy. Windows native helper execution and real vendor login require their own matching-host evidence; macOS and portable tests do not establish it. Helper contract tests must require `npm.cmd` discovery plus @@ -210,12 +199,10 @@ wrong: walk ~/.mise / nvm / volta trees; treat any second copy as unsupported wrong: run user npm from the elevated desktop process wrong: Command::new("npm.cmd") as the helper application name wrong: renderer surfacesForAgent(claude-code)=desktop; sourceKind=managed_desktop -wrong: development Windows LocalProcess `npm i -g @xai-official/grok@1.2.3` -wrong: npm 12 without --allow-scripts=@xai-official/grok; exit 0 -> succeeded correct: login/process PATH + product env -> PATH-default owner correct: registry /latest -> exact version + integrity -> matching registry -> closed plan -> ordinary-user execution -> actual CLI version/owner correct: Windows .cmd shim -> cmd /D /S /C call "{quoted}" via raw_arg correct: renderer admits compact CLI readiness (cli_tooling, no surfaces array) -correct: LocalProcess npm 12 --allow-scripts=@xai-official/grok; reread grok --version +correct: Claude npm script policy allows only @anthropic-ai/claude-code ``` diff --git a/.trellis/spec/backend/external-agent-lifecycle.md b/.trellis/spec/backend/external-agent-lifecycle.md index dfba9ce25..137b710c6 100644 --- a/.trellis/spec/backend/external-agent-lifecycle.md +++ b/.trellis/spec/backend/external-agent-lifecycle.md @@ -4,7 +4,8 @@ Read this contract before changing Agent install readiness, installation inventory, target selection, install/update/launch admission, vendor source -resolution, transfer/job state, desktop identity, or platform deployment. +capability consumption, transfer/job state, deployment orchestration, rollback, +or recovery. Primary owners: @@ -12,10 +13,8 @@ Primary owners: action matrix and default surface; - `src-tauri/src/agent_install/inventory.rs` — normalized candidates, destinations, opaque capabilities, freshness and revalidation; -- `src-tauri/src/agent_install/sources/**` and `fetch.rs` — reviewed source - metadata, redirects and artifact transport; - `src-tauri/src/agent_install/desktop.rs`, `windows.rs`, `macos.rs`, `cli.rs` - — product/platform evidence and execution adapters; + — execution adapters consuming already admitted source/identity evidence; - `src-tauri/src/agent_install/jobs.rs` and `types.rs` — job slot, snapshots, transfer state and closed wire types; - `src-tauri/src/commands/agent_install_readiness.rs` — Tauri transport. @@ -24,7 +23,9 @@ This contract does not own the static catalog/runtime surface ([Catalog and Runtime](./external-agent-catalog-runtime.md)), Auth sessions ([External Agent Auth](./external-agent-auth.md)), or the reusable Codex installer/native helper primitives -([Codex Desktop Installer](./codex-desktop-installer.md)). +([Codex Desktop Installer](./codex-desktop-installer.md)). Product source, +artifact and closed desktop identity rules are owned by +[External Agent Product Sources and Desktop Identity](./external-agent-sources.md). ## 2. Signatures @@ -140,131 +141,22 @@ command, argument vector, token, hash, package format, signer or bypass flags. with `lifecycle_policy.rs`. Treating Claude Code as `managed_desktop` is not a product-absent signal; it is a contract parse failure. -### Product and source policy - -| Product | Owner and current lifecycle policy | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Grok Build | CLI Tooling owner; default fresh install uses the official `@xai-official/grok` npm package, the current registry `latest` resolved at runtime to an exact version + SHA-512, and a mainland-first registry chain. Native `x.ai` install is an explicit secondary action. Updates preserve the observed `native_internal` or `official_npm` owner. | -| Codex | Dedicated Codex Desktop installer; Agent action returns `managed_by_codex_desktop` and does not occupy the Agent job slot. | -| QoderWork CN | Managed desktop; install/launch admitted, FyAgent update disabled. Source is the reviewed first-party `/qoder-work-cn/releases/latest/` aliases and same-host Electron-builder version feed. | -| TRAE Work CN | Managed desktop; install/launch admitted, FyAgent update disabled. Resolve `data.solo` with `region=cn`; never TRAE Code/`data.manifest`. | -| WorkBuddy | Managed desktop; install/launch admitted, FyAgent update disabled. Resolve the closed `/v2/update` platform IDs and reviewed macOS suffix rewrite. | -| Claude Code | CLI only; runtime-resolved official npm latest, shared verified mirrors, owner-preserving updates and ordinary-user execution. See [Claude Code CLI](./claude-code-cli.md). | -| OpenCode Desktop | Desktop only; use reviewed stable desktop artifacts and closed bundle identity on supported hosts. No public OpenCode CLI installer. | - -- Grok npm optional-package admission is resolved by the signed product on the - product host. The closed mappings are Darwin x64/arm64 and Windows x64/arm64; - an unsupported architecture produces no install plan. The current helper's - platform selector is defined only for macOS/Windows; Linux compilation and - package support cannot be inferred from these product-host tests. - The runtime-resolved optional-package map admits no Linux package and no - generic `std::env::consts::OS` fallback. -- Before execution, the product matches both `@xai-official/grok` and the - current platform package SHA-512 against one allowed registry. Version - authority is `resolve_published_manifest` (npmjs `/latest` first, then the - mainland chain). Formal Windows sends only the compact exact-version / - registry / allow-scripts control to the ordinary-user helper. The helper - validates and executes that closed control; it does not resolve registry - metadata, choose a platform package, or invent `@latest`. -- Development Windows LocalProcess is not a second product policy. It - composes the same live argv in-process: never execute - `default_install_command()`'s `@xai-official/grok@1.2.3` fixture, add - `--allow-scripts=@xai-official/grok` when the executing npm major is ≥ 12, - and do not report Agent success if npm blocked that postinstall or the - PATH-default grok version is still below the planned version. Formal - Windows stays on the helper; LocalProcess is development builds only. -- macOS Claude/Grok discovery uses login-shell PATH, process PATH, and product - env. It does not walk mise/nvm/fnm/Volta trees. `default_install` is the - PATH-default copy. See [Claude Code CLI](./claude-code-cli.md). -- CLI `ToolVersion.latest_version` is always live: Claude/Grok/Codex/Gemini/ - OpenClaw from npm registry (OpenCode may fall back to GitHub latest; Hermes - from PyPI). Desktop products keep vendor feeds. Do not compile a reviewed - CLI version/hash JSON. -- Qoder display version comes only from an unindented top-level `version:` in - bounded same-host `latest.yml`/`latest-mac.yml`. The feed ZIP and `sha512` - are metadata, not admitted artifacts. Windows ARM64 remains unsupported - until a separately reviewed first-party artifact exists. -- TRAE source selection uses the Work/Solo CN object and closed host/path/ - filename rules. Local comparable version is `tronBuildVersion` from bounded - `product.json`, not the Electron marketing `appVersion`. -- WorkBuddy uses closed platform IDs and the official download host. On macOS, - rewrite only the validated terminal `.zip` suffix to `.dmg`. A shorter local - dotted marketing version may equal a longer remote product-version prefix; - same-length differing segments remain an update. -- Claude has no public Desktop download resolver. OpenCode uses the reviewed - locale-neutral stable Desktop aliases, including - `windows-x64-nsis` on Windows x64. GitHub latest is display-only and must not - gate installability. Windows x64 OpenCode install is admitted after the - reviewed WinVerifyTrust identity contract; ARM64 remains unsupported. -- A missing/drifted source schema, host, redirect or release capability returns - `source_not_verified`/official-page guidance. Never pin a package URL copied - from an investigation or infer a version from ETag, Last-Modified or prose. - -### Fetch and release capability - -- HTTPS only; no userinfo and no explicit non-default port. -- Validate every redirect hop against the product allowlist and bounded hop - count. Scheme downgrade or unknown host fails closed. -- Metadata is bounded to 1 MiB and artifacts to 2 GiB under the current - installer transport. -- `expectedReleaseId` binds the canonical, backend-resolved release fields for - products that require source freshness. A forced refresh must match before - download. Release IDs never encode or expose a URL. -- Cancellation maps to `cancelled`, not source/schema failure. - -### Closed desktop identity - -Folder names and vendor config directories are not identity. Current closed -identity examples include: - -| Product | macOS bundle ID | Windows closed identity summary | -| ----------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| WorkBuddy | `com.tencent.workbuddy.mac` | Closed relative `WorkBuddy.exe`, ProductName and reviewed signer. | -| QoderWork CN | `com.qoder.work.cn` | Closed QoderWork CN relative EXE names, ProductName and signer. | -| TRAE Work CN | `cn.trae.solo.app` | Closed TRAE SOLO/Work CN relative EXE names, ProductName and signer. | -| OpenCode | `ai.opencode.desktop` | Closed relative `@opencode-aidesktop/OpenCode.exe` (and `OpenCode/OpenCode.exe`), ProductName `OpenCode`, reviewed signer `Anomaly Innovations, Inc https://anoma.ly/`, and Uninstall DisplayName `OpenCode` or `OpenCode `. | -| Claude (legacy identity only) | `com.anthropic.claudefordesktop` | Not an admitted Agent lifecycle target; current Claude support is CLI-only. | - -Windows scan identity is the installed target, not the downloaded installer -leaf: - -- Freeze `windows_relative_exes` from a WinVerifyTrust-Valid installed EXE - under Alice `LocalAppData\Programs`, plus any reviewed installer-stub - relative that still occurs on disk. OpenCode's official `windows-x64-nsis` - stub may be i386 `OpenCode/OpenCode.exe` while the current-user install is - AMD64 `%LOCALAPPDATA%\Programs\@opencode-aidesktop\OpenCode.exe`. Keep both. -- KnownPath `Missing` is dropped. It is not retained evidence and cannot - become `not_installed` by itself. Uninstall/App Paths hints remain. -- Uninstall `DisplayName` matches a closed ProductName exactly - (ASCII-case-insensitive) or `{name} {bounded_version}`. Reject channel - words (`Dev`, `Beta`) and prerelease suffixes (`1.18.27-beta`). Empty - `InstallLocation` is allowed; `DisplayIcon` and derived uninstall directories - are hints, never commands. -- Fresh Windows destination for OpenCode is `WindowsCurrentUser` (same - family as QoderWork CN). Destination `location_label` uses the catalog - display name and is not the known-path folder; do not invent scan relatives - from that label. -- In-app NSIS handoff and a later user-run official installer share this scan. - Successful `ShellExecute` still does not prove installed. -- macOS scans direct-child `.app` bundles in user/system Applications roots, - rejects symlinks and verifies plist/bundle identity. Absence on a shipped - host is `not_installed`; Linux development remains `unknown`. -- Windows combines the frozen Explorer user and machine Uninstall/App Paths in - explicit registry views with known roots. Registry strings are evidence, - never commands. Open inventory parents with query+enumerate rights and - children query-only; rejected shared-view links are absence, while access, - enumeration, bound or Shell-context failure makes the aggregate unknown. -- A Windows candidate is actionable only after stable no-reparse file - identity, supported application architecture, closed ProductName, - `WinVerifyTrust`, exactly one signer and reviewed signer leaf. -- Do not infer installation from `.workbuddy`, `.qoderwork*`, `.trae*` or any - settings directory. -- Official WorkBuddy macOS identity is `com.tencent.workbuddy.mac` from the - signed Tencent `WorkBuddy.app` package. `com.workbuddy.workbuddy` is a stale - closed ID and must not match. `CodeBuddy CN.app` / - `com.tencent.codebuddycn` is a different product. Folder names are not - identity. Scan, system-commit policy, and privileged helper `Policy.swift` - stay in lockstep on this ID. +### Source and desktop-identity routing + +[External Agent Product Sources and Desktop Identity](./external-agent-sources.md) +owns product release discovery, exact npm source admission, redirect/artifact +bounds, closed Desktop identity, and platform scan evidence. This lifecycle +contract consumes only the resulting release capability or normalized platform +observation; it does not duplicate product URLs, registry/hash rules, bundle +IDs, EXE relatives, signer leaves or Uninstall matching. + +Source and identity failure must remain evidence-strength preserving: + +- missing/drifted source data yields `source_not_verified`, never a stale URL; +- incomplete native identity yields `unknown`/unsupported, never installed; +- vendor-wizard handoff is not installed evidence; +- CLI package resolution supplies an exact plan, while lifecycle owns only the + action/job transition around that plan. ### Jobs and platform side effects @@ -325,21 +217,11 @@ leaf: | Windows EXE product/signer/trust/arch/helper/pipe binding fails | Fail before installer launch. | | User cancels Windows UAC/vendor launch | Cancelled/installer-user-cancelled result. | | Windows official EXE ShellExecute succeeds | Job succeeded as handoff; do not claim installed proof. | -| OpenCode Windows x64 identity is complete | Admit current-user NSIS handoff; GitHub latest must not gate the stable source. | -| OpenCode Windows ProductName/relative EXE/signer is empty | Reject EXE download/install; do not claim supported. | -| OpenCode known-path `@opencode-aidesktop\OpenCode.exe` exists with closed ProductName and reviewed signer | Inventory `installed`; `launch` is legal. Manual NSIS uses the same scan as in-app handoff. | -| OpenCode Uninstall DisplayName is `OpenCode ` | Keep the ARP hint; do not require exact `OpenCode`. | -| OpenCode Uninstall DisplayName is `OpenCode Dev`, `OpenCodeAI`, or a prerelease version | Skip that ARP entry. | -| OpenCode KnownPath relative is missing | Drop the observation; do not retain KnownPath Missing. | -| Grok default install has no native expected owner | Resolve official npm `latest` to an exact version + integrity; never install `@latest`. | -| Multiple PATH-visible Claude/Grok copies, one is PATH default | Use PATH default; do not fail as owner-unsupported. | -| CLI latest display uses a compiled version/hash JSON | Contract regression; resolve live registry/`/latest`. | -| Grok macOS/Windows architecture has no closed platform package or manifest integrity | Produce no npm plan/action; do not fall back to Linux or another product package. | -| Non-macOS/non-Windows development host | No admitted CLI package; verify host compilation separately under the development-environment contract, without inventing a Linux installer or crate-wide rejection policy. | -| Grok registry metadata does not match both root and current-platform SHA-512 | Skip that registry; fail with source exhaustion when none match. | -| Development Windows Grok npm argv uses `@xai-official/grok@1.2.3` or `@latest` | Contract regression; resolve live exact version first. | -| npm 12+ omits `--allow-scripts=@xai-official/grok` on a Grok install/update | postinstall blocked; treat as failure even if npm exit 0. | -| npm exit 0 but PATH-default grok is still below the planned version | Fail that registry attempt; do not report Agent `succeeded`. | +| Focused source/identity owner emits one complete trusted candidate | Lifecycle may normalize it and expose only policy-legal actions. | +| Source/identity evidence is absent, conflicting, stale or incomplete | Preserve unknown/not-installed distinction; do not synthesize a candidate or action. | +| Source/desktop identity owner cannot produce an admitted release or installed identity | Preserve its fail-closed reason; do not create or advance a lifecycle job. | +| Claude/Grok source owner cannot produce an exact current-platform npm plan | No package action; never substitute a tag, fixture, foreign platform package or stale manifest. | +| CLI execution exits but owner/version verification is not satisfied | Terminal verification failure; do not report Agent `succeeded`. | | Cancel after `launching_installer`/`installing` | `operation_conflict`; do not kill external/commit operation. | | Secret/path/raw native identity reaches DTO/log/DOM | Security regression. | @@ -354,26 +236,18 @@ leaf: target or restores the prior bundle. - **Base:** Windows vendor wizard opens successfully; the job is a successful handoff while installation status stays unchanged until a fresh inventory. -- **Good:** after a user-run official OpenCode NSIS, a complete scan finds - `@opencode-aidesktop\OpenCode.exe` (or an Uninstall DisplayName - `OpenCode 1.18.27` plus DisplayIcon) and readiness exposes `launch`. +- **Good:** after a manual vendor install, the focused identity owner emits one + complete trusted candidate and a fresh lifecycle scan exposes only the + policy-legal action for that normalized identity. - **Base:** complete Windows discovery finds no candidate and exposes an eligible reviewed destination; an inaccessible view instead remains unknown. - **Bad:** use a researched CDN URL, infer install from a config directory, update Qoder/TRAE/WorkBuddy, choose the first candidate, fake percent without total bytes, or label Windows wizard handoff as installed evidence. -- **Bad:** install Grok with `@latest`, compile a reviewed npm version JSON, - change the user's global npmrc, or claim mainland sign-in/inference because - the CLI installed. -- **Bad:** on development Windows, run `default_install_command()`'s - `@xai-official/grok@1.2.3` fixture, omit npm 12 `--allow-scripts=@xai-official/grok`, - or treat npm exit 0 / "changed N packages" as success while PATH-default - grok remains the previous version. -- **Bad:** treat GitHub latest failure as OpenCode uninstallable, freeze only - the NSIS stub path `OpenCode/OpenCode.exe`, require exact Uninstall - DisplayName equality, or describe Windows OpenCode as supported while - ProductName/relative EXE/signer stay empty. +- **Bad:** bypass the source/identity owner with a tag, fixture, researched URL, + unverified path or stale capability, or treat process exit/vendor handoff as + installed evidence without the required post-action inventory/owner check. ## 6. Tests Required @@ -394,37 +268,23 @@ Assertion points: no source lookup or side effect; - inventory merges duplicate provenance but preserves multiple/conflicting/ incomplete evidence, expires capabilities and rejects drift; -- Qoder/Trae/WorkBuddy/OpenCode source parsers enforce exact host, - platform, schema, redirect and version rules without stale URL fallback; -- Claude CLI tests cover runtime-resolved npm latest, shared registry/argv/helper, - actual version/owner verification, and rejection of the retired Desktop path; - `grok_npm` must reject `version=latest` and must not `include_str!` a - version/hash JSON; `default_install` prefers PATH default; +- source/identity owner tests independently enforce exact host/platform/schema, + redirect, current-platform npm admission and closed desktop identity; this + lifecycle suite proves those failures prevent job creation or advancement; +- Claude/Grok lifecycle tests consume only admitted exact plans, preserve the + observed owner, require post-action owner/version verification and reject the + retired/unsupported surface; source mechanics stay in their focused owner; - renderer `surfacesForAgent` / readiness `sourceKind` stay aligned with lifecycle policy: Grok and Claude are compact CLI/`cli_tooling`; - macOS exact-path deployment, cancellation boundary, running-app protection, rollback/recovery and disabled `/Applications` gate; -- WorkBuddy macOS scan matches only `com.tencent.workbuddy.mac`; a same-folder - `com.workbuddy.workbuddy` fixture stays unmatched; helper/policy/desktop - bundle IDs stay equal via `helper_policy_bundle_ids_match_macos_bundle_id_for`; -- Windows registry access masks/views/link handling, trusted PE identity, - signer leaf, retained artifact, helper protocol/pipe binding, UAC cancel and - vendor-wizard handoff with no wait/kill/post-install claim; -- OpenCode Windows identity keeps both `@opencode-aidesktop/OpenCode.exe` and - `OpenCode/OpenCode.exe`, signer `Anomaly Innovations, Inc https://anoma.ly/`, - and Uninstall DisplayName `{name}` or `{name} {bounded_version}`; OpenCode - catalog copy must not say 「本机识别和启动暂无法确认」; +- platform-specific source, desktop identity, registry/signer and helper + protocol assertions run in their focused owners; lifecycle integration proves + only normalized evidence/capabilities can authorize actions; - job single-flight, terminal slot release, transfer monotonicity, unknown total, cancel refusal after side-effect boundary and unknown job ID; -- Grok owner-preserving lifecycle and ordinary-user helper with no elevated - fallback; product-host cfg maps only Darwin/Windows x64/arm64, runtime - resolution admits no Linux optional package, registry admission matches both - package integrities, and the helper receives only the compact host-selected - plan; -- development Windows LocalProcess Grok npm uses `resolve_published_manifest` - (not `1.2.3`), adds `--allow-scripts=@xai-official/grok` only for npm ≥ 12, - treats blocked install scripts as failure, and rereads PATH-default - `grok --version` before `succeeded`; +- Grok/Claude owner-preserving lifecycle has no elevated fallback and reaches + `succeeded` only after the focused execution owner reports verified outcome; - renderer polls until a terminal native stage and does not paint a poll cap as failure while a job remains active. Browser fixtures do not prove native inventory, installer or signing behavior. @@ -476,37 +336,6 @@ sourceKind === "cli_tooling" Wrong: -```rust -include_str!("claude_npm_manifest.json"); -npm_argv = ["i", "-g", "@anthropic-ai/claude-code@latest"]; -``` - -Correct: - -```rust -let manifest = grok_npm::resolve_published_manifest(OfficialNpmTool::Claude).await?; -// npm argv is package@; never @latest -``` - -Wrong: - -```text -development Windows LocalProcess: - npm i -g @xai-official/grok@1.2.3 - npm exit 0 / "changed 3 packages" -> Agent succeeded -``` - -Correct: - -```text -development Windows LocalProcess: - resolve_published_manifest -> npm i -g @xai-official/grok@ - npm 12+ --allow-scripts=@xai-official/grok - PATH-default grok --version must reach the planned version -``` - -Wrong: - ```rust let candidate = inventory.candidates.first().unwrap(); launch(candidate.path)?; @@ -519,102 +348,3 @@ let validated = validate_action_target(&request, state).await?; // The validated capability is produced only after fresh re-enumeration. dispatch_closed_action(validated, state).await ``` - -Wrong: - -```rust -windows_relative_exes: &["OpenCode/OpenCode.exe"]; -if display_name != "OpenCode" { continue; } -``` - -Correct: - -```rust -windows_relative_exes: &[ - "@opencode-aidesktop/OpenCode.exe", - "OpenCode/OpenCode.exe", -]; -uninstall_display_name_matches(display_name, &["OpenCode"]) -// exact name, or `OpenCode` + space + bounded_version -``` - -## Scenario: OpenCode Windows scan after vendor or manual NSIS - -### 1. Scope / Trigger - -- Trigger: OpenCode Windows x64 is installable through the Agent façade, and - a later inventory scan must find both in-app handoff and a user-run - official NSIS. This is a cross-layer readiness/inventory contract: empty - identity hides Install; a stub-only known-path reports `not_installed` - after a real current-user install. - -### 2. Signatures - -```text -windows_exe_install_admitted(opencode) - -> ProductName/relative EXE nonempty - -get_agent_install_readiness({ agentId: opencode }) - -> installState / allowedActions from the same inventory scan - -uninstall_display_name_matches(displayName, ["OpenCode"]) - -> exact ASCII-case-insensitive name - OR name + " " + bounded_version -``` - -No new wire version. Renderer still sends only `agentId` + action + opaque -capabilities. - -### 3. Contracts - -- Request: renderer never sends a path, Uninstall key, or signer. -- Response: `installState=installed` and `launch` only after a trusted PE at a - closed relative or a matching Uninstall/App Paths hint that inspects to the - same identity. -- Helper product `opencode` admits download/handoff. It does not prove the - scan relatives. Identity lives in `desktop.rs` / `windows.rs`. -- Environment: Alice `LocalAppData\Programs` plus frozen Uninstall/App Paths. - ARM64 stays `platform_unsupported`. - -### 4. Validation & Error Matrix - -| Condition | Required result | -| ------------------------------------------------------------------------------------- | --------------------------------------------------- | -| Identity fields empty | No Install; catalog may not claim local recognition | -| Installed `@opencode-aidesktop\OpenCode.exe` Valid | `installed` + Launch | -| DisplayName `OpenCode 1.18.27`, InstallLocation empty, DisplayIcon points at that EXE | Keep Uninstall hint; inspect the icon/derived path | -| DisplayName `OpenCode Dev` / `OpenCodeAI` | Skip | -| KnownPath `OpenCode\OpenCode.exe` missing | Drop; do not fail the aggregate | - -### 5. Good / Base / Bad Cases - -- Good: user uninstalls, runs official NSIS, reopens Agents; card shows - Launch. -- Base: wizard handoff succeeds; status stays unchanged until the next scan. -- Bad: exact DisplayName equality, or treating the destination label - `Programs\OpenCode` as the known-path. - -### 6. Tests Required - -- `opencode_windows_identity_is_frozen_from_winverifytrust_hil` -- `uninstall_display_name_matches_closed_name_or_bounded_version_suffix` -- OpenCode catalog description omits 「本机识别和启动暂无法确认」 -- complete empty Windows discovery still exposes OpenCode - `WindowsCurrentUser` - -### 7. Wrong vs Correct - -#### Wrong - -```text -installer stub path only -> scan miss after electron-builder current-user install -DisplayName == "OpenCode" -> drop `OpenCode 1.18.27` -``` - -#### Correct - -```text -installed-target relative + stub relative -DisplayName exact or `{name} {bounded_version}` -WinVerifyTrust + ProductName + reviewed signer remain admission -``` diff --git a/.trellis/spec/backend/external-agent-p0.md b/.trellis/spec/backend/external-agent-p0.md index d0d854dd0..9ec3bc517 100644 --- a/.trellis/spec/backend/external-agent-p0.md +++ b/.trellis/spec/backend/external-agent-p0.md @@ -9,7 +9,8 @@ the following focused specifications: | Change area | Read this Spec | | --- | --- | | Static product catalog, runtime observation, trusted launch and ACL | [External Agent Catalog and Runtime](./external-agent-catalog-runtime.md) | -| Readiness, inventory, opaque targets, install/update/launch jobs and source resolution | [External Agent Lifecycle](./external-agent-lifecycle.md) | +| Readiness, inventory, opaque targets, install/update/launch jobs, rollback and recovery | [External Agent Lifecycle](./external-agent-lifecycle.md) | +| Product release sources, exact npm admission, artifact bounds and closed desktop identity | [External Agent Product Sources](./external-agent-sources.md) | | Login/logout/provider sessions and Auth observation | [External Agent Auth](./external-agent-auth.md) | | Qoder Hooks configuration | [QoderWork Hooks Configuration](./qoderwork-hooks.md) | | TRAE model preflight/observation and OpenCode models | [External Agent Model Integration](./external-agent-models.md) | @@ -20,6 +21,7 @@ Related native/security owners remain separate: - [Codex Desktop Installer](./codex-desktop-installer.md) - [Windows Shell-user Runtime](./windows-runtime-security.md) +- [Windows Agent Runtime Security](./windows-agent-runtime-security.md) - [macOS Privileged System-Commit Helper](./macos-system-commit.md) - [SecretRef Native Backend](./secretref-backend.md) diff --git a/.trellis/spec/backend/external-agent-sources.md b/.trellis/spec/backend/external-agent-sources.md new file mode 100644 index 000000000..0145d8a83 --- /dev/null +++ b/.trellis/spec/backend/external-agent-sources.md @@ -0,0 +1,252 @@ +# External Agent Product Sources and Desktop Identity + +## 1. Scope / Trigger + +Read this contract before changing product release discovery, redirect and +artifact admission, CLI package-source selection, desktop installation +identity, or the platform evidence used to turn a scanned application into a +trusted Agent candidate. + +Primary owners are `src-tauri/src/agent_install/sources/**`, `fetch.rs`, +`desktop.rs`, `windows.rs`, `macos.rs`, and the product-specific source and +identity tables they compose. Shared Claude/Grok npm source mechanics are owned +here by `services/tooling/grok_npm.rs` plus the compact user-helper plan types. +[External Agent Lifecycle](./external-agent-lifecycle.md) +owns action legality, inventory normalization, opaque capabilities, jobs and +deployment orchestration. [Claude Code CLI](./claude-code-cli.md) owns +Claude-specific detection, owner/prefix admission and post-install +verification. Windows helper and Explorer-user execution boundaries are in +[Windows Agent Runtime Security](./windows-agent-runtime-security.md). + +## 2. Signatures + +```text +resolve_agent_source(agentId, surface, action) + -> verified release metadata | source_not_verified + +resolve_published_manifest(OfficialNpmTool::Grok | OfficialNpmTool::Claude) + -> exact root version + root/platform SHA-512 + +fetch_release_capability(agentId) + -> expectedReleaseId = "v1:" + 64 lowercase hex + +enumerate_desktop_candidates(agentId, frozenUserContext) + -> platform observations for inventory normalization + +windows_exe_install_admitted(agentId) + -> closed product identity is complete + +uninstall_display_name_matches(displayName, closedNames) + -> exact ASCII-case-insensitive name + | name + " " + bounded stable version +``` + +The renderer never supplies a URL, raw path, registry, package, hash, signer, +Uninstall key, command or executable name. Source metadata and scanned paths +remain backend evidence and are projected only through lifecycle capabilities. + +## 3. Contracts + +### Product source matrix + +| Product | Source and identity owner | +| --- | --- | +| Grok Build | CLI Tooling. Fresh install uses official `@xai-official/grok`; runtime resolves npm `latest` to one exact version plus root/current-platform integrity. Native `x.ai` install is explicit and updates preserve `native_internal` versus `official_npm`. | +| Claude Code | CLI only. This owner resolves official npm metadata and admitted mirrors; [Claude Code CLI](./claude-code-cli.md) owns discovery, owner-preserving update and verification. There is no public Claude Desktop source. | +| Codex | Dedicated Codex Desktop installer. Agent action returns `managed_by_codex_desktop`; this source owner does not duplicate it. | +| QoderWork CN | Reviewed first-party `/qoder-work-cn/releases/latest/` aliases plus same-host Electron-builder feed. Install/launch only; FyAgent update disabled. | +| TRAE Work CN | `data.solo` with `region=cn`; never TRAE Code or `data.manifest`. Comparable local version is bounded `tronBuildVersion`. | +| WorkBuddy | Closed `/v2/update` platform IDs and official download host. macOS rewrites only the validated terminal `.zip` suffix to `.dmg`. | +| OpenCode Desktop | Reviewed locale-neutral stable Desktop aliases and closed installed identity. Windows x64 uses `windows-x64-nsis`; GitHub latest is display-only. No public OpenCode CLI installer. | + +### Official npm source boundary + +- The signed product resolves a concrete version before composing npm argv. + `@latest` and the command-shape `@xai-official/grok@1.2.3` fixture are never + install authority. +- Root and current Darwin/Windows x64/arm64 optional-package SHA-512 values must + match one allowed registry. Unsupported architecture produces no plan; Linux + package support is not inferred from host-compilation tests. +- Version authority is npmjs `/latest` first, then the reviewed mainland + metadata chain. Installation may use only a registry whose exact root and + platform metadata match the resolved manifest. +- Formal Windows receives only the compact exact-version, registry and + allow-scripts control through the ordinary-user helper. Development Windows + composes the same live plan in-process; npm 12+ receives only + `--allow-scripts=@xai-official/grok`, and success requires PATH-default + version readback. See the Windows Agent runtime contract for execution. +- macOS Claude/Grok discovery uses login-shell PATH, process PATH and product + env. It does not walk mise/nvm/fnm/Volta internals. When several copies are + visible, the PATH-default installation is the selected owner. +- CLI `ToolVersion.latest_version` remains live. Claude/Grok/Codex/Gemini/ + OpenClaw use npm metadata, Hermes uses PyPI, and OpenCode may use GitHub as a + display fallback. Desktop products keep their vendor feeds. No reviewed CLI + version/hash JSON is compiled into the product. + +### Desktop feeds and release capabilities + +- Qoder display version is the unindented top-level `version:` from bounded + same-host `latest.yml` / `latest-mac.yml`. Feed ZIP and `sha512` are metadata, + not admitted artifacts. Windows ARM64 stays unsupported until separately + reviewed first-party evidence exists. +- TRAE source selection uses the Work/Solo CN object and closed host/path/ + filename rules. Electron marketing `appVersion` is not the comparable + runtime version. +- A shorter WorkBuddy dotted marketing version may equal a longer remote + product-version prefix. Same-length differing segments remain an update. +- OpenCode GitHub latest failure must not hide a stable admitted source. + Windows ARM64 remains unsupported. +- Every request is HTTPS, without userinfo or explicit non-default port. Each + redirect hop must match the product allowlist and bounded hop count; scheme + downgrade or an unknown host fails closed. +- Metadata is bounded to 1 MiB and artifacts to 2 GiB under the current + transport. Cancellation maps to `cancelled`, not source/schema failure. +- `expectedReleaseId` binds the canonical backend-resolved release fields. + A forced refresh must match before download. The capability never contains + or exposes a URL. +- Missing/drifted schema, host, redirect or capability returns + `source_not_verified` plus official-page guidance. Never pin an investigation + URL or infer version from ETag, Last-Modified or prose. + +### Closed desktop identity + +Folder names and vendor configuration directories are not identity. + +| Product | macOS bundle ID | Windows closed identity summary | +| --- | --- | --- | +| WorkBuddy | `com.tencent.workbuddy.mac` | Closed relative `WorkBuddy.exe`, ProductName and reviewed signer. | +| QoderWork CN | `com.qoder.work.cn` | Closed QoderWork CN relative EXE names, ProductName and signer. | +| TRAE Work CN | `cn.trae.solo.app` | Closed TRAE SOLO/Work CN relative EXE names, ProductName and signer. | +| OpenCode | `ai.opencode.desktop` | `@opencode-aidesktop/OpenCode.exe` and installer-stub `OpenCode/OpenCode.exe`, ProductName `OpenCode`, reviewed signer `Anomaly Innovations, Inc https://anoma.ly/`, and exact/bounded-version Uninstall DisplayName. | +| Claude legacy identity | `com.anthropic.claudefordesktop` | Observation only; not an admitted Agent lifecycle target. Current Claude support is CLI-only. | + +- Windows installed-target identity is distinct from the downloaded installer + leaf. A trusted current-user OpenCode install may be AMD64 at + `%LOCALAPPDATA%\\Programs\\@opencode-aidesktop\\OpenCode.exe` while the + official NSIS stub is i386 at `OpenCode/OpenCode.exe`; preserve both closed + relatives. +- KnownPath `Missing` is dropped. Uninstall/App Paths entries remain hints and + are inspected to the same closed PE identity before becoming candidates. +- Uninstall `DisplayName` accepts a closed name exactly or `{name} + {bounded_stable_version}`. Reject channel words and prerelease suffixes. + Empty `InstallLocation` is allowed; `DisplayIcon` and derived directories are + evidence, never commands. +- OpenCode fresh Windows destination is `WindowsCurrentUser`. Its display label + is not a scanned relative and must not generate one. +- macOS scans direct-child `.app` bundles in user/system Applications roots, + rejects symlinks and verifies plist/bundle identity. Absence on a shipped + host is `not_installed`; Linux development remains `unknown`. +- Windows uses the frozen Explorer user and explicit registry views. Inventory + parents require query+enumerate; validated children are query-only. Optional + absent/rejected shared-view links are absence, while access/enumeration/bound + or Shell-context failure makes the aggregate unknown. +- A Windows candidate is actionable only after stable no-reparse file identity, + supported architecture, closed ProductName, WinVerifyTrust, exactly one + signer and the reviewed signer leaf. +- Do not infer installation from `.workbuddy`, `.qoderwork*`, `.trae*` or any + settings directory. +- WorkBuddy macOS identity is `com.tencent.workbuddy.mac` across scan, + system-commit policy and privileged helper. `com.workbuddy.workbuddy` is + stale, and `com.tencent.codebuddycn` is another product. + +### OpenCode Windows readback after vendor or manual NSIS + +The same inventory scan must find an install launched by FyAgent or completed +manually. Helper product admission authorizes download/handoff only; it does +not prove scan relatives or installation completion. + +`installState=installed` and `launch` require a trusted PE at a closed relative +or a matching Uninstall/App Paths hint that resolves to that identity. ARM64 +stays `platform_unsupported`. Successful `ShellExecute` is only vendor-wizard +handoff; status changes after a later complete scan. + +## 4. Validation & Error Matrix + +| Condition | Required result | +| --- | --- | +| Renderer supplies URL/path/registry/package/hash/signer/command | Reject before source or filesystem work. | +| npm `/latest` document yields tag text instead of concrete semver | Reject; no install plan. | +| Root or platform integrity differs at a candidate registry | Skip registry; source exhaustion if none match. | +| Development Windows uses `1.2.3` fixture or `@latest` | Contract regression; resolve exact live version. | +| npm 12+ omits narrow Grok allow-scripts or postinstall is blocked | Failure even when npm exits zero. | +| PATH-default CLI remains below planned version | Fail the attempt; do not report Agent success. | +| Desktop feed host/schema/redirect/port/body grammar drifts | `source_not_verified`; no stale pin. | +| Release capability changes during forced refresh | Reject before download/commit. | +| Closed desktop identity is incomplete | No install/launch claim; catalog cannot claim local recognition. | +| OpenCode `@opencode-aidesktop\\OpenCode.exe` passes closed identity | Candidate may become `installed` + `launch`. | +| DisplayName is `OpenCode 1.18.27` and points to the trusted EXE | Keep hint and inspect it. | +| DisplayName is `OpenCode Dev`, `OpenCodeAI` or prerelease | Skip. | +| KnownPath `OpenCode\\OpenCode.exe` is missing | Drop the observation; do not fail the aggregate. | +| Optional Windows parent is absent/rejected shared-view link | Absence; continue. | +| Windows parent cannot enumerate or frozen-user context is invalid | Aggregate `unknown`; no fresh destination. | +| Vendor wizard opens successfully | Handoff success only; no installed claim. | + +## 5. Good / Base / Bad Cases + +- **Good:** resolve one exact Grok npm manifest, verify root/platform integrity + at an allowed registry, execute through the correct user boundary, then read + back the PATH-default version. +- **Good:** a user runs the official OpenCode NSIS independently; the next + complete scan finds `@opencode-aidesktop\\OpenCode.exe`, verifies PE identity + and exposes Launch. +- **Base:** a vendor wizard opened but the later scan has not completed; the job + records handoff while inventory remains unchanged. +- **Base:** a shipped-host source is unavailable; the UI receives official-page + guidance without a cached URL or fabricated release. +- **Bad:** install `@latest`, compile reviewed CLI version JSON, trust a folder + name/config directory, require exact `DisplayName == "OpenCode"`, or treat a + destination label as a known path. + +## 6. Tests Required + +- Npm tests parse a concrete `/latest` version, reject `version=latest`, prove + no version/hash JSON is included, match root plus current-platform integrity, + and keep argv free of `@latest` and fixture `1.2.3` authority. +- PATH discovery tests prefer the PATH-default installation among several and + do not walk manager internals. +- Windows LocalProcess tests add the Grok allow-scripts flag only for npm 12+, + preserve an existing flag, reject blocked-install-script output and require + final PATH-default version readback. +- Source parser tests enforce exact host, platform, schema, redirects, body + bounds and release capability refresh for Qoder, TRAE, WorkBuddy and OpenCode. +- Desktop identity tests freeze bundle IDs, relative EXEs, ProductName, signer, + architecture and no-reparse file identity. +- OpenCode tests keep both installed-target and stub relatives, accept exact or + bounded stable-version DisplayName, reject channel/prerelease lookalikes, + drop missing KnownPath and expose `WindowsCurrentUser` only after complete + absence. +- Matching-host Windows/macOS tests remain required for native trust behavior; + portable fixtures do not prove WinVerifyTrust, registry views, signer or + package execution. + +## 7. Wrong vs Correct + +### Wrong + +```text +npm i -g @xai-official/grok@latest +npm exit 0 -> installed +``` + +```rust +windows_relative_exes: &["OpenCode/OpenCode.exe"]; +if display_name != "OpenCode" { continue; } +``` + +### Correct + +```text +resolve live exact manifest + -> match root/current-platform integrity + -> closed user execution + -> PATH-default version readback +``` + +```rust +windows_relative_exes: &[ + "@opencode-aidesktop/OpenCode.exe", + "OpenCode/OpenCode.exe", +]; +uninstall_display_name_matches(display_name, &["OpenCode"]) +// exact name, or `OpenCode` + space + bounded stable version +``` diff --git a/.trellis/spec/backend/github-ci-workflow.md b/.trellis/spec/backend/github-ci-workflow.md index ffa528dcc..5f280b013 100644 --- a/.trellis/spec/backend/github-ci-workflow.md +++ b/.trellis/spec/backend/github-ci-workflow.md @@ -575,88 +575,9 @@ key: Cargo.lock + runner OS/arch never src-tauri/target, never RUSTC_WRAPPER / sccache ``` -## Scenario: Push before SHA missing after history rewrite - -### 1. Scope / Trigger - -- Trigger: lightweight branch-push commit policy still needs a comparison range. - An abnormal history rewrite or force-update can leave `github.event.before` - pointing to a commit that `actions/checkout` `fetch-depth: 0` does not clone - once no ref points at it. This is defensive commit-policy behavior, not a - branch synchronization contract; branch maintenance is outside Required CI. -- Owner: `.github/workflows/commit-convention-push.yml` before - `scripts/ci/verify-commit-messages.mjs`. -- The same verifier checks PR/merge-group ranges. It distinguishes a real - integration object from an ordinary commit that merely claims to be a merge; - this avoids rewriting shared history to normalize an integration title. - -### 2. Signatures - -- Workflow resolves `base_sha` / `head_sha`, then - `node scripts/ci/verify-commit-messages.mjs --base --head `. -- `listCommitSubjectsInRange` returns `{sha, parents: string[], subject}` from - Git `%H`, `%P`, `%s` for the full range or the current HEAD-only comparison. - -### 3. Contracts - -- `push` event `before` that is forty zeroes -> `base_sha = head_sha`. -- `push` event `before` that is not `${base_sha}^{commit}` in the clone -> - `base_sha = head_sha` (empty comparison). -- this fallback never invokes the domain classifier or `CI / Required`. -- Normal commit types and PR-title validation remain unchanged. An explicit - `merge: ` integration subject is accepted only on a - Git object with at least two distinct parents. It is not a general allowed - type, a subject-only exemption or an allowlist of historical hashes. -- All side-branch commits remain enumerated and validated; no first-parent or - no-merges filter hides bad subjects. Existing generated merge/revert subject - rules remain separate. New merges may simply use a conventional `chore:` title. - -### 4. Validation & Error Matrix - -- Forty-zero or unreachable push `before` -> empty comparison and current-head - commit subject validation only. -- Missing PR/merge-group SHA remains a Required classifier failure in - `.github/workflows/ci.yml`. -- Real multi-parent object with the explicit integration subject -> accept; - the same subject on a single-parent commit or a PR title -> reject. -- An empty/nonstandard merge subject or an invalid side-branch commit -> reject; - a valid integration header never exempts the merged work itself. - -### 5. Good / Base / Bad Cases - -- Good: ordinary push; `before` is an ancestor still fetched by complete - history; only the pushed commit range is checked for Conventional Commits. -- Base: force-update drops the previous tip; workflow logs that `before` is - not a commit in the clone and validates `head` against `head`. -- Bad: use branch push as a second Required CI authority or start product-domain - jobs merely to enforce commit-message policy. -- Bad: add `merge` to all normal commit types or disable convention checks to - admit an existing integration; use verified topology for the narrow case. - -### 6. Tests Required - -- `tests/githubWorkflowTriggers.test.ts` asserts the push-only `git cat-file -e` - fallback, queue-ref exclusion, and absence of `CI / Required` in the push - workflow. -- Local tests do not clone GitHub's unreachable `before` objects. -- `tests/verifyCommitMessages.test.ts` creates real temporary Git histories to - check genuine merge parents, HEAD-only merge checks, single-parent impostors, - PR titles, empty/nonstandard merge subjects and invalid side-branch commits. - -### 7. Wrong vs Correct - -#### Wrong - -```bash -node scripts/ci/verify-commit-messages.mjs --base "$PUSH_BASE_SHA" --head "$head_sha" -# git cat-file: before SHA does not identify a commit object -``` - -#### Correct +## Branch-push commit-policy routing -```bash -if ! git cat-file -e "${base_sha}^{commit}" 2>/dev/null; then - base_sha="$head_sha" -fi -node scripts/ci/verify-commit-messages.mjs --base "$base_sha" --head "$head_sha" -``` +[GitHub Branch-Push Commit Policy](./github-push-commit-policy.md) owns the +unreachable-`before` fallback, topology-aware integration subjects and the +lightweight `Commit Convention / Push` workflow. This Required CI owner remains +strict for PR/merge-group identities and never adopts the push fallback. diff --git a/.trellis/spec/backend/github-push-commit-policy.md b/.trellis/spec/backend/github-push-commit-policy.md new file mode 100644 index 000000000..23e5fad87 --- /dev/null +++ b/.trellis/spec/backend/github-push-commit-policy.md @@ -0,0 +1,133 @@ +# GitHub Branch-Push Commit Policy + +## 1. Scope / Trigger + +Read this contract before changing `.github/workflows/commit-convention-push.yml` +or the push-specific range passed to +`scripts/ci/verify-commit-messages.mjs`. + +This is a lightweight branch-push commit-message policy. It is not a second +product CI authority, branch synchronization workflow, or merge-readiness +signal. [GitHub CI Workflow](./github-ci-workflow.md) owns PR/merge-group +classification and the stable `CI / Required` result; [GitHub Merge +Governance](./github-merge-governance.md) owns admission to `main`. + +An abnormal history rewrite can leave `github.event.before` unreachable even +after checkout with full history because no ref points at the former tip. This +owner defines the defensive empty-comparison behavior without widening push +automation. + +## 2. Signatures + +```text +push workflow: + resolve base_sha / head_sha + -> node scripts/ci/verify-commit-messages.mjs + --base <40-hex commit> --head <40-hex commit> + +listCommitSubjectsInRange(base, head) + -> [{ sha, parents: string[], subject }] + # Git format: %H, %P, %s +``` + +Push trigger excludes `gh-readonly-queue/**` and emits only: + +```text +Commit Convention / Push +``` + +It never emits `CI / Required` or invokes the domain classifier. + +## 3. Contracts + +- If push `before` is forty zeroes, set `base_sha=head_sha`. +- If `${base_sha}^{commit}` does not exist in the checked-out clone, set + `base_sha=head_sha`. Log the defensive fallback without treating the + unreachable object as a product-CI failure. +- The fallback validates the current head subject through an empty comparison; + it does not fetch arbitrary history, run product domains, or repair/sync the + branch. +- PR and merge-group missing SHAs remain fail-closed in Required CI. This push + fallback must not leak into `.github/workflows/ci.yml`. +- Normal Conventional Commit types and PR-title rules remain unchanged. +- The explicit `merge: ` integration form is accepted + only when the corresponding Git object has at least two distinct parents. + It is not a general normal type, subject-only exemption, PR-title allowance, + or hash allowlist. +- The verifier enumerates the complete side-branch commit range. A first-parent + or no-merges filter must not hide invalid merged commits. +- Generated GitHub merge subjects and revert subjects remain separate existing + rules. New integrations may use an ordinary valid conventional subject. + +## 4. Validation & Error Matrix + +| Condition | Required result | +| --- | --- | +| Push `before` is forty zeroes | Use head-to-head empty comparison; validate current head. | +| Push `before` is a 40-hex SHA but not a commit in clone | Log fallback, use head-to-head; no domain CI. | +| Normal reachable push range | Validate every commit in explicit base..head range. | +| PR/merge-group base/head SHA missing | Required CI classifier fails; no push fallback. | +| Multi-parent object has valid explicit integration subject | Accept topology-specific form. | +| Same subject is on single-parent commit or PR title | Reject. | +| Integration subject is empty/nonstandard | Reject. | +| Side-branch commit has invalid subject | Reject even when merge commit itself is valid. | +| Queue-ref push triggers this workflow | Contract regression; queue uses merge-group Required CI only. | +| Push workflow starts product domains or emits `CI / Required` | Contract regression. | + +## 5. Good / Base / Bad Cases + +- **Good:** ordinary branch push has a reachable base; only the pushed range is + checked for Conventional Commit subjects. +- **Base:** force-update removes the previous tip; workflow confirms `before` + is not a commit, compares head to itself and still validates current head. +- **Base:** a genuine two-parent historical integration uses the narrow + explicit integration form while every merged side commit remains checked. +- **Bad:** use branch push as a second Required CI authority, start product + jobs to enforce subject style, or fetch/mutate refs to manufacture a range. +- **Bad:** add `merge` to all normal types, accept it by subject alone, skip + side-branch history, or disable convention checks for one old integration. + +## 6. Tests Required + +- `tests/githubWorkflowTriggers.test.ts` asserts push-only + `git cat-file -e "${base_sha}^{commit}"`, head-to-head fallback, + queue-ref exclusion and the absence of `CI / Required`/domain jobs. +- `tests/verifyCommitMessages.test.ts` builds temporary Git histories for real + multi-parent integration objects, head-only comparison, single-parent + impostors, PR titles, empty/nonstandard subjects and invalid side commits. +- Reachable-range fixtures prove every commit is enumerated without + first-parent/no-merges suppression. +- Required CI tests independently prove PR/merge-group identity remains strict + and cannot use this fallback. +- Local tests do not claim to reproduce GitHub's unreachable object storage; + they prove the workflow command and verifier semantics separately. + +## 7. Wrong vs Correct + +### Wrong + +```bash +node scripts/ci/verify-commit-messages.mjs \ + --base "$PUSH_BASE_SHA" --head "$head_sha" +# fatal: before SHA does not identify a commit object +``` + +```text +push -> classify domains -> CI / Required +``` + +### Correct + +```bash +base_sha="$PUSH_BASE_SHA" +if ! git cat-file -e "${base_sha}^{commit}" 2>/dev/null; then + base_sha="$head_sha" +fi +node scripts/ci/verify-commit-messages.mjs \ + --base "$base_sha" --head "$head_sha" +``` + +```text +push -> Commit Convention / Push only +PR/merge_group -> explicit base/head -> CI / Required +``` diff --git a/.trellis/spec/backend/index.md b/.trellis/spec/backend/index.md index ba2211016..e457d3d74 100644 --- a/.trellis/spec/backend/index.md +++ b/.trellis/spec/backend/index.md @@ -34,7 +34,11 @@ secret handling, native source checks, and residual-risk reporting. | [Backend Reuse](./reuse.md) | Existing-owner, adopted-dependency, open-source, adapter, and bespoke implementation order. | | [Development Environment](./development-environment.md) | Toolchain authority, bootstrap, host support, locks, optional macOS Windows-MSVC diagnostics, and environment verification. | | [Optional Codex Development Hooks](./development-hooks.md) | Optional Codex hook files, timeout/failure behavior, and Trellis-version ownership. | -| [Repository Task Runner](./task-runner-contract.md) | Public `mise run` API, effects, parameter transport, host guards, mutation policy, and platform diagnostics. | +| [Repository Task Runner](./task-runner-contract.md) | Public `mise run` API, effects, parameter transport, composition, mutation policy, generated docs, and canonical checks. | +| [Native Host Task Execution](./native-task-runner.md) | Foreground process trees, Windows executable/MSVC child environment, and macOS signed development runner. | +| [Optional Windows-MSVC Cross Diagnostic](./windows-msvc-cross-diagnostic.md) | macOS advisory/strict preflight/default-no cross Clippy and its non-acceptance evidence boundary. | +| [Trellis Direct-Session Prearchive Gate](./trellis-prearchive-gate.md) | Exact active-task/session proof and private one-task exclusion before archive. | +| [Supported-Platform Governance](./supported-platform-governance.md) | Platform-sensitive source/raster identities, one-snapshot repository scans, and fail-closed review seals. | | [Repository Root and Tool Configuration](./repository-layout.md) | Root discovery exceptions, explicit config locations, cwd/alias invariants and placement verification. | | [Database Persistence](./database-persistence.md) | SQLite path, schema version, startup lifecycle, migrations, import/backup/restore, DAO placement, and transactional boundaries. | | [Automatic Cloud Sync Scheduling](./auto-sync.md) | Injected database hints, independent S3/WebDAV workers, bounded debounce, suppression, and upload lifecycle. | @@ -71,7 +75,8 @@ and reuse of Grok's npm mirrors and ordinary-user execution boundary. | [Codex Session Usage Sync](./codex-session-usage.md) | Codex JSONL usage import, typed deferred reasons, retry/fingerprint separation, and bounded logging. | | [WorkBuddy Configuration](./workbuddy-configuration.md) | Revisioned WorkBuddy model/config writes, overwrite capabilities, backup, and reread. | | [External Agent Catalog and Runtime](./external-agent-catalog-runtime.md) | Static Agent catalog, capability/evidence projection, runtime observation, trusted launch, and ACL. | -| [External Agent Lifecycle](./external-agent-lifecycle.md) | Readiness, inventory, opaque targets, install/update/launch jobs, source verification, and recovery. | +| [External Agent Lifecycle](./external-agent-lifecycle.md) | Readiness, inventory, opaque targets, install/update/launch jobs, deployment orchestration, and recovery. | +| [External Agent Product Sources](./external-agent-sources.md) | Product release discovery, exact npm admission, redirect/artifact bounds, and closed desktop identity. | | [External Agent Auth](./external-agent-auth.md) | Login/logout/provider observation, Auth sessions, desktop target binding, and handoff semantics. | | [QoderWork Hooks Configuration](./qoderwork-hooks.md) | QoderWork Hooks snapshot, revisioned writes, allowed hooks, backup, and reread. | | [External Agent Model Integration](./external-agent-models.md) | TRAE model preflight/observation and OpenCode model persistence. | @@ -84,11 +89,13 @@ and reuse of Grok's npm mirrors and ordinary-user execution boundary. | Contract | Owns | | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| [Windows Shell-user Runtime](./windows-runtime-security.md) | Explorer-user authority, per-user paths/HKU, registry masks, single-instance input, COM launch, and helper boundary. | +| [Windows Shell-user Runtime](./windows-runtime-security.md) | Explorer-user authority, per-user paths/HKU, hidden Tauri paths, single-instance input, and interactive HTTP COM launch. | +| [Windows Agent Runtime Security](./windows-agent-runtime-security.md) | Trusted Agent EXE launch, closed Claude/Grok helper routes, LocalProcess parity, and inventory registry rights. | | [Windows Installer](./windows-installer.md) | NSIS mechanics, bounded cleanup, signing evidence, uninstall ownership, and native diagnostics. | | [macOS Privileged System-Commit Helper](./macos-system-commit.md) | Blessed helper, C ABI, product/slot integers, `MacSystemCommitPort`, and production enablement gates. | | [macOS Styled DMG Layout](./macos-dmg-layout.md) | DMG contents, Finder metadata, retries, byte preservation, and layout verification. | -| [GitHub CI Workflow](./github-ci-workflow.md) | Change classification, domain jobs, required aggregation, runner/toolchain evidence, and failure semantics. | +| [GitHub CI Workflow](./github-ci-workflow.md) | PR/merge-group classification, domain jobs, required aggregation, runner/toolchain evidence, and failure semantics. | +| [GitHub Branch-Push Commit Policy](./github-push-commit-policy.md) | Lightweight push-range fallback, topology-aware commit subjects, and queue-ref exclusion. | | [GitHub Release Workflow](./github-release-workflow.md) | Release identity, native builds, signing/notarization, assets, attestation, draft recovery, and publication. | | [GitHub Merge Governance](./github-merge-governance.md) | Merge Queue, merge method, task/spec lifecycle, and merge-readiness governance. | | [CC Switch Upstream Synchronization](./upstream-sync.md) | Immutable upstream identity, ancestry-preserving merge, conflict precedence, and provenance handoff. | diff --git a/.trellis/spec/backend/native-task-runner.md b/.trellis/spec/backend/native-task-runner.md new file mode 100644 index 000000000..716e16f4f --- /dev/null +++ b/.trellis/spec/backend/native-task-runner.md @@ -0,0 +1,268 @@ +# Native Host Task Execution + +## 1. Scope / Trigger + +Read this contract before changing host-native command resolution, the Windows +MSVC environment loader used by local Tauri/Cargo tasks, or the macOS signed +development app runner. + +This owner is intentionally narrower than +[Repository Task Runner](./task-runner-contract.md): the task runner owns the +public `mise run` API, effects, validated argument transport and DAG; this file +owns the last matching-host process/environment boundary immediately before a +native command starts. Optional macOS-to-Windows compile diagnostics are owned +separately by +[Windows MSVC Cross Diagnostic](./windows-msvc-cross-diagnostic.md). + +## 2. Signatures + +```text +resolveTaskExecutable("pnpm", platform) + win32 -> reviewed pnpm.exe from mise lock/runtime + darwin/linux -> direct pnpm executable + +resolveWindowsMsvcEnvironment({ processArch, inheritedEnv }) + -> additive child environment for VS 2022/2026 native tools + +ownedCargoEnvironment(...) + + resolved MSVC/SDK variables + -> final Cargo/Tauri child env + +mise run dev # macOS + -> host-native wrapper + -> signed development app bundle runner + +scripts/tasks/macos-signed-dev.mjs + configure | machine-preflight [--keep-session] | restore-session | ... + +scripts/tasks/macos-signed-dev-cargo.mjs + +RAW_TASKS = dev | dev:renderer | test:unit:watch + +executeTauriTask({ operation: "dev", runForegroundCommand }) + -> runForeground(pnpm, ["exec", "tauri", "dev", ...]) + +signalExitCode(SIGINT | SIGTERM | SIGHUP | SIGQUIT | SIGKILL | other) + -> 130 | 143 | 129 | 131 | 137 | 1 + +killProcessTree(pid, platform) + win32 -> taskkill.exe /pid /t /f + darwin | linux -> kill process group -pid (TERM then KILL) + other -> throw Unsupported task host +``` + +No renderer/user input becomes an executable path, shell command, signing +identity, target triple, compiler wrapper or environment injection. + +## 3. Contracts + +### Windows executable and batch boundary + +- Local mise tasks resolve only the actually used `pnpm` command to + `pnpm.exe` on Windows. This matches the reviewed x64/arm64 executable assets + and SHA-256 values in `mise.lock`. +- The task runner does not synthesize `.cmd` names for pnpm, npm, npx or pnpx + and does not introduce `shell:true`, generic command strings, or batch-shim + quoting. Non-Windows hosts continue direct execution. +- GitHub Actions has a separate reviewed `pnpm.cmd` bridge in the CI toolchain + verifier. That hosted boundary is not reused by local mise tasks. + +### Foreground interactive process ownership + +- `interactive=true` if and only if `raw=true`. The raw task owns the console + and complete child process tree so Ctrl+C or terminal close cannot leave a + hidden Tauri/watch process. +- The closed interactive task set is `dev`, `dev:renderer`, and + `test:unit:watch`. Tauri `dev` uses `runForeground` with inherited stdio and + visible Windows console behavior; noninteractive build/check operations keep + the synchronous hidden runner. +- On the first `SIGINT` or `SIGTERM`, `runForeground` starts graceful tree + shutdown and an unref force-kill timer (default 3000ms). A repeated signal + immediately re-signals the tree and exits with the standard signal code. +- `signalExitCode` maps SIGINT/SIGTERM/SIGHUP/SIGQUIT/SIGKILL to + 130/143/129/131/137; unknown signals map to 1. The synchronous runner uses + the same SIGINT/SIGTERM mapping rather than throwing an unhandled signal. +- Darwin/Linux children start a detached process group and are signalled by + negative PID. Windows remains attached and uses + `taskkill.exe /T /F`. Host dispatch is `win32`, then the closed POSIX helper, + then throw; `platform !== "win32"` is prohibited. +- `scripts/tasks/platform.mjs` is the zero-dependency POSIX-host authority. + Ordinary task helpers may re-export it; bootstrap/CI code that runs before + dependency installation imports it directly. +- The macOS privileged-helper build script traps INT/TERM and exits promptly; + the outer task runner remains owner of the final signal exit code. +- The `taskkill` helper belongs only to development task process control. NSIS + installer scripts must not use it. + +### Windows MSVC environment loader + +- On native Windows only, the guarded wrapper resolves a bounded Visual Studio + 2022 or 2026 instance inside `[17.0,19.0)` with official `vswhere.exe` JSON. + Build Tools is valid when the native component exists. +- Required component is `Microsoft.VisualStudio.Component.VC.Tools.x86.x64` + on x64 or `Microsoft.VisualStudio.Component.VC.Tools.ARM64` on ARM64. +- Visual Studio's supported environment mechanism is the one narrow exception + to the local no-`cmd.exe` rule. Spawn `cmd.exe` directly—not `shell:true`—with + `[/d, /s, /c, ]` and + `windowsVerbatimArguments:true`. +- The closed command calls the validated `VsDevCmd.bat` with `-no_logo`, + architecture/host architecture derived from `process.arch`, then invokes the + current Node executable to serialize `process.env` as JSON. Do not parse + localized `set` output or hard-code x64 on ARM64. +- Validate `INCLUDE`, `LIB`, numeric `VCToolsVersion`, and a Visual Studio + environment major matching the selected 17.x/18.x instance. +- Merge the result only into the child environment. Never mutate + `process.env`, user/system environment or registry. +- The merge is additive and may not override owned Rust/Cargo controls, + including RUSTC/RUSTDOC, target, linker, runner, wrappers and flags already + established by `ownedCargoEnvironment`. +- Missing prerequisites fail with bounded guidance naming the Visual Studio + “Desktop development with C++” workload. The wrapper never elevates or + installs components. + +### macOS signed development runner + +- The canonical macOS `mise run dev` remains current-host-only, interactive + and raw. It keeps Tauri HMR while wrapping the emitted debug executable in a + real development app bundle before launch. +- Preflight uses full Xcode and user-local Developer ID PKCS#12 configuration + to create/reuse one mode-0700 cache keychain. It extracts the certificate and + private key into a temporary mode-0700 directory, imports the leaf, + traditional RSA key and pinned Apple Root/Developer ID G2 public + certificates, and smoke-signs a copy of `/usr/bin/true`. +- `machine-preflight --keep-session` keeps that keychain as the user default + through the detached Tauri process and nested signing. App-runner or + `restore-session` restores the original default/search list after success, + setup failure or process exit. Standalone preflight restores immediately; + restore is idempotent when no session exists. +- Never delete a keychain after it has signed with the identity. Delete only + temporary extracted PEM files after import. +- Build a development-flavor universal privileged helper/client, verify + embedded plists, compile Tauri with the privileged-client feature, assemble + the app bundle, embed client/helper, sign inside-out with the frozen reviewed + Developer ID identity, verify signature/link/rpath, then launch the bundle + executable. Development does not notarize or staple. +- The Cargo runner accepts only Tauri/Cargo's fixed protocol and rejects + forwarded application arguments. It owns/sanitizes `DEVELOPER_DIR`, + Cargo/rustc runner settings, privileged artifact variables, `DYLD_*`, + `RUSTFLAGS`, `NODE_OPTIONS` and related injection surfaces. +- Ctrl+C terminates the complete child process group. Linux and Windows retain + their matching-host native behavior; no macOS wrapper becomes a cross-host + acceptance path. +- The repository never stores the developer PKCS#12 path or password. + `macos-signed-dev.mjs configure` writes a mode-0600 local configuration under + FyAgent Application Support, referencing local PKCS#12 and credentials + files. `mise run dev` accepts no env/argv override for those secrets. + +## 4. Validation & Error Matrix + +| Condition | Required result | +| --- | --- | +| Windows local task resolves `pnpm.cmd` or uses `shell:true` | Reject; use reviewed `pnpm.exe` direct execution. | +| Interactive task lacks `raw=true` | Task validation fails. | +| Tauri dev uses the synchronous/hidden runner | Task-contract failure. | +| Windows tree kill uses POSIX negative PID | Reject; use `taskkill.exe /T /F`. | +| Darwin/Linux tree kill uses `taskkill.exe` | Reject; signal the detached group. | +| Repeated Ctrl+C during shutdown | Immediate tree termination and exit 130. | +| Unsupported host falls through a negated Windows branch | Supported-platform failure; throw explicitly. | +| NSIS script contains `taskkill` | Installer-contract failure. | +| Windows x64/ARM64 mise executable or lock digest is absent/drifted | Fail tool admission before task child. | +| `vswhere` finds no complete 17.x/18.x instance/native component | Bounded workload hint; no elevation/install. | +| MSVC loader hard-codes architecture or accepts unsupported `process.arch` | Reject before `VsDevCmd`. | +| Closed `cmd.exe` command/path is caller-controlled | Reject; no shell execution. | +| Parsed environment lacks INCLUDE/LIB/version match | Fail before Cargo/Tauri. | +| MSVC env overrides owned Rust/Cargo controls or mutates parent/system state | Contract regression; abort. | +| macOS signing configuration is missing/unsafe | Fail preflight before build/sign/launch. | +| Keychain session restores before detached/nested signing completes | Fail lifecycle; signing authority must remain available until owner cleanup. | +| App/helper/plist/signature/link/rpath verification fails | Do not launch the development app. | +| Cargo runner receives application args or injected target/wrapper/env | Reject before compile/launch. | +| Development flow claims notarization, Release trust or foreign-host acceptance | Evidence regression; keep those gates separate. | + +## 5. Good / Base / Bad Cases + +- **Good:** native Windows resolves the reviewed `pnpm.exe`, loads one complete + matching-architecture VS environment into the child only, then starts the + fixed current-host Tauri plan. +- **Good:** `mise run dev` on macOS/Linux/Windows owns the process group/tree; + first Ctrl+C begins graceful shutdown and a second exits immediately with + code 130. +- **Good:** macOS preflight keeps its cache-keychain session through detached + compilation and nested bundle signing, verifies the assembled app, restores + the prior keychain state and terminates the whole group on Ctrl+C. +- **Base:** Linux executes ordinary direct pnpm/current-host tasks and never + invokes Windows MSVC or macOS signing owners. +- **Base:** a macOS developer runs standalone preflight; it restores keychain + state immediately because no detached dev session follows. +- **Bad:** choose `pnpm.cmd`, set `shell:true`, parse localized `set`, merge VS + env into `process.env`, hard-code x64, store signing secrets in Git, or + launch an unsigned/unverified debug binary outside the app bundle. +- **Bad:** fix only Darwin process teardown, use `platform !== "win32"` as an + implicit POSIX branch, or let a Windows GUI child survive terminal close. + +## 6. Tests Required + +- Executable-resolution tests require `pnpm.exe` only on Win32, bind both + reviewed mise lock assets/digests, preserve direct non-Windows execution and + reject `.cmd`, shell and command-string fallbacks. +- Task metadata tests require exactly the three raw interactive tasks. + `executeTauriTask({operation:"dev"})` must call the foreground runner. +- Process tests cover every signal-code mapping, first/repeated interrupt, + child signal exits, Windows `taskkill.exe /pid /t /f`, Darwin/Linux `-pid` + signalling and unsupported-host throw. Supported-platform tests reject + implicit non-Windows branches; installer tests reject NSIS `taskkill`. +- The macOS helper-build source must trap INT/TERM while the outer runner owns + final status mapping. +- Windows MSVC tests cover VS 2022/2026 selection, x64/ARM64 component mapping, + exact closed `cmd.exe` argv, architecture derivation, JSON environment parse, + required variables/version matching, parent-env immutability and additive + merge protection for every owned Rust/Cargo control. +- Missing-tool fixtures prove bounded guidance and zero installer/elevation + children. +- macOS signed-dev tests cover configuration mode/path confinement, temporary + extraction cleanup, cache-keychain permissions, keep/restore lifecycle, + idempotent restore, smoke signing, helper/client/plist assembly, inside-out + signing, signature/link/rpath verification and no notarization. +- Cargo runner tests reject forwarded application args and every target, + wrapper, runner, linker, flag, DYLD/NODE/privileged-artifact injection. +- Foreground lifecycle tests prove Ctrl+C terminates the full POSIX group and + Windows process tree through the task-runner owner. +- Matching-host execution remains required. Portable unit tests do not prove + Visual Studio, Keychain, signing identity or native launch behavior. + +## 7. Wrong vs Correct + +### Wrong + +```js +spawn("pnpm.cmd", args, { shell: true }); +Object.assign(process.env, parsedVsEnvironment); +if (platform !== "win32") process.kill(-pid, "SIGKILL"); +``` + +```text +mise run dev -> launch target/debug/fyagent directly +repo -> developer-signing.p12 + password +``` + +### Correct + +```text +win32 -> reviewed pnpm.exe -> fixed argv + -> validated VS instance/component + -> closed cmd /d /s /c VsDevCmd + Node JSON env + -> additive child-only merge + -> fixed current-host Cargo/Tauri command + +interactive dev -> foreground child + -> win32 taskkill tree | reviewed POSIX process-group signals | throw + -> standard signal exit code +``` + +```text +local mode-0600 signing configuration + -> keep-session cache keychain + -> helper/client + Tauri bundle + -> inside-out sign and verify + -> launch bundle executable + -> restore original keychain/session state +``` diff --git a/.trellis/spec/backend/supported-platform-governance.md b/.trellis/spec/backend/supported-platform-governance.md new file mode 100644 index 000000000..f3f0e03ba --- /dev/null +++ b/.trellis/spec/backend/supported-platform-governance.md @@ -0,0 +1,164 @@ +# Supported-Platform Source and Asset Governance + +## 1. Scope / Trigger + +Read this contract before changing `supported-platform-check.mjs`, its +first-party source/raster identity inventories, the whole-repository scanner +integration test, or any platform guard that changes which tracked files are +considered platform-sensitive. + +This owner protects review identity and scanner completeness. It does not own +the implementation behavior inside each listed file, product platform support, +or native acceptance. The public task entry points are documented by +[Repository Task Runner](./task-runner-contract.md). The temporary active-task +exclusion used only before Trellis archive is documented by +[Trellis Direct-Session Prearchive Gate](./trellis-prearchive-gate.md). + +## 2. Signatures + +```text +mise run supported-platform:check + +node scripts/tasks/supported-platform-check.mjs + [--exclude-active-task ] + +scripts/tasks/supported-platform-structure-assets.json + -> reviewed platform-sensitive first-party source identities + +scripts/tasks/supported-platform-raster-assets.json + -> reviewed tracked raster identities + +identity entry = { + path: canonical repository-relative path, + mode: reviewed Git index mode, + sha256: lowercase file digest +} +``` + +The checker is dependency-free and runnable from a clean checkout with Node +built-ins only. CI Changes invokes it before dependency installation. + +## 3. Contracts + +### Complete source and asset inventories + +- The two inventories are fail-closed review authorities, not content + exclusions. Every listed file still passes normal path, text and structural + scanners. +- The source candidate set is recomputed bidirectionally from all tracked Cargo + manifests/build scripts and executable/configuration files containing + reviewed platform selectors. Candidate path, Git index mode, regular + non-symlink file type and SHA-256 must match exactly. +- Platform-sensitive source is normally `100644`. The Tauri Cargo runner + `scripts/tasks/macos-signed-dev-cargo.mjs` is deliberately `100755` because + Tauri executes its `--runner` directly; changing that mode breaks runtime and + is not a hardening improvement. +- Adding, removing, renaming, moving, editing or changing the mode of a + candidate fails until the final bytes and platform dispatch are reviewed and + the inventory is deliberately updated. A digest-only refresh is not review. +- Recompute after formatting or any later source edit. A manifest made green + before final formatting is stale evidence. +- Canonical manifest order is `path.localeCompare(other, "en")`, not raw byte + order. Bulk-refreshing unreviewed entries or disabling the checker to unblock + CI is prohibited. +- A platform guard added to a previously neutral module creates a new + candidate. Add and review it rather than introducing an exclusion. + +### Whole-repository snapshot validation + +- The integration test captures the tracked Git file list once, then runs + every scanner against that same immutable snapshot. No scanner may silently + enumerate a different repository state during the same assertion set. +- The repository currently contains more than 1,000 tracked files. The + integration test has a bounded 15-second test watchdog for enumeration and + parsing scheduling; that watchdog is not a product latency or scanner + performance budget. Other tests retain their normal timeout. +- Zero findings and inspected-file-count assertions are mandatory. Negative + fixtures for each inventory/scanner remain required. +- Do not change the production scanner, suppress findings, or loosen a product + performance budget to accommodate the integration runner's scheduling. +- When repairing a source-text guard, assert the complete owning platform block + and add negative fixtures for moving or widening the protected operation. + Attribute/call adjacency alone is not an authority boundary. + +### Runtime and CI boundary + +- The checker imports only Node built-ins and dependency-free repository + bootstrap helpers. The always-running CI Changes job executes it immediately + after checkout and Node setup, before package installation. +- GitHub-hosted Linux execution proves the repository scanner and inventories, + not a shipped Linux product surface or native Windows/macOS behavior. +- Default local/CI checks never infer an active-task exclusion. Only the + separately validated prearchive wrapper may provide the private path, and the + post-archive canonical run removes it. + +## 4. Validation & Error Matrix + +| Condition | Required result | +| --- | --- | +| Candidate set differs from manifest in either direction | Fail with added/removed identities. | +| Tracked mode, regular-file type or SHA-256 differs | Fail; require review and explicit inventory update. | +| Candidate or inventory entry is a symlink/escape | Reject. | +| Manifest order differs from English locale ordering | Fail canonicalization. | +| New platform guard is hidden by an exclusion | Contract regression; admit and review the candidate. | +| Integration scanners enumerate separate live file lists | Test failure; use one captured Git snapshot. | +| Whole-repository scan exceeds the bounded integration watchdog | Diagnose scheduling/fixture cost; do not weaken production scanner or product budget. | +| Zero-findings or inspected-count assertion is removed | Contract regression. | +| Checker imports an installed package before CI dependency setup | Changes job failure; restore dependency-free boundary. | +| Canonical check receives an inferred/private task exclusion | Reject; only validated prearchive may exclude one exact task. | +| Portable scan is cited as native runtime/install/signing evidence | Keep matching-host evidence pending. | + +## 5. Good / Base / Bad Cases + +- **Good:** a reviewed platform branch is added, the complete source bytes are + inspected, the new candidate is inserted in canonical order with final mode + and digest, and every scanner passes against one Git snapshot. +- **Good:** a source-text assertion is widened to the entire authority block + and negative fixtures prove the protected call cannot move outside it. +- **Base:** an ordinary docs change still runs the dependency-free checker in + the CI Changes job and finds the same complete repository identities. +- **Base:** the whole-repository integration test needs up to its dedicated + watchdog under host load while individual scanner/unit budgets stay intact. +- **Bad:** refresh every digest without reviewing the diff, sort with raw bytes, + mark the executable Tauri runner `100644`, or skip an added platform file. +- **Bad:** raise application performance thresholds or suppress findings + because a repository integration test enumerates many files. + +## 6. Tests Required + +- Bidirectional fixtures cover added, removed, renamed, moved and newly + platform-sensitive source, plus mode/digest/file-type/symlink drift. +- Raster fixtures cover tracked identity, content and mode drift. +- Ordering tests require `localeCompare(..., "en")` output and reject raw-byte + alternatives. +- Whole-repository validation captures one Git file list and asserts every + scanner's inspected count plus zero findings within the dedicated watchdog. +- Negative source-text fixtures move or widen protected operations and must + fail even when an attribute/call token remains adjacent. +- Clean-checkout tests prove the checker uses only Node built-ins and can run + before dependency installation. +- `tests/remainingPlatformSurface.test.ts`, the dedicated checker tests, and + `mise run supported-platform:check` remain green. +- Prearchive tests prove exactly one validated active-task path may be excluded; + canonical local/CI/post-archive tests prove no exclusion is present. + +## 7. Wrong vs Correct + +### Wrong + +```text +source changed -> bulk regenerate every SHA -> commit +new `cfg(target_os)` file -> add ignore pattern +integration timeout -> remove inspected-file assertion +``` + +### Correct + +```text +review final source bytes and dispatch + -> recompute exact candidate set + -> verify mode + regular file + SHA-256 + -> insert by localeCompare(path, "en") + -> run all scanners against one captured Git snapshot + -> require zero findings and exact inspected counts +``` diff --git a/.trellis/spec/backend/task-runner-contract.md b/.trellis/spec/backend/task-runner-contract.md index 5641c128f..22699120c 100644 --- a/.trellis/spec/backend/task-runner-contract.md +++ b/.trellis/spec/backend/task-runner-contract.md @@ -105,162 +105,22 @@ gate runs only on its matching GitHub Actions runner. Repository tasks never install or activate a non-host Rust target as part of the standard local execution path. -There is one explicit non-acceptance exception for early diagnostics on macOS: -`system:check:windows-msvc-cross`, -`system:check:windows-msvc-cross:advisory`, and -`rust:clippy:windows-msvc-cross`. The strict preflight is read-only and reports -the complete bounded prerequisite set. The advisory task is also read-only and -is the only one allowed in `bootstrap`: missing tools print `ADVISORY` and -exit 0, so onboarding never fails. The Clippy task is -`FYAGENT_TASK_EFFECT=dependency-environment`, requires a default-no -confirmation because cargo-xwin may download/cache Microsoft CRT/SDK content, -and runs only after the same preflight passes. All three tasks fix cargo-xwin -to the reviewed version, target only `x86_64-pc-windows-msvc`, use the clang-cl -backend and reviewed xwin toolset, accept no forwarded argument, reject caller -Rust/C/CMake/xwin controls and effective Cargo target/toolchain config, and -invoke a fixed workspace/all-targets/locked Clippy argv with `-D warnings`. -They never install the Rust target, LLVM, CMake, Ninja, a system package, or -accept a license on the developer's behalf. Strict preflight and Clippy are -never reachable from `bootstrap`, `check`, `check:backend`, CI release gates, -or a standard dev/build/test alias. Advisory is bootstrap-only and is also -absent from `check`. The result is cross-compilation diagnostics only; native -Windows CI/HIL remains the authority for registry, PackageManager, WebView2, -installer, UAC, launch, runtime, signing, packaging, and release behavior. -The executable signatures, JSON report, frozen argv, and override matrix for -this exception live in **Scenario: Optional macOS Windows-MSVC Clippy -diagnostic** below. +The sole non-acceptance exception for optional macOS-to-Windows compile +diagnostics is owned by +[Optional macOS Windows-MSVC Compile Diagnostic](./windows-msvc-cross-diagnostic.md). +Its advisory may appear in `bootstrap`; strict preflight and default-no Clippy +remain outside every canonical check/CI/Release gate and never substitute for +matching-host Windows evidence. Linux x64/arm64 is a development host for `check` and other current-host tasks; it is not a shipped product platform and does not add a local cross-compile or Actions job. ### Foreground interactive process ownership -#### 1. Scope / Trigger - -`mise run dev` (and other `FYAGENT_TASK_EFFECT=interactive` tasks) must own -Ctrl+C and terminal close on every development host. This is a cross-host -process-control contract: macOS/Linux and Windows must be specified -together, not inferred as “not Windows means POSIX”. - -#### 2. Signatures - -```text -RAW_TASKS = dev | dev:renderer | test:unit:watch - -executeTauriTask({ operation: "dev", runForegroundCommand }) - -> runForeground(pnpm, ["exec", "tauri", "dev", ...]) - -signalExitCode(signal) - SIGINT -> 130 - SIGTERM -> 143 - SIGHUP -> 129 - SIGQUIT -> 131 - SIGKILL -> 137 - other -> 1 - -killProcessTree(pid, platform) - win32 -> spawnSync("taskkill.exe", ["/pid", pid, "/t", "/f"]) - darwin | linux -> process.kill(-pid, SIGTERM then SIGKILL) - other -> throw Unsupported task host -``` - -#### 3. Contracts - -- `interactive = true` if and only if `raw = true`. `raw` makes the console - the process-group leader so Ctrl+C and closing the terminal reach the - child. -- `dev` uses `runForeground` (`spawn`, `stdio: "inherit"`, - `windowsHide: false`). Other Tauri operations keep `run` / - `spawnSync` / `windowsHide: true`. -- On first interrupt signal (`SIGINT` / `SIGTERM`), `runForeground` initiates - graceful shutdown via `killProcessTree` and starts an unref fallback force-kill - timer (default 3000ms). -- Repeated interrupt signals (such as a second `Ctrl+C`) during shutdown - immediately re-signal the tree and force-exit the process with standard exit - code (130 for `SIGINT`). -- Normal signal exits assign standard exit codes (`signalExitCode`, e.g. 130 - for `SIGINT`, 143 for `SIGTERM`) instead of generic failure exit code 1. -- Synchronous task runner `run()` terminates with `signalExitCode` on `SIGINT` - (130) and `SIGTERM` (143) instead of throwing unhandled signal errors. -- The macOS development helper build script - (`build-macos-privileged-helper.sh`) traps `INT` and `TERM` and exits - promptly, preventing orphaned build children during `mise run dev` - preflight. The outer task runner still owns the user-visible standard signal - exit code. -- POSIX hosts (`darwin`, `linux`) start a new process group with - `detached: true` and kill `-pid`. Windows stays attached - (`detached: false`) and - uses `taskkill.exe /T /F`. That helper belongs to the development task - runner only. -- NSIS installers must not use `taskkill`. JavaScript host branches must - be `win32` then `isPosixTaskHost` then throw; `platform !== "win32"` is - forbidden. -- `scripts/tasks/platform.mjs` is the zero-dependency owner of the closed - POSIX-host predicate. `scripts/tasks/lib.mjs` re-exports it for ordinary task - callers, while bootstrap/CI helpers that run before dependency installation - import the owner directly. - -#### 4. Validation & Error Matrix - -| Condition | Required result | -| ---------------------------------------------------- | ---------------------------------------------- | -| `FYAGENT_TASK_EFFECT=interactive` without `raw=true` | `tasks:validate` fails | -| `dev` uses `run` / `spawnSync` | `miseTaskContract` fails | -| Windows tree kill uses POSIX `kill(-pid)` | Child GUI survives; contract test fails | -| POSIX group kill uses `taskkill.exe` | Contract test fails | -| Repeated Ctrl+C during dev task shutdown | Immediate termination with exit code 130 | -| Child process terminated by SIGINT | Process exitCode set to 130 | -| `platform !== "win32"` fallback | `supported-platform:check` fails | -| NSIS script contains `taskkill` | Windows installer contract fails | -| Unsupported `process.platform` | Throw `Unsupported task host`; no silent POSIX | - -#### 5. Good/Base/Bad Cases - -- Good: `mise run dev` on macOS or Windows; Ctrl+C or closing the terminal - stops Tauri and its children. A second Ctrl+C immediately force-quits with code 130. -- Base: `mise run build` still uses the hidden `run()` helper. -- Bad: only fix Darwin because the author is on a Mac; Windows keeps - `spawnSync` / `windowsHide: true`. - -#### 6. Tests Required - -- `RAW_TASKS` equals the three interactive tasks above; every interactive task has - `raw=true`. -- `executeTauriTask({ operation: "dev" })` calls `runForegroundCommand`, - not `run`. -- `signalExitCode` maps signals to standard exit codes (130 for SIGINT, 143 for SIGTERM). -- `runForeground` force-kills and exits with 130 on repeated SIGINT. -- Child exit on signal yields standard signal exit code. -- the macOS helper source traps INT/TERM, while the outer runner remains the - owner of final `signalExitCode` mapping; -- `killProcessTree` on `win32` records `taskkill.exe /pid /t /f`; on - `darwin` and `linux` signals `-pid`; on any other host throws. -- `supported-platform:check` rejects implicit non-Windows branches in - `scripts/tasks/lib.mjs`. - -#### 7. Wrong vs Correct - -#### Wrong - -```js -if (platform !== "win32") process.kill(-pid, "SIGKILL"); -``` - -#### Correct - -```js -if (platform === "win32") { - runner("taskkill.exe", ["/pid", String(pid), "/t", "/f"], { - windowsHide: true, - stdio: "ignore", - }); -} else if (isPosixTaskHost(platform)) { - posixKill(-pid, "SIGTERM"); - posixKill(-pid, "SIGKILL"); -} else { - throw new Error(`Unsupported task host: ${platform}`); -} -``` +[Native Host Task Execution](./native-task-runner.md) owns the closed raw-task +set, foreground child lifecycle, signal exit codes and Windows/POSIX process +tree termination. This task-runner contract only requires interactive effects +to delegate to that owner; it does not duplicate host dispatch. ## 4. Parameter Transport @@ -271,105 +131,19 @@ Arguments must never be concatenated into a command string. ### Prearchive active-task verification -**Scope / trigger.** This lifecycle bridge allows one directly active, -in-progress Trellis task to be excluded while its own tracked planning markers -are still present before archival. It is reusable across task names only -because identity is derived and re-proved on every invocation; it never means -"skip active tasks" generally. Ordinary local checks, CI, and post-archive -verification use canonical tasks without an exclusion. - -**Signatures.** `check:prearchive` and `check:contracts:prearchive` each require -`--exclude-active-task `. They delegate to -`scripts/tasks/prearchive-check.mjs`, which selects only `check` or -`check:contracts` and never forwards the usage argument to unrelated leaves. - -**Contracts.** The accepted path is exactly one repository-relative direct -child matching `.trellis/tasks/MM-DD-`. The checker resolves it below the -canonical tasks root, rejects traversal, backslashes, nesting, archive paths, -wildcards, symlinks, non-direct realpaths, a non-directory task, and a missing, -symlinked, or non-regular `task.json`. It derives `` from the path and -requires `task.json.id` and `task.json.name` to equal it, with -`status === "in_progress"`. - -The same canonical task path must be Trellis's current pointer with -`stale === false` and a direct `source: "session:"`; `session-fallback`, a -different task, or any stale pointer fails closed. Validation transports the -path through the private `FYAGENT_SUPPORTED_PLATFORM_ACTIVE_TASK` entry only -after proving that identity. The leaf accepts exactly one input channel: direct -CLI, mise usage, or private environment. Caller-preseeded, conflicting, or -duplicate channels fail. Default `check`, `check:contracts`, and -`supported-platform:check` never infer or apply an exclusion. - -**Error matrix.** Missing usage, unknown wrapper mode, malformed/noncanonical -path, path/realpath/file-type escape, metadata ID/name/status mismatch, -missing/stale/fallback/wrong session pointer, caller-preseeded internal state, -multiple input channels, or nested nonzero status stops the wrapper. No failure -may retry with a broader path or omit the platform check. - -**Good/base/bad cases.** Good: two differently named canonical fixture tasks -both validate when each is the direct current in-progress task. Base: canonical -`check` runs with no internal entry. Bad: accepting a hard-coded historical ID, -`session-fallback`, an archived/nested/symlinked task, or a second input source. - -**Tests required.** Pure and integration tests cover two valid task identities; -path/date/ID/traversal/backslash/archive/nesting failures; task-directory and -metadata-file symlinks; ID/name/status mismatch; stale, fallback, and mismatched -current pointers; and CLI/usage/private-environment duplication. Acceptance -records a real prearchive composite from the directly bound session and a -post-archive canonical run without an exclusion. The private environment entry -is lifecycle evidence and is never provided to CI. - -**Wrong vs correct.** Wrong: freeze a historical task constant, broadcast the -raw flag through every task, add a task glob, or teach canonical checks to skip -`.trellis/tasks/**`. Correct: derive one canonical path/ID, prove exact direct -session ownership and metadata, transport it privately to one leaf, archive, -then rerun canonical checks with no exclusion. +[Trellis Direct-Session Prearchive Gate](./trellis-prearchive-gate.md) owns the +exact task-path grammar, direct-session proof, private transport, failure matrix +and archive/post-archive evidence. This task-runner contract exposes only the +two public wrapper names and their required `--exclude-active-task ` +argument. Canonical checks never infer an exclusion. ### Supported-platform identity seals -The durable surface checker keeps two reviewed identity inventories in -`scripts/tasks/`: one for platform-sensitive first-party source and one for -tracked raster assets. These inventories are fail-closed review authorities, -not content exclusions. Every listed file still passes the normal path, text, -and structure scanners. - -The whole-repository integration test captures its Git file list once, then -runs every scanner against that same snapshot. It has a bounded 15-second test -watchdog because filesystem enumeration and parsing more than 1,000 files is -not a product latency benchmark; other tests keep their default timeout. -Its zero-findings and inspected-file-count assertions remain mandatory, as do -all negative inventory fixtures. Do not change the production scanner or a -performance budget to accommodate integration-runner scheduling. - -The source inventory is recomputed bidirectionally from all tracked Cargo -manifests and build scripts plus executable/configuration files containing -platform selectors. The candidate set, canonical paths, reviewed Git index -mode, regular non-symlink file type, and SHA-256 digest must match exactly. -Platform-sensitive source is `100644` except the single Tauri Cargo runner -`scripts/tasks/macos-signed-dev-cargo.mjs`, which is deliberately `100755` -because Tauri invokes `--runner` directly as ` run ...`; making that -file non-executable is a runtime failure, not a hardening improvement. -Adding, removing, renaming, moving, changing, or changing the mode of a -candidate fails until the source diff is reviewed and the identity inventory -is deliberately updated. A digest-only update is not evidence that a platform -dispatch remains safe. -Review the final source bytes before updating individual digests, including -changes that do not add a new platform branch. Recompute after formatting and -rerun `supported-platform:check`; a previously green manifest does not cover -later source edits. Do not bulk-refresh unreviewed entries or disable the seal -to unblock the always-running CI Changes job. - -The manifest's canonical ordering is `path.localeCompare(other, "en")`, not -raw byte sorting. Adding a platform guard to a previously platform-neutral -module also adds a candidate: review that file and add its identity rather -than excluding it. When repairing a stale source-text test, assert the complete -owning platform block and add negative cases for moving/widening the protected -operation; attribute/call adjacency alone is not an authority boundary. - -The checker and both inventories must remain runnable from a clean checkout -using only Node built-ins. The always-running CI Changes job invokes this path -before dependency installation, so importing a package or a helper with a -package dependency is a contract violation. +[Supported-Platform Source and Asset Governance](./supported-platform-governance.md) +owns candidate discovery, source/raster identity inventories, mode/digest +review, canonical ordering, the one-snapshot whole-repository test and its +dedicated watchdog. `supported-platform:check` stays a read-only public task; +the checker remains dependency-free and available before CI installs packages. `format:files` accepts one or more reviewed files and first validates every operand. It routes validated `.jsonl` names @@ -394,39 +168,12 @@ formatting is syntactic record normalization only. A consumer-specific JSONL schema, if one exists, must be validated by that consumer's executable tests or tooling. -On native Windows, local mise tasks resolve only the actually used `pnpm` -command to `pnpm.exe`. This matches the audited `mise.lock` assets -`pnpm-win-x64.exe` and `pnpm-win-arm64.exe`; both carry required SHA-256 -checksums. The task runner does not synthesize `.cmd` names for pnpm, npm, npx, -or pnpx and does not introduce `cmd.exe`, `shell: true`, or command-string -quoting. Non-Windows commands remain direct. This local mise boundary is -distinct from GitHub Actions, which does not install mise and uses its own -reviewed `pnpm.cmd` batch-shim bridge in the CI toolchain verifier. - -On Windows only, the guarded native wrapper resolves a bounded Visual Studio -2022 or Visual Studio 2026 -MSVC/SDK environment for the child process immediately before the final -`cargo`/`pnpm tauri` compile. This is the single controlled exception to the -"no `cmd.exe`" rule: Visual Studio's only supported loading mechanism is -`cmd.exe` + `VsDevCmd.bat`. `scripts/tasks/windows-msvc-env.mjs` locates the -latest complete instance inside `[17.0,19.0)` (including Build Tools) through -the official `vswhere.exe`, requests UTF-8 JSON, and verifies the native-host -component: `Microsoft.VisualStudio.Component.VC.Tools.x86.x64` on x64 or -`Microsoft.VisualStudio.Component.VC.Tools.ARM64` on ARM64. It then spawns -`cmd.exe` directly (not `shell: true`) with the argv array -`["/d", "/s", "/c", ""]` and `windowsVerbatimArguments: true`, where -`` is manually built as -`call "" -no_logo -arch= -host_arch= >nul && "" -e "process.stdout.write(JSON.stringify(process.env))"`. -The child dumps `process.env` as JSON to avoid `set` text encoding/quoting -ambiguity; the result is parsed and validated for `INCLUDE`/`LIB`, a numeric -`VCToolsVersion`, and a Visual Studio environment major matching the selected -17.x/18.x installation. The loaded -environment merges only into the child env and never mutates `process.env`, -writes the system/user environment, or touches the registry. -`-arch`/`-host_arch` derive from `process.arch` (x64/x64 or arm64/arm64) and are -never hard-coded. The merge is additive: it only adds MSVC/SDK variables and -never overrides the owned RUSTC/RUSTDOC/target/linker/runner controls from -`ownedCargoEnvironment`. macOS never invokes the loader. +[Native Host Task Execution](./native-task-runner.md) owns Windows `pnpm.exe` +admission, the closed Visual Studio environment loader, child-only additive +merge and the macOS signed development app runner. The repository task API +continues to pass only validated argv and owned environment into that boundary; +it never exposes `cmd.exe`, signing secrets, compiler selection or shell text +as public usage parameters. Contract tests execute real `mise run` calls for a positional value, a flag, and a filtered test. Metadata inspection alone is not sufficient proof that @@ -619,220 +366,12 @@ Interactive tasks set `raw` and tear down the POSIX group or Windows tree explicitly. Generated `mise-tasks.md` identity is `tasks:docs:check` / `docs-contract-check.mjs` only. -## Scenario: Optional macOS Windows-MSVC Clippy diagnostic - -### 1. Scope / Trigger - -- Trigger: new public `mise run` names, a foreign-target argv, a - `dependency-environment` confirmation, and a JSON report that must not be - confused with Windows native acceptance. Code-spec depth is mandatory. -- Owner: `scripts/tasks/windows-msvc-cross.mjs` plus the two mise task tables. - Host-native override rejection is reused from `host-native.mjs`; this owner - adds C/CMake/xwin/native-dependency prefixes. Semantic Windows runtime - evidence stays in [Windows Shell-user Runtime](./windows-runtime-security.md) - and native CI/Release. - -### 2. Signatures - -```text -mise run system:check:windows-msvc-cross:advisory - env.FYAGENT_TASK_EFFECT = read-only - run = node scripts/tasks/windows-msvc-cross.mjs advisory - bootstrap DAG only; never check/CI/Release - -mise run system:check:windows-msvc-cross [--json] - env.FYAGENT_TASK_EFFECT = read-only - run = node scripts/tasks/windows-msvc-cross.mjs check - -mise run rust:clippy:windows-msvc-cross - env.FYAGENT_TASK_EFFECT = dependency-environment - confirm.default = no - run = node scripts/tasks/windows-msvc-cross.mjs clippy - -CARGO_XWIN_VERSION # exact string in windows-msvc-cross.mjs -WINDOWS_MSVC_CROSS_TARGET = x86_64-pc-windows-msvc -WINDOWS_MSVC_CROSS_HOST_TARGETS = darwin-x64 | darwin-arm64 -> that target -``` - -JSON report (`--json` or `usageBoolean("json")`): - -```text -{ - ok: boolean, - platform: Node process.platform, - target: "x86_64-pc-windows-msvc", - checks: [{ - id: "supported-host" | "caller-environment" | "cargo-xwin" | "clippy" - | "rust-target" | "clang-cl" | "lld-link" | "llvm-lib" - | "cmake" | "ninja", - name: string, - ok: boolean, - hint?: string, - detail?: string # first captured line, truncated to 240 chars - }] -} -``` - -Frozen Clippy plan from `planWindowsMsvcCrossClippy` (`shell: false`): - -```text -cargo xwin clippy - --cross-compiler clang-cl - --xwin-version 17 - --target x86_64-pc-windows-msvc - --workspace --all-targets --locked - --manifest-path src-tauri/Cargo.toml - -- -D warnings -``` - -Exact cargo-xwin version equality is against `CARGO_XWIN_VERSION` in the -script. Do not copy that literal into this spec or into generic docs. - -### 3. Contracts - -- `advisory` is read-only bootstrap reporting. Unsupported hosts print SKIP - and exit 0 without probing. Detect that skip by catching - `expectedWindowsMsvcCrossTarget(process.platform, process.arch)` against - `WINDOWS_MSVC_CROSS_HOST_TARGETS`. Do not write - `process.platform !== "darwin"` or `platform !== "darwin"`: the - `js:implicit-target` scanner treats a negated Darwin predicate as an - implicit non-macOS branch and fails `supported-platform:check`. On a - reviewed macOS host it prints the same complete report as `check`; missing - tools add an `ADVISORY` line and still exit 0. It never starts Clippy, - downloads CRT/SDK, or fails `bootstrap`. -- `check` is the explicit strict preflight: probe every bounded prerequisite - and print the complete report. Incomplete or unsupported-host results exit 1. - It never installs, downloads, caches CRT/SDK, accepts a license, or starts - Clippy, and it is not in `bootstrap`. -- `clippy` runs only after the same preflight is fully green. Mise owns the - default-no confirmation because cargo-xwin may download/cache Microsoft - CRT/SDK. The Node owner still prints the license note, then runs the frozen - argv. It does not prompt a second time. -- Hosts other than `darwin-x64` / `darwin-arm64` fail the strict preflight - before probing tools, with a single `supported-host` check. Windows - developers use native CI/HIL. -- Caller overrides are rejected before any toolchain child: reuse the - host-native Rust target/compiler/wrapper/runner/linker/Cargo-config scan, - plus exact names `AR`, `CC`, `CXX`, `CFLAGS`, `CXXFLAGS`, `LDFLAGS`, - `CMAKE`, `CMAKE_GENERATOR`, `CMAKE_PREFIX_PATH`, `CMAKE_TOOLCHAIN_FILE`, - `RUSTFLAGS`, `RUSTDOCFLAGS`, and prefixes `CARGO_XWIN_`, `XWIN_`, `CMAKE_`, - `CC_`, `CXX_`, `AWS_LC_`, `RING_`. -- No forwarded arguments. `clippy` with extra argv fails before Cargo. -- Strict preflight and Clippy are absent from `bootstrap`, `check`, - `check:backend`, `check:frontend`, `check:contracts`, `dev`, `build`, and - CI/Release. Advisory is required in the `bootstrap` closure and forbidden - in the `check` closure. -- Passing Clippy is compile diagnostics only. It does not claim registry, - PackageManager, WebView2, installer, UAC, launch, signing, packaging, or - HIL. - -### 4. Validation & Error Matrix - -| Condition | Required result | -| ----------------------------------------------------------------------- | ----------------------------------------------------------------- | -| Advisory on non-macOS | Print SKIP; exit 0; no tool probe | -| Advisory skip uses `process.platform !== "darwin"` | `js:implicit-target` / `supported-platform:check` fails | -| Advisory on macOS with missing tools | Print complete report + `ADVISORY`; exit 0; bootstrap continues | -| Strict preflight host is not macOS x64/arm64 | `ok=false`, `checks=[{id:supported-host}]`; exit 1; no tool probe | -| Caller env/Cargo-config override is set | `ok=false`, `checks=[{id:caller-environment}]`; no Cargo | -| Any bounded prerequisite missing or cargo-xwin version ≠ owner constant | Report every remaining check; strict preflight exit 1; no Clippy | -| `clippy` invoked without mise confirmation | Mise does not start the task; no download/cache | -| Forwarded Clippy argv | Throw before `cargo`; no child | -| Strict preflight or Clippy referenced from `bootstrap` / `check` / CI | Task-contract failure | -| Advisory missing from `bootstrap` or present in `check` | Task-contract failure | -| Result cited as native Windows acceptance | Keep the native gate pending | - -### 5. Good / Base / Bad Cases - -- **Good:** `bootstrap` prints the advisory without failing. A macOS developer - who wants the diagnostic then runs `system:check:windows-msvc-cross --json` - and explicitly confirms `rust:clippy:windows-msvc-cross`. Default `check` is - unchanged. -- **Base:** Windows or Linux `bootstrap` prints SKIP for the advisory. The - strict preflight on those hosts exits 1 with `supported-host`. Native - Windows CI/HIL remains the authority. -- **Bad:** let advisory exit 1, put strict preflight or Clippy in bootstrap, - skip hosts with `process.platform !== "darwin"`, accept forwarded - `--target`, pin a second cargo-xwin version in a spec/workflow, or treat a - green report as Windows installer/registry evidence. - -### 6. Tests Required - -- `tests/windowsMsvcCross.test.ts`: exact version parse, complete missing- - tool reporting, unsupported-host and override rejection before spawn, - frozen argv, default-no metadata, no package-manager/elevation command, - live `--json` preflight shape, advisory in bootstrap with exit 0, and - strict preflight/Clippy absent from bootstrap/check. -- `supported-platform:check` / `tests/remainingPlatformSurface.test.ts`: the - owner has no negated Darwin/`!== "win32"` fallback; advisory skip is the - reviewed host-map throw, not an implicit-target branch. -- `tests/localBuildBoundary.test.ts` and `miseTaskContract`: the three named - tasks exist with the signatures above; standard entrypoints still reject - other cross-target markers. -- `tests/classifyChanges.test.ts`: the new script is classified with other - task-runner sources, not as a native Windows acceptance path. - -### 7. Wrong vs Correct - -#### Wrong - -```text -bootstrap -> system:check:windows-msvc-cross # exit 1 blocks onboarding -check -> rust:clippy:windows-msvc-cross -cite macOS cargo-xwin as Windows HIL -if (process.platform !== "darwin") { SKIP; return } -``` - -#### Correct - -```text -bootstrap -> system:check:windows-msvc-cross:advisory # never fails -mise run system:check:windows-msvc-cross --json # explicit, may fail -mise run rust:clippy:windows-msvc-cross # default-no, frozen argv -native Windows CI/HIL # remaining acceptance -try { expectedWindowsMsvcCrossTarget(process.platform, process.arch) } -catch { print SKIP; return } -``` +## Specialized native task owners -## macOS signed development runner - -On macOS, the canonical `dev` task remains current-host-only, interactive, and -raw. It keeps Tauri dev/HMR, but supplies a fixed Cargo runner chain that wraps -the emitted debug executable in a real development app bundle before launch. -The chain performs, in order: - -1. fixed-path full-Xcode plus user-local Developer ID PKCS#12 preflight through - a reusable 0700 cache keychain. The task extracts the same certificate and - private key into a temporary 0700 directory, imports the leaf and traditional - RSA private key alongside the pinned Apple Root and Developer ID G2 public - certificates, never installs the release private key permanently in the - login keychain, and smoke-signs a copy of `/usr/bin/true` before the Swift - helper build. `machine-preflight --keep-session` keeps the cache keychain as - the user default through the detached Tauri spawn and nested app-runner - signing. App-runner or `restore-session` restores the original default and - search list after nested signing, setup failure, or Tauri process exit. A - standalone preflight restores immediately, and `restore-session` is - idempotent when no session is active. Never restore early or delete a - keychain after it has signed with this identity; delete only the temporary - extracted PEM files after import. -2. development-flavor universal privileged helper/client build and embedded - plist verification; -3. Tauri dev compilation with the privileged-client Cargo feature; -4. app bundle assembly, client/helper embedding, inside-out signing with the - frozen `Developer ID Application: William Wang (HY446996QX)` identity, - strict signature/link/rpath verification, and direct bundle executable - launch. Development does not notarize or staple the app. - -The runner accepts only Tauri/Cargo's fixed protocol arguments and rejects -forwarded application arguments. The task owns and sanitizes -`DEVELOPER_DIR`, Cargo/rustc runner settings, privileged artifact variables, -`DYLD_*`, `RUSTFLAGS`, `NODE_OPTIONS`, and related injection surfaces. Ctrl+C -continues to terminate the complete child process group. Linux and Windows keep -their previous native task behavior. - -The repository does not contain a developer-machine PKCS#12 path or password. -`scripts/tasks/macos-signed-dev.mjs` subcommand `configure` writes a mode-0600 configuration -under the user's FyAgent Application Support directory that references a local -PKCS#12 and credentials file. `mise run dev` consumes only that fixed local -configuration; callers cannot override signing paths or credentials through -task arguments or environment variables. +- [Native Host Task Execution](./native-task-runner.md) owns Windows executable + admission/MSVC environment loading and the macOS signed development app + runner. +- [Optional macOS Windows-MSVC Compile Diagnostic](./windows-msvc-cross-diagnostic.md) + owns the advisory, strict preflight and default-no cross-Clippy tasks. Its + output is compile diagnostics only and never matching-host Windows + acceptance. diff --git a/.trellis/spec/backend/trellis-prearchive-gate.md b/.trellis/spec/backend/trellis-prearchive-gate.md new file mode 100644 index 000000000..5101a3029 --- /dev/null +++ b/.trellis/spec/backend/trellis-prearchive-gate.md @@ -0,0 +1,130 @@ +# Trellis Direct-Session Prearchive Gate + +## 1. Scope / Trigger + +Read this contract before changing `check:prearchive`, +`check:contracts:prearchive`, `scripts/tasks/prearchive-check.mjs`, or the +private active-task exclusion accepted by the supported-platform checker. + +This gate exists only for the narrow interval in which one directly active, +in-progress Trellis task still contains tracked planning markers that will be +moved by `task.py archive`. It does not define the ordinary `check`, +`check:contracts`, CI, or post-archive behavior. The public task surface and +argument transport are owned by +[Repository Task Runner](./task-runner-contract.md); the downstream inventory +checker is owned by +[Supported-Platform Governance](./supported-platform-governance.md). + +## 2. Signatures + +```text +mise run check:prearchive --exclude-active-task +mise run check:contracts:prearchive --exclude-active-task + +scripts/tasks/prearchive-check.mjs + mode = check | check:contracts + excludeActiveTask = repository-relative canonical task path + +FYAGENT_SUPPORTED_PLATFORM_ACTIVE_TASK= + # private child-process transport only +``` + +Accepted path shape: + +```text +.trellis/tasks/MM-DD- +``` + +The leaf accepts exactly one input channel: direct CLI, mise usage, or the +private environment variable. It never infers an active task from a glob or +from repository state alone. + +## 3. Contracts + +- The wrapper accepts one repository-relative direct child below the canonical + `.trellis/tasks` root. It rejects parent traversal, backslashes, nesting, + archive paths, wildcards, symlinks, non-direct realpaths, non-directories, + and missing/symlinked/non-regular `task.json` files. +- `` is derived from the canonical directory name. `task.json.id` and + `task.json.name` must both equal it, and `status` must be `in_progress`. +- The same canonical path must be Trellis's current pointer with + `stale=false` and a direct `source="session:"`. A fallback pointer, + another task, or a stale record cannot authorize exclusion. +- Only after all identity checks pass may the wrapper transport the path + through `FYAGENT_SUPPORTED_PLATFORM_ACTIVE_TASK` to the one checker leaf. + Callers may not preseed it, combine it with a CLI/usage path, or make the + variable a CI input. +- The wrapper selects only `check` or `check:contracts`; it never forwards the + exclusion to unrelated leaves or retries a failed check without it. +- Canonical `check`, `check:contracts`, `supported-platform:check`, CI, and + post-archive validation always run without an exclusion. +- The exclusion is identity-specific, not semantic. It suppresses only the + directly active task directory during its own archive transition; all other + repository files and task records remain scanned. + +## 4. Validation & Error Matrix + +| Condition | Required result | +| --- | --- | +| `--exclude-active-task` missing or duplicated | Fail before nested check. | +| Wrapper mode is not `check` or `check:contracts` | Fail closed. | +| Path is absolute, nested, archived, traversing, wildcarded or contains backslashes | Reject before filesystem scan. | +| Task directory or `task.json` is a symlink/non-regular escape | Reject before nested check. | +| Directory-derived ID differs from `task.json.id` or `.name` | Reject. | +| Task status is not `in_progress` | Reject. | +| Current pointer is missing, stale, fallback, another task or another session | Reject. | +| Caller preseeds the private env variable or supplies more than one channel | Reject; do not choose a winner. | +| Nested check exits nonzero | Propagate the failure; never retry broader or omit the platform check. | +| Ordinary check/CI attempts an active-task exclusion | Contract regression; canonical scan must remain unfiltered. | + +## 5. Good / Base / Bad Cases + +- **Good:** two differently named fixture tasks each validate when that exact + path is the direct current in-progress task for its own session. +- **Base:** canonical `mise run check` executes with no private entry and scans + all active/archive paths normally. +- **Base:** after `task.py archive`, post-archive contracts run without an + exclusion and validate the moved task in its durable location. +- **Bad:** hard-code a historical task ID, accept `session-fallback`, skip every + `.trellis/tasks/**` path, or let CI provide the private variable. +- **Bad:** when prearchive fails, rerun canonical checks while silently omitting + the offending task. + +## 6. Tests Required + +- Pure path tests cover two valid task identities plus malformed date/ID, + traversal, backslash, archive, nesting, wildcard, directory symlink, + `task.json` symlink and realpath escape. +- Metadata tests cover `id`, `name`, `status`, missing and non-regular + `task.json` failures. +- Session tests cover direct ownership, stale pointers, fallback pointers, + wrong task and wrong session. +- Input-channel tests reject caller-preseeded private state and every + CLI/usage/environment duplication. +- Integration evidence records one real prearchive composite from the directly + bound session, archives the task, then runs the canonical post-archive gate + without an exclusion. +- CI/workflow tests prove the private environment entry is never supplied by + hosted automation. + +## 7. Wrong vs Correct + +### Wrong + +```text +check:prearchive -> skip .trellis/tasks/** +check failed -> rerun check without supported-platform scanner +CI -> FYAGENT_SUPPORTED_PLATFORM_ACTIVE_TASK= +``` + +### Correct + +```text +derive one .trellis/tasks/MM-DD-id path + -> prove realpath/file type + -> prove task.json id/name/status + -> prove direct current session ownership + -> privately pass to exactly one supported-platform leaf + -> archive + -> run canonical post-archive checks with no exclusion +``` diff --git a/.trellis/spec/backend/windows-agent-runtime-security.md b/.trellis/spec/backend/windows-agent-runtime-security.md new file mode 100644 index 000000000..784f159e7 --- /dev/null +++ b/.trellis/spec/backend/windows-agent-runtime-security.md @@ -0,0 +1,225 @@ +# Windows Agent Runtime Security + +## 1. Scope / Trigger + +Read this contract before changing Windows Agent desktop launch, elevated +Claude/Grok lifecycle routing, ordinary-user helper actions, or Uninstall/App +Paths enumeration rights. + +This owner assumes the frozen Explorer-user, hidden-path and elevation +fundamentals in [Windows Shell-user Runtime](./windows-runtime-security.md). +[External Agent Lifecycle](./external-agent-lifecycle.md) owns action legality, +inventory normalization and jobs; [External Agent Product Sources and Desktop +Identity](./external-agent-sources.md) owns release and installed-product +identity plus shared exact npm plans; [Claude Code CLI](./claude-code-cli.md) +owns Claude-specific discovery, owner and post-install verification. +This file owns the Windows execution/access boundary after those authorities +have admitted an action or observation. + +## 2. Signatures + +```text +InteractiveUserLaunch::trusted_windows_exe(path) + -> TrustedWindowsExe | InvalidWindowsExe + +InteractiveUserLauncher::open_trusted_windows_exe(path) +launch_trusted_windows_exe_as_user(path) + -> Explorer ShellExecute(SW_SHOWNORMAL) | INTERACTIVE_USER_UNAVAILABLE + +machine_program_files_directories() + -> [ProgramFiles, ProgramFilesX86] + +formal Windows Claude install|update + -> claude-tool helper + +formal Windows Grok observe|install|update + -> grok-tool helper + +development Windows Grok npm + -> LocalProcess using the same live exact plan + +fyagent-user-helper.exe + codex-msix-install --job-id --pipe + agent-exe-install --product qoderwork|trae-work|workbuddy|opencode + --job-id --pipe + grok-tool --action observe|install|update [--owner native|npm] + --job-id --pipe + claude-tool --action observe|install|update --job-id --pipe + +RegistryRights { query_value, enumerate_subkeys, create_subkey, set_value } +READ_VALUES = query +UPDATE_VALUES = query + set +TRAVERSE = query + enumerate +INVENTORY_PARENT_READ = query + enumerate, no create/set + +open_shell_user(Uninstall | AppPaths) -> INVENTORY_PARENT_READ +open_machine(Uninstall | AppPaths, Registry32 | Registry64) + -> INVENTORY_PARENT_READ +open_child(validatedName) -> READ_VALUES +``` + +No generic command/path helper exists. Public invalid-EXE errors map to +`external_launch_invalid_windows_exe`. + +## 3. Contracts + +### Trusted desktop EXE launch as Alice + +- Desktop identity is proven before this boundary. The launcher validates only + a nonempty host-absolute `.exe` path (case-insensitive), with no NUL, + `ParentDir` component or arguments. +- Launch reuses the Explorer COM adapter and `SW_SHOWNORMAL`, so the process + starts as Alice. COM failure returns `INTERACTIVE_USER_UNAVAILABLE`; it never + falls back to elevated `CreateProcess`, direct `Command`, Bob's + `ShellExecuteW`, or a renderer path. +- Keep trusted EXE, trusted AUMID and directory/URL request types separate. + A downloaded installer EXE is not eligible for ordinary launch; install uses + the retained package and closed helper product action. +- Observation roots are frozen Alice `LocalAppData\\Programs` plus machine + Program Files roots resolved with `SHGetKnownFolderPath(..., token=None)`. + Tests may substitute `FYAGENT_TEST_HOME` only through the reviewed fixture. +- Non-Windows implementations fail as unavailable/unsupported. A macOS unit + test must not pretend a `C:\\...` string is host-absolute. + +### Closed CLI/helper boundary on formal elevated Windows + +- OpenCode remains Desktop-only. Claude uses its dedicated closed lifecycle; + Grok remains the sole writable generic Tooling CLI. Other generic tools fail + before side effects. +- Formal elevated Windows never launches a user CLI or CLI-backed Auth from + the elevated parent. Claude and Grok delegate to distinct authenticated + ordinary-user helper actions; direct Auth remains unavailable. There is no + generic tool/Auth/helper verb. +- Helper argv contains only the fixed action/product identity, job UUID and + pipe nonce. After Hello, Grok npm receives the compact reviewed plan. Paths, + URLs, shell strings, free tool names, environment, stdout/stderr, browser + URLs, device codes and command lines never cross back to the elevated parent + or renderer. +- The signed product host selects the current x64/arm64 Grok optional package, + validates root and platform SHA-512 at an allowed registry, and sends only + exact version, registry index and the narrow allow-scripts bit. The helper + does not fetch metadata, choose Darwin/Linux packages or invent `@latest`. +- Native Grok install is an explicit closed action and consumes no npm plan. + An absent/malformed/`@latest`/unknown-registry npm plan produces no child. +- Development Windows LocalProcess is not an alternate product policy. It + resolves the same live exact plan, finds the npm major from the sibling or + PATH-default `npm.cmd` that actually runs, adds only + `--allow-scripts=@xai-official/grok` for npm 12+, treats blocked scripts as + failure, and requires PATH-default Grok version readback. It never executes + the `1.2.3` command-shape fixture. +- Closed desktop Agent EXE install uses its own protected package bridge. + Trusted desktop launch is not an install bypass. + +### Inventory parent registry rights + +- Intermediate fixed components use `TRAVERSE`. Uninstall/App Paths parent + leaves use the distinct `INVENTORY_PARENT_READ` constant even when its + current bit mask equals traverse. Validated child names open query-only with + `READ_VALUES`. +- Win32 parent mask is `KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS` plus the + selected WOW64 view. It never includes create/set rights or the broad + `KEY_READ` convenience mask. +- Optional parent `NotFound` is absence. A rejected registry-link on an + optional parent is also absence: shared WOW64 keys such as machine App Paths + may expose a `SymbolicLinkValue` in the 32-bit view for a location already + enumerated in the 64-bit view. The link is never followed. +- Raw OS access denied, enumeration/bounds failure, or frozen Shell-context + drift makes discovery incomplete. It must not become a false complete-empty + scan or `not_installed`. +- Child names are length/charset validated before open. Registry values remain + hints and are never executed. This access does not authorize WinGet, + PowerShell, a second scanner, or writable Uninstall/App Paths. + +## 4. Validation & Error Matrix + +| Condition | Required result | +| --- | --- | +| Trusted EXE path is relative, contains `..`/NUL, has arguments or non-EXE suffix | `external_launch_invalid_windows_exe`; no Explorer call. | +| Shape-valid observer-proven absolute EXE | Explorer ShellExecute as Alice. | +| Explorer COM unavailable | `INTERACTIVE_USER_UNAVAILABLE`; no elevated fallback. | +| Downloaded installer is submitted to trusted launch | Reject; use closed install helper. | +| Formal elevated parent attempts direct CLI or CLI-backed Auth | Fail before user process. | +| Formal Grok/Claude lifecycle | Matching closed helper; no generic/elevated fallback. | +| Helper plan missing, `@latest`, malformed or registry unknown | Fail closed; no npm child. | +| Helper argv contains path/URL/shell/free tool or returns raw process evidence | Contract failure. | +| Product host lacks matching Grok platform/integrity | Produce no plan/helper npm child. | +| Development LocalProcess uses `1.2.3`/`@latest` | Contract regression; resolve live exact version. | +| npm 12+ omits narrow allow-scripts or install scripts are blocked | Fail even if npm exits zero. | +| PATH-default Grok remains below planned version | Fail the attempt; no Agent success. | +| Inventory parent opens query-only then enumerates | Discovery incomplete on real hives. | +| Optional parent missing/rejected shared-view link | Absence; continue other views. | +| Parent access/enumeration/bound/Shell context fails | Incomplete aggregate; no false absence. | +| Parent or child receives create/set rights | Contract failure. | + +## 5. Good / Base / Bad Cases + +- **Good:** inventory proves a closed WorkBuddy/Qoder/TRAE EXE, then Explorer + opens it as Alice with no arguments from the elevated app. +- **Good:** formal Windows routes Grok through `grok-tool` and Claude through + `claude-tool`; development Windows applies the same exact plan and verifies + the PATH-default Grok version. +- **Good:** Alice/machine Uninstall and App Paths parents enumerate with + query+enumerate while every validated child remains query-only. +- **Base:** CLI-backed Auth stays unavailable on formal elevated Windows while + closed install/update helper actions remain usable. +- **Base:** an optional registry parent is absent or a rejected shared-view + link; the remaining complete views can still establish absence. +- **Bad:** `Command::new(exe)`, generic `helper run --cmd`, helper metadata + resolution, `npm @latest`, `KEY_READ`, WinGet, or treating access denied as + “not installed”. + +## 6. Tests Required + +- Trusted-launch tests reject relative, `.bat`, NUL and `nested/../*.exe` + inputs before a fake launcher runs, preserve AUMID/request separation and + prove no installer path uses this API. +- Matching-host launch tests prove Explorer-user execution and controlled COM + failure; portable shape tests do not claim Windows process identity. +- Formal boundary tests keep non-Grok generic tools fail-closed, route Grok to + ordinary-user helper and Claude to its dedicated helper, and reject direct + CLI/Auth execution. +- Static/helper protocol tests prove there is no generic verb/path/URL/raw + stdout DTO and bind the compact plan framing to exact version, registry index + and allow-scripts bit only. +- Product-host tests admit only current `grok-win32-x64`/`arm64`, require both + integrities and never move metadata resolution into the helper. +- LocalProcess tests reject fixture/`@latest` argv, add allow-scripts only for + npm 12+, preserve an existing flag, detect blocked-install-script output and + require PATH-default version readback. +- Registry tests prove parent leaves use `INVENTORY_PARENT_READ`, child opens + use `READ_VALUES`, masks exclude create/set, and shared-link rejection is + distinct from raw access denied. +- Inventory projection tests distinguish complete/no-candidate from incomplete + discovery. Alice HKU/Wow6432Node/UAC HIL remains explicit residual evidence. + +## 7. Wrong vs Correct + +### Wrong + +```text +elevated FyAgent -> CreateProcess(observerPath) +helper run --cmd +development Windows -> npm i -g @xai-official/grok@1.2.3 -> exit 0 = success +open(UninstallParent, READ_VALUES) -> enum_keys -> treat access denied as empty +``` + +### Correct + +```text +observer-proven closed EXE + -> validate absolute no-arg .exe shape + -> Explorer ShellExecute as Alice + +formal Claude/Grok + -> distinct closed helper action + authenticated pipe + -> host-selected exact npm plan where applicable + +development Grok + -> live exact plan + npm-major script policy + -> PATH-default version reaches planned version + +open parent(INVENTORY_PARENT_READ) + -> enumerate + -> validate child name + -> open child(READ_VALUES) +``` diff --git a/.trellis/spec/backend/windows-msvc-cross-diagnostic.md b/.trellis/spec/backend/windows-msvc-cross-diagnostic.md new file mode 100644 index 000000000..781de5b0b --- /dev/null +++ b/.trellis/spec/backend/windows-msvc-cross-diagnostic.md @@ -0,0 +1,181 @@ +# Optional macOS Windows-MSVC Compile Diagnostic + +## 1. Scope / Trigger + +Read this contract before changing the optional macOS-only Windows-MSVC +preflight or Clippy diagnostic exposed through `mise run`. + +The owner is `scripts/tasks/windows-msvc-cross.mjs` plus the matching task +definitions and executable tests. This diagnostic may help a macOS developer +find Windows compile errors earlier, but it is never Windows runtime, +installer, registry, signing, packaging or HIL evidence. Native host task +execution remains in [Native Host Task Execution](./native-task-runner.md); +actual Windows evidence remains in CI/Release and the Windows runtime specs. + +## 2. Signatures + +```text +mise run system:check:windows-msvc-cross:advisory + effect = read-only + -> node scripts/tasks/windows-msvc-cross.mjs advisory + -> bootstrap DAG only; never check/CI/Release + +mise run system:check:windows-msvc-cross [--json] + effect = read-only + -> node scripts/tasks/windows-msvc-cross.mjs check + +mise run rust:clippy:windows-msvc-cross + effect = dependency-environment + confirmation.default = no + -> node scripts/tasks/windows-msvc-cross.mjs clippy + +WINDOWS_MSVC_CROSS_TARGET = x86_64-pc-windows-msvc +WINDOWS_MSVC_CROSS_HOST_TARGETS = darwin-x64 | darwin-arm64 +``` + +The exact `cargo-xwin` version is owned once by the script. Generic docs/specs +must not duplicate its literal. + +JSON report: + +```text +{ + ok: boolean, + platform: Node process.platform, + target: "x86_64-pc-windows-msvc", + checks: [{ + id: "supported-host" | "caller-environment" | "cargo-xwin" | "clippy" + | "rust-target" | "clang-cl" | "lld-link" | "llvm-lib" + | "cmake" | "ninja", + name: string, + ok: boolean, + hint?: string, + detail?: string + }] +} +``` + +Frozen Clippy plan (`shell:false`): + +```text +cargo xwin clippy + --cross-compiler clang-cl + --xwin-version 17 + --target x86_64-pc-windows-msvc + --workspace --all-targets --locked + --manifest-path src-tauri/Cargo.toml + -- -D warnings +``` + +## 3. Contracts + +### Advisory + +- Advisory is read-only bootstrap reporting. On unsupported hosts it prints + `SKIP`, exits zero and performs no tool probe. +- Supported-host detection calls the reviewed host-map owner and handles its + failure. Do not write a negated Darwin predicate such as + `process.platform !== "darwin"`; the supported-platform scanner treats that + as an implicit non-macOS target branch. +- On macOS, advisory runs the complete prerequisite report. Missing tools emit + `ADVISORY` and still exit zero so bootstrap remains useful. +- Advisory never starts Clippy, downloads/caches Windows CRT/SDK, accepts a + license or mutates repository/system state. + +### Strict preflight and Clippy + +- `check` is explicit and strict. It probes every bounded prerequisite, prints + the complete report and exits nonzero for an unsupported/incomplete setup. + It never installs, downloads or starts Clippy. +- `clippy` starts only after the same preflight is fully green. Mise owns the + default-no confirmation because cargo-xwin may download/cache Microsoft + CRT/SDK. The script prints the license note and does not prompt again. +- Only Darwin x64/arm64 is admitted. Windows developers use native builds; all + other hosts fail strict preflight before probing tools. +- No forwarded argv is accepted. The target and Cargo plan are closed. +- Caller overrides are rejected before any child. Reuse the host-native Rust + target/compiler/wrapper/runner/linker/Cargo-config checks and additionally + reject exact `AR`, `CC`, `CXX`, `CFLAGS`, `CXXFLAGS`, `LDFLAGS`, `CMAKE`, + `CMAKE_GENERATOR`, `CMAKE_PREFIX_PATH`, `CMAKE_TOOLCHAIN_FILE`, `RUSTFLAGS`, + `RUSTDOCFLAGS`, plus prefixes `CARGO_XWIN_`, `XWIN_`, `CMAKE_`, `CC_`, + `CXX_`, `AWS_LC_`, and `RING_`. +- Strict preflight and Clippy are absent from `bootstrap`, `check`, all scoped + check aliases, dev/build, CI and Release. Advisory is required in bootstrap + and forbidden in the canonical check closure. + +### Evidence boundary + +A green compile diagnostic proves only that the frozen source can pass the +selected cross Clippy plan in that macOS environment. It does not prove +Windows PackageManager, registry, Credential Manager, WebView2, installer, +UAC, ordinary-user launch, signing, packaging, runtime or HIL behavior. + +## 4. Validation & Error Matrix + +| Condition | Required result | +| --- | --- | +| Advisory on non-macOS | Print `SKIP`, exit zero, no tool probe. | +| Advisory uses a negated Darwin fallback branch | Supported-platform check fails. | +| Advisory on macOS misses prerequisites | Complete report + `ADVISORY`, exit zero, no Clippy. | +| Strict preflight on unsupported host | One failed `supported-host` check, exit nonzero, no tool probe. | +| Caller env/Cargo config override is present | Failed `caller-environment`, no tool child. | +| Prerequisite missing or cargo-xwin version differs from owner | Report all bounded checks, exit nonzero, no Clippy. | +| Clippy invoked without mise confirmation | Task does not start; no download/cache. | +| Extra argv or target override is supplied | Reject before Cargo. | +| Strict/Clippy enters bootstrap, canonical check, CI or Release | Task-contract failure. | +| Advisory leaves bootstrap or enters canonical check | Task-contract failure. | +| Cross result is cited as Windows native acceptance | Keep native evidence pending. | + +## 5. Good / Base / Bad Cases + +- **Good:** bootstrap on macOS prints a non-blocking advisory. A developer who + wants more evidence explicitly runs strict preflight, then explicitly accepts + the dependency-environment confirmation for the frozen Clippy plan. +- **Base:** bootstrap on Windows/Linux prints `SKIP`; strict preflight there + fails with only `supported-host`. Native Windows CI remains authority. +- **Base:** one macOS prerequisite is missing; the advisory reports all checks + and exits zero while strict mode returns the same facts nonzero. +- **Bad:** let advisory fail onboarding, put strict/Clippy in canonical checks, + accept forwarded `--target`, duplicate the cargo-xwin version, install tools, + or claim Windows installer/runtime acceptance. + +## 6. Tests Required + +- `tests/windowsMsvcCross.test.ts` covers exact owner-version parsing, + complete missing-tool reports, supported/unsupported hosts, override + rejection before spawn, frozen argv, default-no metadata, no installer/ + elevation command, live JSON shape and advisory zero-exit behavior. +- Task graph tests require advisory in bootstrap and prove strict/Clippy are + absent from bootstrap, canonical checks, dev/build, CI and Release. +- `supported-platform:check` and + `tests/remainingPlatformSurface.test.ts` reject negated Darwin/implicit-target + branches and require the reviewed host-map exception path. +- `tests/localBuildBoundary.test.ts` and `miseTaskContract` prove the three + task signatures/effects and that standard entry points reject other + cross-target markers. +- `tests/classifyChanges.test.ts` classifies this script as task-runner source, + not native Windows acceptance. +- Matching-host Windows jobs/HIL remain independently required. + +## 7. Wrong vs Correct + +### Wrong + +```text +bootstrap -> strict Windows-MSVC preflight -> nonzero blocks onboarding +check -> cargo xwin clippy +if (process.platform !== "darwin") return +green cross Clippy -> Windows accepted +``` + +### Correct + +```text +bootstrap -> advisory # always non-blocking +explicit strict preflight --json # may fail +explicit default-no cross Clippy # closed target/argv +native Windows CI/HIL # runtime acceptance + +try { expectedWindowsMsvcCrossTarget(platform, arch) } +catch { print SKIP; return } +``` diff --git a/.trellis/spec/backend/windows-runtime-security.md b/.trellis/spec/backend/windows-runtime-security.md index 2d72bb2f4..19ae695de 100644 --- a/.trellis/spec/backend/windows-runtime-security.md +++ b/.trellis/spec/backend/windows-runtime-security.md @@ -18,23 +18,15 @@ one-operation `%ProgramData%\FyAgent.PackageBridge-{96F39D37-0F42-486F-8C86-3631C12171C5}\v1` package bridge is a separate executable-installer object with no state, lease, HMAC, activation, or startup-admission role. Codex MSIX and the reviewed -Agent EXE products (`qoderwork | trae-work | workbuddy | opencode`) reuse it -through separate closed helper actions. OpenCode Windows x64 uses the -reviewed WinVerifyTrust identity; ARM64 remains unsupported. Grok -Build observe/install/update reuses the same helper executable and pipe -handshake but does not use PackageBridge. The +Agent actions may reuse the helper executable and authenticated pipe only +through their separately reviewed closed protocols; see +[Windows Agent Runtime Security](./windows-agent-runtime-security.md). The application bridge module owns normal settlement and next-elevated-creation orphan cleanup; neither this runtime nor NSIS may reinterpret it as the former runtime tree. ## 2. Signatures -Claude CLI lifecycle also uses the existing ordinary-user helper with the -closed `claude-tool --action observe|install|update` action. Its independent -action identity, exact npm plan and native verification are owned by -[Claude Code CLI](./claude-code-cli.md); it does not weaken the parent process's -elevated CLI execution prohibition or admit Auth commands. - ```rust pub fn initialize_windows_user_context() -> Result<(), WindowsStartupErrorCode>; @@ -63,21 +55,8 @@ pub(crate) async fn open_http_url_as_user( pub(crate) fn open_http_url_as_user_sync( raw_url: &str, ) -> Result<(), ProcessLaunchError>; - -#[cfg(target_os = "windows")] -pub(crate) fn machine_program_files_directories() -> Vec; - -pub(crate) fn launch_trusted_windows_exe_as_user( - executable: &Path, -) -> Result<(), String>; ``` -`launch_trusted_windows_exe_as_user` is crate-private. It does not accept -arguments, a working directory, or a verb. Invalid shape maps to -`external_launch_invalid_windows_exe` before any Explorer call. The AUMID -helper remains a separate Codex path and must not be deleted to add EXE -launch. - The frozen internal value is deliberately not serializable: ```text @@ -149,11 +128,9 @@ expose the Shell SID or paths and does not decide which user owns state. would address the elevated process account and is forbidden for per-user FyAgent policy on Windows. The only writable FyAgent-policy locations are the fixed `Environment` and `Software\Microsoft\Windows\CurrentVersion\Run` - keys. Agent inventory additionally opens Alice Uninstall/App Paths and the - matching machine Uninstall/App Paths as read-only parents; that capability - is `RegistryRights::INVENTORY_PARENT_READ` and does not expand the writable - policy set. Semantic inventory projection belongs to - [External Agent P0 Safety](./external-agent-p0.md). + keys. Agent inventory read rights and incomplete-discovery semantics are + owned by [Windows Agent Runtime Security](./windows-agent-runtime-security.md) + and do not expand this writable policy set. Each component is opened relative to an already pinned parent with `REG_OPTION_OPEN_LINK`; any `SymbolicLinkValue` marker is rejected. An existing key returned by create-or-open is discarded and reopened with the @@ -214,46 +191,22 @@ expose the Shell SID or paths and does not decide which user owns state. ### Preserve the narrow elevated-command boundary `formal_windows_build` remains a compile-time manifest fact. In a formal -elevated build, the elevated parent does not probe or execute a user CLI. -Grok Build and the dedicated Claude CLI façade delegate observe/install/update -to distinct closed ordinary-user helper actions; neither is permission to run -the CLI in the elevated parent. Helper failure must not fall back to elevated -CLI execution. The Codex model-catalog CLI fallback is skipped completely. A non-formal build -may execute Alice's discovered Codex entry only after clearing the inherited -environment and rebuilding a narrow environment from the frozen Alice paths -and OS-resolved constants. The shared child-environment builder clears inherited -variables and supplies Alice Profile/Local/Roaming/TEMP, frozen PATH (with only -the selected entry directory optionally prepended), PATHEXT, and OS-resolved -ComSpec/SystemRoot. Windows CLI, version, and cmd-shim execution all use that -builder. Shared detected-tool execution helpers enforce the -same formal-build gate themselves so internal callers cannot bypass a public -command-level check. -It no longer selects a machine runtime or requires the process SID to equal the -Shell SID. Legacy Run-value cleanup is known-name-only, runs after primary -instance admission, and is best-effort; its failure must not block startup. -The generic Tooling lifecycle endpoint remains Grok-only; Claude's Agent -lifecycle uses the separate `services/tooling/claude.rs` owner and -`claude-tool` action documented in [Claude Code CLI](./claude-code-cli.md). -OpenCode CLI lifecycle is not admitted. Auth operations that require a user -CLI still fail closed on formal Windows; no login/logout/status verb is added -to either tool helper. OpenCode's bounded credential-file observation and -trusted Desktop handoff have their separate [Auth](./external-agent-auth.md) -contract and are not user-CLI execution. -The ordinary-user helper has four closed action families: Codex MSIX, Agent -EXE with the product enum `qoderwork | trae-work | workbuddy | opencode` -(OpenCode uses the reviewed WinVerifyTrust identity on x64; ARM64 remains -unsupported), Grok tool and Claude tool. Their tool verbs are only -`observe | install | update`; Grok retains its optional `none | native | npm` -owner, while Claude's independent owner/plan rules are in its focused spec. -Default Grok install is official npm. After Hello, the host writes an -80-byte `GrokNpmInstallPlan` control (exact version, closed registry index, -allow-scripts flag). The helper does not resolve `@latest`, does not invent a -registry, and refuses npm install when the plan is missing or invalid. -Native install is only the explicit `install_native` / native owner path. -It accepts no free CLI tool name, command, URL, package path, working -directory, verb, scope, silent switch, environment block, or raw argument -vector. Helper stdout/stderr is discarded after local bounded parsing and never -crosses the pipe. +elevated build, the parent never probes or executes a user CLI. Closed +ordinary-user helper failure must not fall back to elevated execution, and the +Codex model-catalog CLI fallback is skipped completely. + +A non-formal build may execute Alice's discovered Codex entry only after +clearing the inherited environment and rebuilding a narrow child environment +from frozen Alice Profile/Local/Roaming/TEMP, the real frozen PATH (with only +the selected entry directory optionally prepended), PATHEXT and OS-resolved +ComSpec/SystemRoot. Version and cmd-shim execution use the same builder, and +shared detected-tool helpers enforce the formal-build gate themselves. + +The runtime no longer selects a machine user runtime or requires process/Shell +SID equality. Known legacy Run-value cleanup occurs only after primary-instance +admission and remains best-effort. Agent helper action families, Claude/Grok +plans, direct Auth restrictions and development LocalProcess parity are owned +by [Windows Agent Runtime Security](./windows-agent-runtime-security.md). ### Open validated links through the interactive Explorer shell @@ -275,13 +228,10 @@ IShellFolderViewDual.Application -> IShellDispatch2`. `IShellDispatch2::ShellExecute`, so the system browser receives a foreground- eligible normal-show request. The fixed installer-helper launch retains its separate empty show argument and action-owned exact argument contract. -- Closed desktop-agent `.exe` paths (WorkBuddy / QoderWork CN / TRAE Work CN / - OpenCode) use the same Explorer `ShellExecute` route after the observer - proves PE `ProductName` at a closed relative path. OpenCode's installed - relative is `@opencode-aidesktop/OpenCode.exe` in addition to - `OpenCode/OpenCode.exe`. The launch boundary accepts only an - absolute `.exe` with no arguments, `..`, or NUL. Identity proof stays in - the observer; this module never scans vendor config directories. +- Observer-proven desktop EXE launch may reuse this Explorer COM transport only + through the closed path/identity contract in + [Windows Agent Runtime Security](./windows-agent-runtime-security.md). The + general HTTP owner never accepts executable input. - There is no `ShellExecuteW`, `Command::new`, `cmd`, PowerShell, arbitrary executable, current-process browser launch, or `window.open` fallback. If the Explorer COM chain is unavailable, fail closed instead of launching as the @@ -302,8 +252,6 @@ IShellFolderViewDual.Application -> IShellDispatch2`. | Frozen Shell session/SID drifts before a protected side effect | Stop that side effect; do not mutate the context or select another user. | | Alice Store/window-state JSON is missing, corrupt, or oversized | Use safe defaults at the same Alice path; do not consult or create Bob's app-data directories or allocate beyond the fixed read limit. | | Any fixed Alice HKU path component is a registry symbolic link | Reject that operation before reading, deleting, or writing a value; never reopen the key by an unverified full string path. | -| Inventory Uninstall/App Paths parent is opened query-value-only, then subkeys are enumerated | Real views fail; supported products collapse to `unknown`. Open the parent with `INVENTORY_PARENT_READ`. | -| Inventory parent or enumerated child receives create/set rights | Contract test fails; inventory is read-only. | | Legacy Alice Run value is absent, inaccessible, or cleanup fails | Continue startup and emit only a bounded diagnostic after first-instance admission. | | A protected installer PackageBridge orphan exists | Do not use it for startup, activation, identity, or user-path selection; only the executable-installer bridge owner may inspect it during the next elevated bridge creation. | | Single-instance envelope is oversized, contains controls, or has an invalid deep link | Reject before lightweight/focus/event behavior; never log the raw payload. | @@ -313,15 +261,6 @@ IShellFolderViewDual.Application -> IShellDispatch2`. | External link is accepted but browser would remain backgrounded | Pass fixed `SW_SHOWNORMAL` for ordinary external links; helper show semantics remain unchanged. | | Explorer COM acquisition or `ShellExecute` fails | Return controlled `INTERACTIVE_USER_UNAVAILABLE`; do not try a command, direct shell, renderer, or elevated-user fallback. | | OAuth authorize URL is opened with `cmd /c start` | Contract regression; `&` splits the query. Use `open_http_url_as_user_sync`. | -| Closed desktop-agent `.exe` is relative, contains `..` or NUL, or is not `.exe` | `external_launch_invalid_windows_exe`; Explorer is not invoked. | -| Closed desktop-agent `.exe` is observer-proven under Alice Programs or machine Program Files | Explorer `ShellExecute` as Alice; never `CreateProcess` / `ShellExecuteW` from Bob. | -| Formal elevated Windows direct user-CLI execution or CLI-based Auth | Fail closed before probing/launching; OpenCode file observation/Desktop handoff is not a CLI exception. | -| Formal elevated Windows Grok Build observe/install/update | Closed `grok-tool` helper action under the frozen Explorer user; helper failure must not fall back to elevated CLI. | -| Formal Windows Claude Agent lifecycle | Dedicated Claude owner uses closed `claude-tool`; never the generic Tooling endpoint or an Auth verb. | -| The installer helper accepts URL, path, shell string, scope, silent switch, or raw child stdout | Contract/static test fails; only exact Codex MSIX, Agent EXE, Grok tool and Claude tool families are registered. | -| Helper `Hello(action)` differs from the parent-selected action/product | Reject before bridge control/admission; zero installer launch. | -| Agent EXE helper `ShellExecuteEx` succeeds, including a missing process handle | Job `succeeded` (vendor-wizard handoff); do not wait, kill, or delete the PackageBridge EXE leaf. | -| Agent EXE helper launch uses a null verb or inherits the helper console | Contract/static test fails; fMask is `SEE_MASK_NOCLOSEPROCESS` plus `SEE_MASK_NO_CONSOLE` and `lpVerb` is `open`. | | Non-Windows platform | Preserve its existing path resolver, Store/window-state plugin, and single-instance behavior. | ## 5. Good / Base / Bad Cases @@ -345,11 +284,6 @@ IShellFolderViewDual.Application -> IShellDispatch2`. - Bad: restore `%ProgramData%\FyAgent\runtime`, treat PackageBridge as runtime state or an activation channel, infer a user from an active WTS session, or let a second-instance argument invoke helper/package/filesystem side effects. -- Good: observer-proven `WorkBuddy.exe` (absolute, `.exe`, no `..`) opens - through the same Explorer `ShellExecute` chain as catalog HTTPS links. -- Bad: start Claude/OpenCode CLI or a Catalog EXE from the elevated - parent with `CreateProcess`, run Grok from the elevated parent instead of - the helper, or add a helper that accepts a renderer command string. ## 6. Tests Required @@ -374,13 +308,10 @@ IShellFolderViewDual.Application -> IShellDispatch2`. and both initial/lightweight WebViews use Alice's explicit data path. - Registry tests cover regular/missing/link components, an intermediate link, a final link, newly created keys, and the required no-follow reopen after an - existing create result. Inventory-parent tests prove Uninstall/App Paths - leaves use `INVENTORY_PARENT_READ` (`KEY_QUERY_VALUE | -KEY_ENUMERATE_SUB_KEYS`, no create/set), that the constant stays distinct - from `TRAVERSE` even when the current mask is identical, and that enumerated - children stay `READ_VALUES`. Native registry-link HIL remains unexecuted. Any - future, separately authorized runtime validation must use only disposable - HKCU test keys when checking intermediate and final link rejection. + existing create result. Separately authorized runtime validation uses only + disposable HKCU test keys for intermediate/final link rejection. Agent + inventory-parent masks and native residual evidence are owned by + [Windows Agent Runtime Security](./windows-agent-runtime-security.md). - Single-instance tests cover count, item, aggregate, control-character, malformed/unsupported deep-link, valid deep-link, no-link focus, renderer-readiness queuing/drain, and no privileged callback action. Never @@ -391,25 +322,9 @@ KEY_ENUMERATE_SUB_KEYS`, no create/set), that the constant stays distinct intermediate cast, exact OLE/DDE initialization, `SW_SHOWNORMAL` only on the ordinary link path, and negative scans for `SWC_EXPLORER`, command interpreters, direct `ShellExecuteW`, and arbitrary executable fallback. - Trusted-exe tests must accept an absolute `.exe` (use a host-absolute - temp path; a `C:\...` string is not absolute on Unix) and reject - relative / `..` / non-`.exe` before the fake launcher. Native acceptance - must click a real Tauri catalog action and observe the target in the - interactive user's foreground browser; process creation or a successful - HRESULT alone is insufficient. -- Agent tests distinguish dedicated Claude lifecycle from still-blocked - direct CLI/Auth execution and unsupported OpenCode CLI. No generic command - helper is registered. Claude helper identity/plan tests live in its focused - contract. Grok Build tests must prove - the closed `grok-tool` helper path and the absence of elevated fallback. - Installer-helper tests must prove exact action/product CLI, v3 Hello-action - binding, Grok wire codes 5–13, fixed bridge artifact kind, and no - tool/URL/path argv. Agent EXE helper tests must prove - `SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE` plus fixed `open`, no - wait/`GetExitCodeProcess`, and that helper success retains the PackageBridge - EXE leaf. Existing Tooling formal-build fail-closed tests remain - authoritative. - Bob/Alice/UAC installer HIL remains unverified. +- Agent-specific trusted-EXE, Claude/Grok helper and inventory-parent tests are + required by [Windows Agent Runtime Security](./windows-agent-runtime-security.md) + rather than duplicated here. - A real current-host Tauri click may prove only the external-link path it exercises. It does not establish Windows 10/11 coverage, ARM64, elevated Bob/Alice, startup admission, WebView path ownership, Shell-token freezing, @@ -451,362 +366,10 @@ validated HTTP(S) -> SWC_DESKTOP automation chain -> IDispatch cast COM failure -> controlled error with no fallback ``` -Wrong: - -```text -elevated FyAgent -> CreateProcess(WorkBuddy.exe) -observer -> ~/.workbuddy exists => installed -``` - -Correct: - -```text -observer proves closed relative path + PE ProductName - -> launch_trusted_windows_exe_as_user(absolute .exe, no args) - -> Explorer ShellExecute as Alice -``` - -## Scenario: Trusted desktop-agent EXE launch as Alice - -### 1. Scope / Trigger - -- Trigger: WorkBuddy / QoderWork CN / TRAE Work CN launch on formal - Windows must run as Alice. This is a new process-launch variant plus - machine Program Files roots, so code-spec depth is mandatory. -- Identity proof stays in `agent_install/desktop.rs`. This module only - validates EXE shape and opens Explorer. - -### 2. Signatures - -```text -InteractiveUserLaunch::trusted_windows_exe(path) -> TrustedWindowsExe | InvalidWindowsExe -InteractiveUserLauncher::open_trusted_windows_exe(path) -launch_trusted_windows_exe_as_user(path) -> () | INTERACTIVE_USER_UNAVAILABLE -machine_program_files_directories() -> [ProgramFiles, ProgramFilesX86] -``` - -Public error: `ProcessLaunchError::InvalidWindowsExe` → -`external_launch_invalid_windows_exe`. - -### 3. Contracts - -- Shape: nonempty, host-absolute, `.exe` (case-insensitive), no NUL, no - `ParentDir` component, no arguments. -- Explorer adapter reuses `launch_from_explorer(path)` with - `SW_SHOWNORMAL`. macOS opener returns `InteractiveUserUnavailable`. -- Keep `open_trusted_windows_app_aumid` / AUMID helper. Do not collapse - EXE launch into AUMID or into `open_directory`. -- Observation roots on Windows: Alice `LocalAppData\Programs` plus - `machine_program_files_directories()` (`SHGetKnownFolderPath` with - token `None`). Tests may substitute `FYAGENT_TEST_HOME`. - -### 4. Validation & Error Matrix - -| Condition | Required result | -| -------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| Relative, `..`, NUL, or non-`.exe` | `external_launch_invalid_windows_exe`; no Explorer | -| Shape-valid absolute `.exe` | Explorer `ShellExecute` as Alice | -| Explorer COM unavailable | `INTERACTIVE_USER_UNAVAILABLE`; no `CreateProcess` | -| macOS / Linux call the EXE opener | `InteractiveUserUnavailable` / `PlatformUnsupported` | -| Downloaded installer EXE is submitted to ordinary trusted-EXE launch | Reject; install requires retained package + closed helper product action | - -### 5. Good/Base/Bad Cases - -- Good: tempdir `WorkBuddy.exe` is accepted by the shape check on Unix - test hosts because `Path::is_absolute` is host-native. -- Base: HTTPS catalog links and Codex AUMID launch remain separate - request types. -- Bad: `Command::new(exe)`, `ShellExecuteW` from Bob, or treating - `C:\WorkBuddy.exe` as absolute in a macOS unit test. - -### 6. Tests Required - -- `verified_windows_exe_launch_rejects_non_exe_input_before_the_fake_runs`. -- Negative: relative path, `.bat`, `nested/../WorkBuddy.exe`. -- Desktop observation tests on both hosts as listed in - [External Agent P0 Safety](./external-agent-p0.md). -- NSIS contract still forbids `taskkill`; this launch path is not an - installer. - -### 7. Wrong vs Correct - -#### Wrong - -```rust -std::process::Command::new(exe).spawn()?; -``` - -#### Correct - -```rust -crate::platform::process_launch::launch_trusted_windows_exe_as_user(exe) - .map_err(|_| AgentReasonCode::InteractiveUserUnavailable)?; -``` - -## Scenario: Agent Catalog CLI and Auth sessions on formal elevated Windows - -### 1. Scope / Trigger - -- Trigger: Agent Catalog starts OpenCode Desktop install through the - Agent façade, Claude CLI through its dedicated owner, Grok Build through - generic Tooling, and Auth through - the separate Auth-session façade. - Formal elevated Windows still forbids launching a user CLI from the elevated - parent. Both admitted CLI lifecycle owners must use their distinct - closed ordinary-user helper actions. Neither surface may grow a generic command/path - helper. - -### 2. Signatures - -No generic Windows command helper is registered. The existing installer helper -keeps closed MSIX and Agent-EXE actions plus separate Grok and Claude tool -families. Direct CLI-based Auth remains unavailable on formal elevated Windows; -OpenCode Desktop provider observation/connect is owned separately. - -```text -start_agent_action({ agentId: opencode, surface: cli, ... }) - -> surface_not_supported - -start_agent_action({ agentId: claude-code, surface: cli, action: install|update, ... }) - -> dedicated Claude lifecycle -> claude-tool helper - -run_tool_lifecycle_action(tools=["claude"|"opencode"|...], action) - -> error before any side effect unless tool == "grok" - -run_tool_lifecycle_action(tools=["grok"], action=install|update|install_official_npm|install_native) - formal Windows -> grok-tool helper; no elevated fallback - default install -> official npm exact-version plan (no @latest) - install_native -> official x.ai/PowerShell installer - development Windows LocalProcess -> same live npm plan as helper - (resolve_published_manifest; never default_install_command 1.2.3) - npm 12+ --allow-scripts=@xai-official/grok from the executing npm major - exit 0 is not success if postinstall was blocked or PATH-default grok - is still below the planned version - macOS -> existing Tooling owner, same Grok rules - -CLI-backed Auth observation/session - -> unavailable / interactive_user_unavailable on formal Windows - // No Auth helper verb. OpenCode file/desktop paths are not CLI-backed. - -fyagent-user-helper.exe - codex-msix-install --job-id --pipe - agent-exe-install --product qoderwork|trae-work|workbuddy|opencode - --job-id --pipe - grok-tool --action observe|install|update [--owner native|npm] - --job-id --pipe - claude-tool --action observe|install|update --job-id --pipe - // After Hello: host writes 80-byte GrokNpmInstallPlan. Missing/invalid/@latest - // plan => helper refuses npm. The Windows product has already selected and - // integrity-checked the win32 x64/arm64 optional package; platform package - // and registry metadata resolution never move into the helper. Native - // install does not consume the npm plan. -``` - -### 3. Contracts - -- Generic Tooling and direct CLI execution keep their formal-build gate. - Grok and the separate Claude lifecycle delegate to their closed helper - actions. The Claude addition does not authorize elevated probing, Auth - execution, PowerShell/WinGet installation or arbitrary helper commands. -- Helper stdout/stderr, environment, browser URL, device code, executable - path, and command line must never return to the elevated parent or - renderer. -- The Windows product host is the platform-package authority. It maps only the - current x64/arm64 architecture to the corresponding `grok-win32-*` manifest - entry, validates root and platform SHA-512 at an allowed registry, and then - sends the compact exact-version/registry/allow-scripts control. The helper - must not select Darwin/Linux packages or fetch registry metadata. -- Development Windows LocalProcess must apply the same live argv and - allow-scripts policy without widening the elevated parent. Detect npm major - from the sibling or PATH-default `npm.cmd` that will actually run. npm 11 - must not receive `--allow-scripts`; npm 12+ uses only - `--allow-scripts=@xai-official/grok`. Blocked install scripts or a - PATH-default grok version below the planned version fail that attempt. -- Catalog desktop EXE install uses the separate protected package bridge and - closed product action; this does not authorize CLI tools. There is no generic - `ShellExecute` of a renderer/download path from Bob. Launch of an - observer-proven closed identity uses - `launch_trusted_windows_exe_as_user` and is not an install bypass. - -### 4. Validation & Error Matrix - -| Condition | Required result | -| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| Formal elevated Windows direct CLI/Auth execution | Fail before a user process; dedicated lifecycle helper actions are separate. | -| Formal elevated Windows Grok Build lifecycle | Closed `grok-tool` helper; no elevated fallback | -| Grok npm helper has no plan, `@latest`, or unknown registry | Fail closed; no npm child process | -| Development Windows Grok npm uses `@xai-official/grok@1.2.3` or `@latest` | Contract regression; resolve live exact version first | -| npm 12+ LocalProcess omits `--allow-scripts=@xai-official/grok` | postinstall blocked; treat as failure even if npm exit 0 | -| npm exit 0 but PATH-default grok is still below the planned version | Fail that registry attempt; do not report Agent succeeded | -| Windows product has no matching `grok-win32-*` package/integrity or no registry matches both hashes | Produce no helper plan; no npm child process | -| OpenCode Windows x64 ProductName/relative EXE/signer is reviewed | Admit current-user NSIS handoff; ARM64 remains unsupported | -| OpenCode Windows ProductName/relative EXE/signer is empty | `windows_exe_install_admitted` rejects download and install; do not claim supported | -| OpenCode helper product is admitted but scan relatives omit `@opencode-aidesktop` | Inventory miss after a real current-user install; helper admission is not scan identity | -| Helper argv contains URL/path/shell string/free tool name | Contract test fails; no child process | -| Helper gains OpenCode or a generic tool/Auth command | Architecture regression; Claude's closed action has independent identity/plan tests. | -| Generic Tooling lifecycle | Grok remains its only writable CLI; dedicated Claude is not a generic endpoint expansion. | - -### 5. Good/Base/Bad Cases - -- Good: Grok uses generic Tooling, Claude uses its dedicated CLI owner, and - OpenCode stays Desktop-only; none runs a user CLI in the elevated parent. -- Good: development Windows LocalProcess npm 12 adds - `--allow-scripts=@xai-official/grok` and rereads PATH-default `grok --version` - before reporting success. -- Base: formal Windows CLI-based Auth stays unavailable, while admitted - lifecycle actions can use their closed helpers when Explorer is available. -- Bad: `fyagent-user-helper.exe run --cmd `. -- Bad: helper npm install without a host plan, or with `@latest`. -- Bad: development Windows LocalProcess `npm i -g @xai-official/grok@1.2.3`, - or treating npm exit 0 as success while grok postinstall was blocked or the - PATH-default version did not change. -- Bad: claim OpenCode Windows is supported while identity fields are empty, or - treat helper product `opencode` as proof that `OpenCode/OpenCode.exe` is the - installed folder. - -### 6. Tests Required - -- Existing `formal_windows_cli_boundary_is_fail_closed_without_a_native_runtime` - remains green: non-Grok tools stay fail-closed; Grok uses - `OrdinaryUserHelper` on formal Windows and `LocalProcess` only on - development builds. -- Direct CLI/Auth paths map elevated failures to - `interactive_user_unavailable` / `executor_not_implemented`; Claude lifecycle - tests separately prove its closed helper route. -- Negative scan: no generic CLI/Auth helper verb, no path/URL argv, no raw - stdout DTO. Closed `grok-tool` and `claude-tool` actions are not generic execution. -- Windows product-host tests admit only `grok-win32-x64`/`grok-win32-arm64`, - reject absent platform integrity, and prove the 80-byte helper control - carries, besides fixed framing/version bytes, only exact package version, - registry index and the allow-scripts bit. The helper does not resolve - optional-package metadata. -- LocalProcess Grok tests must keep live install argv on the resolved version - (not `1.2.3`), add `--allow-scripts=@xai-official/grok` only for npm ≥ 12, - preserve an already-present allow-scripts flag, and treat the npm - "install scripts blocked" warning as failure. -- Bob/Alice/UAC HIL remains unverified residual risk. - -### 7. Wrong vs Correct - -#### Wrong +## Agent-specific Windows runtime routing -```text -elevated parent -> helper argv includes installer URL or shell command -helper -> raw child stdout back to renderer -development Windows LocalProcess -> npm i -g @xai-official/grok@1.2.3 -npm exit 0 / "changed 3 packages" -> Agent succeeded -``` - -#### Correct - -```text -formal elevated Windows direct CLI/Auth -> interactive_user_unavailable -installer helper -> exact Codex MSIX, Agent EXE, Grok tool, or Claude tool action -Grok Build lifecycle -> grok-tool helper; no elevated fallback -Claude lifecycle -> claude-tool helper; no Auth verbs or generic command argv -development Windows LocalProcess -> live @xai-official/grok@ - + npm 12 --allow-scripts=@xai-official/grok - + PATH-default grok --version reaches the planned version -``` - -## Scenario: Inventory parent registry enumeration rights - -### 1. Scope / Trigger - -- Trigger: Windows Agent inventory enumerates Uninstall/App Paths children. - Opening those parents query-value-only and then calling subkey enumeration - fails on real hives and collapses supported products to `unknown`. Access- - mask ownership is `windows_runtime/registry.rs`; install-state projection - is [External Agent P0 Safety](./external-agent-p0.md). - -### 2. Signatures - -```text -RegistryRights { query_value, enumerate_subkeys, create_subkey, set_value } - -READ_VALUES = query # enumerated children -UPDATE_VALUES = query + set # FyAgent Environment/Run writes -TRAVERSE = query + enumerate # intermediate fixed components -INVENTORY_PARENT_READ = query + enumerate, no create/set - # Uninstall / App Paths parent leaves - -open_shell_user(Uninstall | AppPaths) -> INVENTORY_PARENT_READ -open_machine(Uninstall | AppPaths, Registry32 | Registry64) -> INVENTORY_PARENT_READ -enum_keys(parent) -> child names -open_child(validated name) -> READ_VALUES -``` - -Win32 mask for `INVENTORY_PARENT_READ`: `KEY_QUERY_VALUE | -KEY_ENUMERATE_SUB_KEYS` plus the requested `KEY_WOW64_*` view. Never -`KEY_CREATE_SUB_KEY` or `KEY_SET_VALUE` on this path. - -Keep `TRAVERSE` and `INVENTORY_PARENT_READ` as separate constants even if -the current bit mask is identical, so a later query-only change cannot -silently downgrade the inventory leaf. - -### 3. Contracts - -- Intermediate fixed components use `TRAVERSE`. Inventory parent leaves use - `INVENTORY_PARENT_READ`. Caller-controlled child names are length/charset - validated, then opened `READ_VALUES`. -- Optional parent `NotFound` is absence and does not mark the aggregate - incomplete. A rejected registry-link on an optional parent is also absence: - WOW64 shared keys such as machine `App Paths` open as `SymbolicLinkValue` - under `REG_OPTION_OPEN_LINK` in the 32-bit view, which is the same location - already enumerated in the 64-bit view. The link is never followed. Access - (including raw OS access-denied), enumeration, bound, or frozen - Shell-context errors keep the aggregate incomplete. -- Registry values remain hints. They are never executed. Link rejection and - no-follow reopen are unchanged. -- This capability does not add WinGet, PowerShell, a second scanner, or - writable Uninstall/App Paths. - -### 4. Validation & Error Matrix - -| Condition | Required result | -| --------------------------------------------------- | ----------------------------------------------------------------------------- | -| Parent opened `READ_VALUES` then `enum_keys` | Real hive access fails; inventory `unknown` | -| Optional parent missing | Absence; remaining views may still be complete | -| Optional parent is a rejected WOW64 shared-key link | Absence; the 64-bit view still enumerates that location; link is not followed | -| Parent/child access, bound, or Shell drift | Incomplete aggregate; no false `not_installed` | -| Child name fails length/charset validation | Skip/reject that child; do not open by raw string | -| Parent or child granted create/set | Contract failure | - -### 5. Good/Base/Bad Cases - -- **Good:** Alice HKU and machine 32/64 Uninstall/App Paths open with - query+enumerate, children stay query-only, complete empty views project - `not_installed`. -- **Base:** Environment/Run keep their existing query/set FyAgent-policy - rights; inventory does not reuse `UPDATE_VALUES`. -- **Bad:** `KEY_READ` convenience, WinGet, or treating a raw OS access-denied - as “no software installed”. - -### 6. Tests Required - -- `registry.rs`: parent leaf records `INVENTORY_PARENT_READ`; mask has - enumerate without create/set; children stay `READ_VALUES`. Rejected WOW64 - shared-key `SymbolicLinkValue` is classified separately from raw OS - access-denied. -- `inventory.rs`: complete/no-candidate keeps fresh destinations; - incomplete discovery is `Unknown` + `native_projection_unavailable` with - ineligible destinations. -- Native Alice HKU / Wow6432Node HIL remains unexecuted residual evidence. - -### 7. Wrong vs Correct - -#### Wrong - -```rust -open(parent, READ_VALUES)?; -enum_keys(parent)?; // ACCESS DENIED on real Uninstall/App Paths -``` - -#### Correct - -```rust -open(parent, INVENTORY_PARENT_READ)?; -for name in enum_keys(parent)? { - open_child(validate(name), READ_VALUES)?; -} -``` +[Windows Agent Runtime Security](./windows-agent-runtime-security.md) owns +observer-proven desktop EXE launch as Alice, closed Claude/Grok helper actions, +development Grok LocalProcess parity, and Uninstall/App Paths parent rights. +The shell-user contract above remains the prerequisite authority for frozen +Alice identity, hidden paths, elevation and Explorer COM availability. diff --git a/.trellis/tasks/09-14-audit-ahead-main/check.jsonl b/.trellis/tasks/09-14-audit-ahead-main/check.jsonl new file mode 100644 index 000000000..b50f721fc --- /dev/null +++ b/.trellis/tasks/09-14-audit-ahead-main/check.jsonl @@ -0,0 +1,20 @@ +{"file":".trellis/spec/backend/index.md","reason":"Verify owner routing, file-size policy and backend full-scope checks"} +{"file":".trellis/spec/backend/claude-code-cli.md","reason":"Verify live npm and platform execution assertions remain executable"} +{"file":".trellis/spec/backend/external-agent-lifecycle.md","reason":"Verify source/identity failures are consumed without duplicate authority"} +{"file":".trellis/spec/backend/external-agent-sources.md","reason":"Verify live npm, source and closed identity contracts are complete"} +{"file":".trellis/spec/backend/task-runner-contract.md","reason":"Verify canonical task API still routes to focused owners"} +{"file":".trellis/spec/backend/trellis-prearchive-gate.md","reason":"Verify exact active-task/session exclusion and postarchive removal"} +{"file":".trellis/spec/backend/supported-platform-governance.md","reason":"Verify identity inventory, snapshot and dependency-free checker boundaries"} +{"file":".trellis/spec/backend/native-task-runner.md","reason":"Verify foreground/native child execution and platform ownership"} +{"file":".trellis/spec/backend/windows-msvc-cross-diagnostic.md","reason":"Verify optional diagnostics do not become acceptance gates"} +{"file":".trellis/spec/backend/windows-runtime-security.md","reason":"Verify general shell-user runtime remains focused and complete"} +{"file":".trellis/spec/backend/windows-agent-runtime-security.md","reason":"Verify Agent helper, trusted launch and registry rights remain closed"} +{"file":".trellis/spec/backend/github-ci-workflow.md","reason":"Verify local evidence maps to required remote checks"} +{"file":".trellis/spec/backend/github-push-commit-policy.md","reason":"Verify push fallback stays isolated from Required CI"} +{"file":".trellis/spec/backend/github-merge-governance.md","reason":"Verify PR and merge-readiness governance"} +{"file":".trellis/spec/frontend/index.md","reason":"Verify all affected renderer owners and quality checks"} +{"file":".trellis/spec/frontend/user-facing-copy.md","reason":"Verify concise copy does not erase safety or uncertainty"} +{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"Verify warning guard, browser and performance evidence"} +{"file":".trellis/tasks/09-14-audit-ahead-main/research/spec-audit.md","reason":"Audit checklist and measured SPEC size findings"} +{"file":".trellis/tasks/09-14-audit-ahead-main/research/commit-audit.md","reason":"Verify every baseline commit and local gate has explicit review evidence"} +{"file":".trellis/tasks/09-14-audit-ahead-main/research/merge-handoff.md","reason":"Verify task closure is not confused with later remote merge evidence"} diff --git a/.trellis/tasks/09-14-audit-ahead-main/design.md b/.trellis/tasks/09-14-audit-ahead-main/design.md new file mode 100644 index 000000000..321aa8317 --- /dev/null +++ b/.trellis/tasks/09-14-audit-ahead-main/design.md @@ -0,0 +1,72 @@ +# Design — Ahead-of-main audit and integration + +## 1. Baseline and ownership + +The immutable review baseline is the six commits in +`origin/main..dev/laiyongjie` captured on 2026-09-14. The review treats them as +two coherent workstreams rather than one feature: + +1. **Official npm CLI lifecycle** — live package metadata, exact-version plans, + PATH-default discovery, npm 12 script policy and Windows ordinary-user or + development execution. +2. **Concise Renderer surfaces** — copy/layout ownership, warning guards, + browser scroll/density evidence and repository-wide contract snapshots. + +No history rewrite is required. Corrections land as new review commits so the +PR preserves the original implementation and the audit trail. + +## 2. SPEC decomposition + +Trellis injects at most 32768 bytes per referenced SPEC by default. Three +changed owner files exceed that limit and therefore cannot remain the only +source for sub-agent execution: + +| Current owner | Problem | Target ownership | +| --- | --- | --- | +| `external-agent-lifecycle.md` | Core inventory/jobs and product/source/desktop identity are mixed; npm detail is duplicated | Keep inventory, capability and job orchestration in the original file. Move product source resolution, shared Claude/Grok npm admission, closed desktop identity and product-specific scan material to `external-agent-sources.md`. Keep only Claude-specific owner/prefix/execution rules in `claude-code-cli.md`. | +| `task-runner-contract.md` | Public task API, prearchive validation, supported-platform identity seals and host diagnostics are one 50 KiB document | Move supported-platform source/asset inventory and whole-repository snapshot rules to `supported-platform-governance.md`; leave a concise routing section in the task-runner owner. | +| `windows-runtime-security.md` | Shell-user authority and Agent-specific helper/registry scenarios are one 50 KiB document | Keep frozen Explorer-user/path/elevation fundamentals in the original file. Move trusted Agent EXE launch, closed CLI helper and inventory-parent registry scenarios to `windows-agent-runtime-security.md`. | + +The original filenames stay valid entry points. Backend `index.md` names each +new semantic owner. Cross-references replace duplicated implementation detail. +Every resulting owner must remain below the per-file injection limit. + +## 3. Review model + +Review proceeds in four layers: + +1. **Commit intent** — compare each commit message and task artifact with its + actual diff. +2. **Executable contract** — trace live npm/package discovery and Renderer + state/layout behavior through implementation and tests. +3. **Repository gates** — run formatter, type/lint, Rust, unit, browser, + performance and contract checks using repository-owned commands. +4. **Remote integration** — create/update one PR, inspect exact failing jobs, + make scoped fixes, then merge only when GitHub reports all required checks + successful. + +## 4. Compatibility and rollback + +- No serialized DTO, command name, persisted schema or product capability is + intentionally changed by the SPEC split. +- Spec moves use ordinary Markdown files and relative links; no redirect file + is deleted. +- If a split causes context or link validation failure, restore the moved + section to its prior owner and narrow the split rather than increasing + `max_file_bytes`. +- CI fixes must preserve existing performance and security budgets. A failing + native-platform check is repaired at the owning code/test boundary, not + bypassed in workflow configuration. + +## 5. Merge shape + +Use a small number of coherent commits after the six baseline commits: + +1. SPEC ownership/decomposition plus any directly required link/contract tests. +2. Code or test fixes found by the audit, if any. +3. Trellis archive commit. +4. Trellis journal commit. + +The PR targets `main` from the long-lived `dev/laiyongjie` branch. Merge method +and queue usage follow `github-merge-governance.md` and current repository +settings observed through GitHub CLI. diff --git a/.trellis/tasks/09-14-audit-ahead-main/implement.jsonl b/.trellis/tasks/09-14-audit-ahead-main/implement.jsonl new file mode 100644 index 000000000..663473aa8 --- /dev/null +++ b/.trellis/tasks/09-14-audit-ahead-main/implement.jsonl @@ -0,0 +1,20 @@ +{"file":".trellis/spec/backend/index.md","reason":"Backend semantic owners, maintenance rules and full-scope quality gate"} +{"file":".trellis/spec/backend/claude-code-cli.md","reason":"Executable live npm, discovery and ordinary-user CLI lifecycle contract"} +{"file":".trellis/spec/backend/external-agent-lifecycle.md","reason":"Action, inventory, job and recovery owner after source/identity decomposition"} +{"file":".trellis/spec/backend/external-agent-sources.md","reason":"Shared live npm, product source and closed desktop identity owner"} +{"file":".trellis/spec/backend/task-runner-contract.md","reason":"Public mise API and canonical task composition after focused owner splits"} +{"file":".trellis/spec/backend/trellis-prearchive-gate.md","reason":"Direct-session active-task exclusion and archive transition owner"} +{"file":".trellis/spec/backend/supported-platform-governance.md","reason":"Platform-sensitive identity seals and whole-repository snapshot owner"} +{"file":".trellis/spec/backend/native-task-runner.md","reason":"Foreground process, Windows MSVC and macOS signed development owner"} +{"file":".trellis/spec/backend/windows-msvc-cross-diagnostic.md","reason":"Optional macOS cross-compile diagnostic and evidence boundary"} +{"file":".trellis/spec/backend/windows-runtime-security.md","reason":"Frozen Explorer-user and general Windows shell-runtime owner"} +{"file":".trellis/spec/backend/windows-agent-runtime-security.md","reason":"Agent-specific trusted launch, helper and inventory access owner"} +{"file":".trellis/spec/backend/github-ci-workflow.md","reason":"Required CI jobs, aggregation and failure semantics"} +{"file":".trellis/spec/backend/github-push-commit-policy.md","reason":"Push-only commit range and topology-aware subject policy"} +{"file":".trellis/spec/backend/github-merge-governance.md","reason":"PR readiness, Trellis lifecycle and merge policy"} +{"file":".trellis/spec/frontend/index.md","reason":"Frontend owner routing and renderer/browser quality gate"} +{"file":".trellis/spec/frontend/user-facing-copy.md","reason":"Concise secondary-surface copy and evidence boundaries"} +{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"Renderer warning, browser and performance evidence contracts"} +{"file":".trellis/tasks/09-14-audit-ahead-main/research/spec-audit.md","reason":"Measured size, duplication and review-risk findings for this audit"} +{"file":".trellis/tasks/09-14-audit-ahead-main/research/commit-audit.md","reason":"Six-commit code-path review and completed local evidence"} +{"file":".trellis/tasks/09-14-audit-ahead-main/research/merge-handoff.md","reason":"Post-archive exact-head PR, queue and main readback boundary"} diff --git a/.trellis/tasks/09-14-audit-ahead-main/implement.md b/.trellis/tasks/09-14-audit-ahead-main/implement.md new file mode 100644 index 000000000..7afd2ed57 --- /dev/null +++ b/.trellis/tasks/09-14-audit-ahead-main/implement.md @@ -0,0 +1,49 @@ +# Implementation plan + +## Phase A — establish evidence + +- [x] Capture branch status, six-commit range, changed-file inventory and any existing PR. +- [x] Review the two archived/related Trellis task sets and compare acceptance criteria with the actual commits. +- [x] Inspect all changed SPEC hunks, file sizes and semantic ownership; record findings in `research/spec-audit.md`. + +## Phase B — improve SPEC ownership + +- [x] Split `external-agent-lifecycle.md` into core lifecycle and product/source owner while preserving links and seven-section contracts. +- [x] Split supported-platform identity/snapshot governance out of `task-runner-contract.md`. +- [x] Split Agent-specific Windows helper/registry scenarios out of `windows-runtime-security.md`. +- [x] Update `backend/index.md` and affected cross-references; run link/context validation and keep every owner under 32768 bytes. +- [x] Compress duplicated Claude/Grok npm and Renderer concision prose without removing validation, error, security or evidence boundaries. + +## Phase C — code and test audit + +- [x] Trace live npm metadata/argv, PATH-default selection, npm 12 allow-scripts, Windows LocalProcess and post-install readback through code/tests. +- [x] Trace secondary-page copy/layout changes, React warning guard lifecycle, scroll ownership, performance config and repository snapshot tests. +- [x] Fix only demonstrated defects; add or strengthen regression assertions for every code fix. No product-code defect was demonstrated; the required corrections were SPEC ownership and routing. + +## Phase D — local gates + +- [x] `task.py validate` with curated context. +- [x] `mise run format:check` and `mise run check:contracts`. +- [x] `mise run check` (full repository gate). +- [x] Run focused browser/performance commands required by the changed owners, preserving real timing configuration. +- [x] Re-run the exact failing command after every repair and finish with a clean full-scope pass. No gate failed; focused and full-scope passes are recorded in `research/commit-audit.md`. + +## Phase E — commit, PR and merge + +- [ ] Review final diff and write coherent work commit(s) without amending the six baseline commits. +- [ ] Archive this Trellis task and record the session journal after work commits. +- [ ] Run canonical post-archive contracts/readback, freeze the exact reviewed head, and leave a clean worktree for merge handoff. + +## Post-archive merge executor (outside task-closure evidence) + +- Push the frozen `dev/laiyongjie` head and create/update a PR to `main`. +- Inspect every required PR check. A new fix commit invalidates the prior exact-head readiness and must repeat the applicable local/Trellis lifecycle before auto-merge is re-enabled. +- Enable auto-merge only with `--match-head-commit `; never use `--admin`, direct `main` push, squash or rebase. +- Require the Merge Queue `merge_group` `CI / Required` authority, then read back the resulting remote `main` merge SHA. + +## Stop/rollback gates + +- A proposed fix that changes public behavior beyond the six-commit intent is out of scope. +- A platform check that cannot be proven on the current host remains explicit + residual evidence; it is never converted into a passing mock claim. +- Do not merge with required checks pending, skipped unexpectedly or failing. diff --git a/.trellis/tasks/09-14-audit-ahead-main/prd.md b/.trellis/tasks/09-14-audit-ahead-main/prd.md new file mode 100644 index 000000000..04ae80bb6 --- /dev/null +++ b/.trellis/tasks/09-14-audit-ahead-main/prd.md @@ -0,0 +1,36 @@ +# 审计本地领先提交并合并 main + +## Goal + +审计 dev/laiyongjie 相对 origin/main 的全部领先提交,重构与补充相关 Trellis SPEC,完成全量验证、PR 创建、CI 修复与合并。 + +## Requirements + +- 审计基线固定为任务开始时的 `origin/main..dev/laiyongjie` 六个提交,区分两条既有工作流: + 1. Claude/Grok CLI npm 最新版本解析、发现与 Windows 开发态执行修复; + 2. 二级页面精简、Renderer/浏览器测试与仓库契约稳定性修复。 +- 不新增产品能力,不改变已评审的用户流程;发现缺陷时只做与上述提交直接相关的修复。 +- 逐项核对代码、测试、任务归档与 `.trellis/spec/**` 是否一致,消除同一契约在多个 SPEC 中的重复所有权。 +- 对超过 Trellis 默认 `context_injection.max_file_bytes=32768` 的受影响 SPEC 做语义拆分;保留稳定入口与显式交叉引用,不通过提高注入上限或删除安全/回滚契约解决。 +- 受影响的基础设施、跨层和平台契约必须保留可执行签名、验证/错误矩阵、Good/Base/Bad、测试断言和 Wrong/Correct 示例。 +- 更新 backend/frontend 索引及所有本次变更涉及的有效 SPEC 引用;历史归档材料仅在出现失效的活动路径时调整,不做无意义批量改写。 +- 运行与 CI 对齐的完整质量门禁;任何失败都要定位根因并补回归测试或契约,而不是放宽预算、跳过检查或隐藏警告。 +- Trellis 任务在“可交给远端合并执行器”的边界关闭:完成直接会话预归档、工作提交、任务归档、journal、post-archive 校验与 exact-head 只读复核。 +- 任务归档后,从 `dev/laiyongjie` 向 `main` 创建 PR,等待所有必需检查通过,按仓库合并治理完成合并;若 CI 失败,修复后重新走适用的本地/Trellis readiness 流程。 +- 合并后确认远端 `main` 包含 PR 结果,并保持受保护的 `dev/laiyongjie` 分支可继续使用。远端 PR/队列/合并读回属于 post-archive merge executor 证据,不伪装成任务归档前已经完成。 + +## Acceptance Criteria + +- [ ] `origin/main..HEAD` 的每个提交和所有改动文件均已审查,审查结论写入任务材料。 +- [ ] 受影响 SPEC 的语义所有权清晰;所有被任务上下文引用的 owner 文档不超过 32768 bytes。 +- [ ] 新拆分文档已加入 backend/frontend 索引,原入口保留准确路由,不存在相互矛盾或复制粘贴的规则。 +- [ ] Claude/Grok live npm、PATH-default 发现、npm 12 allow-scripts、Windows LocalProcess、Renderer 精简与测试稳定性均有对应自动化断言。 +- [ ] Trellis 任务 `validate`、契约检查、全仓检查和本次变更对应的浏览器/性能门禁通过。 +- [ ] 工作提交、任务归档和 journal 顺序符合 Trellis;post-archive 校验通过,exact PR head 已冻结且工作树干净。 +- [ ] `research/merge-handoff.md` 明确记录 post-archive PR、exact-head CI、Merge Queue 和最终 `main` 读回要求,且这些步骤不会被误写为任务归档前证据。 + +## Notes + +- 用户在本轮请求中已明确授权完成审计、SPEC 调整、PR、CI 修复和合并,不需要另行扩大功能范围。 +- 初始工作树干净;`dev/laiyongjie` 相对 `origin/dev/laiyongjie` 领先四个提交,相对 `origin/main` 领先六个提交。 +- 用户级工作在远端 merge/readback 后结束;Trellis 任务按 `github-merge-governance.md` 的强制顺序先归档,再由同一会话继续执行 post-archive merge handoff。 diff --git a/.trellis/tasks/09-14-audit-ahead-main/research/commit-audit.md b/.trellis/tasks/09-14-audit-ahead-main/research/commit-audit.md new file mode 100644 index 000000000..df6b0420f --- /dev/null +++ b/.trellis/tasks/09-14-audit-ahead-main/research/commit-audit.md @@ -0,0 +1,85 @@ +# Ahead-of-main commit audit — 2026-09-14 + +## Reviewed baseline + +The review baseline is the six commits that were present in +`origin/main..dev/laiyongjie` when this task started: + +| Commit | Intent | Review result | +| --- | --- | --- | +| `223fbbc0` | Resolve Claude/Grok npm latest metadata at runtime instead of compiling reviewed manifests. | Implementation uses a concrete semver and root/current-platform integrity before composing executable argv. Registry tags never become install authority. The principal defect was documentation ownership: shared npm source mechanics were repeated across lifecycle, Claude and Windows specs. | +| `b2cdc098` | Apply the live Grok npm plan and npm 12 script policy on development Windows. | LocalProcess consumes the same exact plan, derives policy from the npm executable that actually runs, rejects blocked install scripts and requires PATH-default version readback. The `1.2.3` value remains only a command-shape fixture. No product-code defect was demonstrated. | +| `379bb0d1` | Simplify secondary Renderer pages and close warning/layout/browser validation gaps. | Copy and layout changes preserve destructive-action, uncertainty, secret and recovery boundaries. React warning guards survive restore-mock lifecycle; scroll/density tests exercise native wheel/keyboard behavior, responsive re-entry and draft preservation. | +| `ee475a78` | Stabilize the full-repository supported-platform snapshot test. | The integration test captures one tracked-file snapshot, keeps zero-findings and inspected-file-count assertions, and gives only that repository integration case a bounded 15-second watchdog. It does not loosen a production scanner or product performance budget. | +| `9a6ceab4` | Archive the prior concise-renderer Trellis task. | Archive contents match the implementation commit and retain its check/research artifacts. No active-task path was left behind. | +| `ce6e4916` | Record the prior work journal. | Journal ordering follows the work commit and task archive. No code or SPEC authority is hidden in the journal commit. | + +## Code-path trace + +### Shared live npm lifecycle + +- `services/tooling/grok_npm.rs` parses a live `/latest` document, requires a + concrete version and current-platform optional package, and admits a registry + only when root and platform SHA-512 values match the same manifest. +- `GrokNpmInstallPlan::npm_argv_for` composes `package@`; + neither `@latest` nor the command-shape fixture is executable authority. +- Claude and Grok use the same compact exact plan while retaining distinct + package, scoped-registry, script-policy and post-install verification. +- Windows helper execution derives the npm major from the executable that will + run. npm 12+ receives only the reviewed package-specific allow-scripts flag. +- Development Grok LocalProcess rejects blocked-script output and does not + report success until the PATH-default Grok version reaches the plan. +- macOS discovery consumes login/process PATH plus product environment, does + not walk manager internals, and selects the PATH-default install when several + copies are visible. + +Focused Rust evidence: `mise run rust:test -- grok_npm` completed successfully; +the primary library ran 9/9 matching manifest, integrity, concrete-version and +script-policy assertions, and every filtered Rust test binary exited cleanly. + +### Concise Renderer surfaces + +- Skills and MCP retain assignment switches, trust/overwrite dialogs, source + visibility and secret redaction while removing repeated explanatory cards. +- Health removes redundant summary prose without removing live status, + filtering, stop semantics, configuration routes or error evidence. +- Auth removes duplicated labels while preserving account identity, connection + identity, request source and device-code recovery. +- Prompt/Memory retain truthful native-only empty/error states and reachable + controls; no seeded or fabricated data was introduced. +- The React warning guard is tested across consecutive cases with the actual + setup lifecycle, so `restoreMocks` cannot silently disable it. +- Responsive/scroll tests use real wheel and keyboard movement, multiple + Chromium viewports, WebKit, narrow/wide re-entry, enlarged text and draft + node preservation. + +Focused Renderer evidence: + +- `mise run check:frontend`: typecheck, ESLint, formatting, 187 unit files / + 1649 tests and desktop visual preflight passed. +- `mise run test:browser`: production build plus 586/586 browser tests passed + across Chromium and WebKit. The initial production-route performance boot + checks also passed. + +## SPEC findings and resolution + +Four changed owner documents exceeded, or were too close to, Trellis's default +32768-byte injection boundary. They mixed stable public contracts with +platform-, source- or workflow-specific details. Raising the limit was rejected. + +The review introduced focused owners for product sources/desktop identity, +prearchive session proof, supported-platform governance, native host task +execution, optional Windows-MSVC diagnostics, Windows Agent execution and +branch-push commit policy. Existing filenames remain valid routing entry points. + +After decomposition, the largest affected owner is +`github-ci-workflow.md` at 29922 bytes. All local Markdown links resolve, and +the active task's curated implement/check contexts validate with 18 entries +each and no truncation warning. + +## Remaining evidence boundary + +Portable/macOS checks do not prove native Windows registry views, +WinVerifyTrust, Explorer-user process identity, Visual Studio environment +loading, installer/UAC behavior or Windows packaging. Those remain explicit +matching-host CI/HIL evidence and are not represented as locally passed mocks. diff --git a/.trellis/tasks/09-14-audit-ahead-main/research/merge-handoff.md b/.trellis/tasks/09-14-audit-ahead-main/research/merge-handoff.md new file mode 100644 index 000000000..bfe638464 --- /dev/null +++ b/.trellis/tasks/09-14-audit-ahead-main/research/merge-handoff.md @@ -0,0 +1,52 @@ +# Post-archive merge handoff + +## Boundary + +This Trellis task closes at the exact-head readiness boundary required by +`github-merge-governance.md`: implementation/SPEC convergence, direct-session +prearchive, work commit, task archive, journal, post-archive contracts, final +diff/readback and clean worktree. + +The same interactive session then continues as the post-archive merge executor. +Remote PR, exact-head hosted CI, Merge Queue and final `main` readback are later +evidence levels and must not be represented as facts in the archive commit. + +## Required sequence + +```text +frozen reviewed HEAD + -> push dev/laiyongjie + -> create/update PR to main + -> inspect exact-head hosted checks + -> gh pr merge --auto --match-head-commit + -> Merge Queue creates merge_group against latest main + -> CI / Required passes for merge_group + -> one merge commit enters main + -> read back remote main SHA and ancestry +``` + +## Failure handling + +- A failing PR or merge-group check is inspected at the exact job/step. Fixes + stay scoped to the audited six-commit intent and receive regression evidence. +- Any new commit invalidates the previous handoff. Re-run the applicable full + local gate, update durable SPEC/task evidence if behavior changed, archive a + follow-up Trellis task when required, and enable auto-merge only for the new + exact head. +- Never use `--admin`, direct push to `main`, squash/rebase, temporary merge + policy changes, skipped required checks or a stale head guard. +- If `origin/main` advances, Merge Queue—not manual update-branch churn—is the + latest-main authority unless GitHub reports a real conflict that requires a + branch fix. + +## Final readback + +The merge executor records: + +- PR number and URL; +- frozen PR head SHA; +- exact-head required-check result; +- merge-group `CI / Required` result; +- resulting `origin/main` merge SHA; +- ancestry/readback proving the merged branch result is contained by main; +- confirmation that `dev/laiyongjie` remains available. diff --git a/.trellis/tasks/09-14-audit-ahead-main/research/spec-audit.md b/.trellis/tasks/09-14-audit-ahead-main/research/spec-audit.md new file mode 100644 index 000000000..06a4aca70 --- /dev/null +++ b/.trellis/tasks/09-14-audit-ahead-main/research/spec-audit.md @@ -0,0 +1,80 @@ +# Initial SPEC audit — 2026-09-14 + +## Baseline + +`dev/laiyongjie` is six commits ahead of `origin/main` and four commits ahead of +`origin/dev/laiyongjie`. The worktree was clean before this audit task was +created. The six commits comprise two npm/tooling commits, the Renderer +refactor, a repository snapshot-test stabilization commit, the prior task +archive, and its journal record. + +## Injection-size findings + +Trellis defaults `context_injection.max_file_bytes` to 32768 bytes. Current +changed SPEC sizes include: + +| File | Bytes | Finding | +| --- | ---: | --- | +| `backend/external-agent-lifecycle.md` | 40882 | Must split; product source/identity is separable from inventory/job orchestration. | +| `backend/task-runner-contract.md` | 50971 | Must split; supported-platform identity seals are a cohesive repository-governance owner. | +| `backend/windows-runtime-security.md` | 50781 | Must split; Agent helper and registry scenarios are separable from the frozen Shell-user core. | +| `backend/claude-code-cli.md` | 14155 | Safe size; current live npm details fit the mandatory seven-section form. | +| Largest changed frontend owner | 19954 | Safe size; review for duplication, not size pressure. | + +Raising the configured limit would only hide truncation risk and is rejected. + +## Duplication findings to verify + +- Live npm resolution and Windows LocalProcess details are repeated in + `claude-code-cli.md`, `external-agent-lifecycle.md` and + `windows-runtime-security.md`. One file must own mechanics; the others should + state only orchestration/platform boundaries and link to the owner. +- Concise secondary-surface rules appear in user copy, visual language, + surfaces, Skills/MCP and Prompts/Memory. Feature owners should state their + concrete projection; shared rationale belongs only in copy/visual/surface + owners. +- The repository-wide snapshot timeout belongs to test/governance behavior, + not product performance policy. Its SPEC must preserve zero-findings and + inspected-file assertions while separating runner scheduling from scanner + budgets. + +## Review risks + +- A live npm resolver must never turn a registry tag into npm argv; the install + plan must carry a concrete version and matching root/platform integrity. +- macOS discovery must not walk manager internals and must select the PATH + default when multiple copies exist. +- Development Windows must not execute the command-shape `1.2.3` fixture and + npm 12 must receive the narrow Grok allow-scripts flag. +- React warning guards can be silently disabled by `restoreMocks`; setup tests + must exercise consecutive cases against the real lifecycle. +- Browser/performance configuration must not retain traces in accepted timing + runs or replace real motion/scroll evidence with unit mocks. + +## Final decomposition + +The review resolved the oversize/duplication findings without changing +`context_injection.max_file_bytes`: + +| Owner | Final bytes | Responsibility | +| --- | ---: | --- | +| `external-agent-lifecycle.md` | 21425 | Action legality, normalized inventory, opaque capabilities, jobs, deployment and recovery. | +| `external-agent-sources.md` | 14050 | Product release sources, shared Claude/Grok exact npm admission, artifact bounds and closed Desktop identity. | +| `task-runner-contract.md` | 26189 | Public mise API, effects, validated transport, composition and canonical checks. | +| `trellis-prearchive-gate.md` | 5775 | Exact direct-session active-task exclusion before archive. | +| `supported-platform-governance.md` | 8246 | Platform-sensitive source/raster identities and one-snapshot repository scans. | +| `native-task-runner.md` | 13856 | Foreground trees, Windows MSVC child environment and macOS signed development runner. | +| `windows-msvc-cross-diagnostic.md` | 7656 | Optional macOS cross-compile diagnostic and its non-acceptance boundary. | +| `windows-runtime-security.md` | 24434 | Frozen Explorer-user authority, hidden paths/elevation and general HTTP COM launch. | +| `windows-agent-runtime-security.md` | 11154 | Trusted Agent EXE launch, closed Claude/Grok helper routes, LocalProcess parity and registry enumeration rights. | +| `github-ci-workflow.md` | 29922 | PR/merge-group Required CI classification, domains and aggregation. | +| `github-push-commit-policy.md` | 5588 | Push-only unreachable-base fallback and topology-aware subject policy. | + +All affected owner documents are below 32768 bytes with durable routing from +`backend/index.md`. A local Markdown link scan reported zero missing targets; +the curated task implement/check contexts validate without truncation. + +Shared live npm source authority is now singular: `external-agent-sources.md` +owns metadata/mirror/integrity/exact-plan mechanics, Claude/Grok focused owners +own product-specific detection/execution/post-verification, and the lifecycle +owner consumes only admitted capabilities/outcomes. diff --git a/.trellis/tasks/09-14-audit-ahead-main/task.json b/.trellis/tasks/09-14-audit-ahead-main/task.json new file mode 100644 index 000000000..3ed5634b4 --- /dev/null +++ b/.trellis/tasks/09-14-audit-ahead-main/task.json @@ -0,0 +1,26 @@ +{ + "id": "audit-ahead-main", + "name": "audit-ahead-main", + "title": "审计本地领先提交并合并 main", + "description": "审计 dev/laiyongjie 相对 origin/main 的全部领先提交,重构与补充相关 Trellis SPEC,完成全量验证、PR 创建、CI 修复与合并。", + "status": "in_progress", + "dev_type": null, + "scope": "origin/main..HEAD audit; Trellis spec quality; full validation; PR and merge", + "package": null, + "priority": "P2", + "creator": "pythonrust", + "assignee": "pythonrust", + "createdAt": "2026-09-14", + "completedAt": null, + "branch": "dev/laiyongjie", + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file From 202cf8406bbf9925825f6087d4b722e873e9d835 Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:35:57 +0800 Subject: [PATCH 08/12] chore(task): archive 09-14-audit-ahead-main --- .../{ => archive/2026-09}/09-14-audit-ahead-main/check.jsonl | 0 .../{ => archive/2026-09}/09-14-audit-ahead-main/design.md | 0 .../2026-09}/09-14-audit-ahead-main/implement.jsonl | 0 .../{ => archive/2026-09}/09-14-audit-ahead-main/implement.md | 0 .../tasks/{ => archive/2026-09}/09-14-audit-ahead-main/prd.md | 0 .../2026-09}/09-14-audit-ahead-main/research/commit-audit.md | 0 .../2026-09}/09-14-audit-ahead-main/research/merge-handoff.md | 0 .../2026-09}/09-14-audit-ahead-main/research/spec-audit.md | 0 .../{ => archive/2026-09}/09-14-audit-ahead-main/task.json | 4 ++-- 9 files changed, 2 insertions(+), 2 deletions(-) rename .trellis/tasks/{ => archive/2026-09}/09-14-audit-ahead-main/check.jsonl (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-audit-ahead-main/design.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-audit-ahead-main/implement.jsonl (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-audit-ahead-main/implement.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-audit-ahead-main/prd.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-audit-ahead-main/research/commit-audit.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-audit-ahead-main/research/merge-handoff.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-audit-ahead-main/research/spec-audit.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-audit-ahead-main/task.json (93%) diff --git a/.trellis/tasks/09-14-audit-ahead-main/check.jsonl b/.trellis/tasks/archive/2026-09/09-14-audit-ahead-main/check.jsonl similarity index 100% rename from .trellis/tasks/09-14-audit-ahead-main/check.jsonl rename to .trellis/tasks/archive/2026-09/09-14-audit-ahead-main/check.jsonl diff --git a/.trellis/tasks/09-14-audit-ahead-main/design.md b/.trellis/tasks/archive/2026-09/09-14-audit-ahead-main/design.md similarity index 100% rename from .trellis/tasks/09-14-audit-ahead-main/design.md rename to .trellis/tasks/archive/2026-09/09-14-audit-ahead-main/design.md diff --git a/.trellis/tasks/09-14-audit-ahead-main/implement.jsonl b/.trellis/tasks/archive/2026-09/09-14-audit-ahead-main/implement.jsonl similarity index 100% rename from .trellis/tasks/09-14-audit-ahead-main/implement.jsonl rename to .trellis/tasks/archive/2026-09/09-14-audit-ahead-main/implement.jsonl diff --git a/.trellis/tasks/09-14-audit-ahead-main/implement.md b/.trellis/tasks/archive/2026-09/09-14-audit-ahead-main/implement.md similarity index 100% rename from .trellis/tasks/09-14-audit-ahead-main/implement.md rename to .trellis/tasks/archive/2026-09/09-14-audit-ahead-main/implement.md diff --git a/.trellis/tasks/09-14-audit-ahead-main/prd.md b/.trellis/tasks/archive/2026-09/09-14-audit-ahead-main/prd.md similarity index 100% rename from .trellis/tasks/09-14-audit-ahead-main/prd.md rename to .trellis/tasks/archive/2026-09/09-14-audit-ahead-main/prd.md diff --git a/.trellis/tasks/09-14-audit-ahead-main/research/commit-audit.md b/.trellis/tasks/archive/2026-09/09-14-audit-ahead-main/research/commit-audit.md similarity index 100% rename from .trellis/tasks/09-14-audit-ahead-main/research/commit-audit.md rename to .trellis/tasks/archive/2026-09/09-14-audit-ahead-main/research/commit-audit.md diff --git a/.trellis/tasks/09-14-audit-ahead-main/research/merge-handoff.md b/.trellis/tasks/archive/2026-09/09-14-audit-ahead-main/research/merge-handoff.md similarity index 100% rename from .trellis/tasks/09-14-audit-ahead-main/research/merge-handoff.md rename to .trellis/tasks/archive/2026-09/09-14-audit-ahead-main/research/merge-handoff.md diff --git a/.trellis/tasks/09-14-audit-ahead-main/research/spec-audit.md b/.trellis/tasks/archive/2026-09/09-14-audit-ahead-main/research/spec-audit.md similarity index 100% rename from .trellis/tasks/09-14-audit-ahead-main/research/spec-audit.md rename to .trellis/tasks/archive/2026-09/09-14-audit-ahead-main/research/spec-audit.md diff --git a/.trellis/tasks/09-14-audit-ahead-main/task.json b/.trellis/tasks/archive/2026-09/09-14-audit-ahead-main/task.json similarity index 93% rename from .trellis/tasks/09-14-audit-ahead-main/task.json rename to .trellis/tasks/archive/2026-09/09-14-audit-ahead-main/task.json index 3ed5634b4..3285bf8c6 100644 --- a/.trellis/tasks/09-14-audit-ahead-main/task.json +++ b/.trellis/tasks/archive/2026-09/09-14-audit-ahead-main/task.json @@ -3,7 +3,7 @@ "name": "audit-ahead-main", "title": "审计本地领先提交并合并 main", "description": "审计 dev/laiyongjie 相对 origin/main 的全部领先提交,重构与补充相关 Trellis SPEC,完成全量验证、PR 创建、CI 修复与合并。", - "status": "in_progress", + "status": "completed", "dev_type": null, "scope": "origin/main..HEAD audit; Trellis spec quality; full validation; PR and merge", "package": null, @@ -11,7 +11,7 @@ "creator": "pythonrust", "assignee": "pythonrust", "createdAt": "2026-09-14", - "completedAt": null, + "completedAt": "2026-09-14", "branch": "dev/laiyongjie", "base_branch": "main", "worktree_path": null, From 8de80f7e05e847ec1b3e37d05487a429339bbdd3 Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:36:03 +0800 Subject: [PATCH 09/12] chore: record journal --- .trellis/workspace/pythonrust/index.md | 5 +++-- .trellis/workspace/pythonrust/journal-2.md | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.trellis/workspace/pythonrust/index.md b/.trellis/workspace/pythonrust/index.md index 2171dd7e7..2b45d298d 100644 --- a/.trellis/workspace/pythonrust/index.md +++ b/.trellis/workspace/pythonrust/index.md @@ -8,7 +8,7 @@ - **Active File**: `journal-2.md` -- **Total Sessions**: 88 +- **Total Sessions**: 89 - **Last Active**: 2026-09-14 @@ -19,7 +19,7 @@ | File | Lines | Status | |------|-------|--------| -| `journal-2.md` | ~805 | Active | +| `journal-2.md` | ~827 | Active | | `journal-1.md` | ~1987 | Archived | @@ -30,6 +30,7 @@ | # | Date | Title | Commits | Branch | |---|------|-------|---------|--------| +| 89 | 2026-09-14 | Audit ahead-of-main commits and split SPEC owners | `20dd6537` | `dev/laiyongjie` | | 88 | 2026-09-14 | Simplify secondary pages and close validation gaps | `379bb0d113702421779e01eb4f49f4cf6c13c0aa`, `ee475a784f6995ecfd933b3a6b78fe1ae8950193` | `dev/laiyongjie` | | 87 | 2026-09-09 | Repair Grok PR 185 CI and archive superseded plans | `46e378e8`, `ac458fcb`, `98d57692` | `fix/grok-pr185-ci-closeout` | | 86 | 2026-09-07 | Prepare FyAgent 0.4.4 release | `9c058cee8ae8e29614be14fd3438662b3ff9a521` | `dev/laiyongjie` | diff --git a/.trellis/workspace/pythonrust/journal-2.md b/.trellis/workspace/pythonrust/journal-2.md index 1a768803a..9fafb759c 100644 --- a/.trellis/workspace/pythonrust/journal-2.md +++ b/.trellis/workspace/pythonrust/journal-2.md @@ -803,3 +803,25 @@ Simplified all eight renderer route families and secondary surfaces, repaired ba ### Status [OK] **Completed** + + +## Session 89: Audit ahead-of-main commits and split SPEC owners + + +**Date**: 2026-09-14 +**Task**: Audit ahead-of-main commits and split SPEC owners +**Branch**: `dev/laiyongjie` + +### Summary + +Reviewed the six commits ahead of main, decomposed oversized Trellis SPEC owners, and completed full local, browser, performance, prearchive, and contract gates for exact-head merge readiness. + +### Git Commits + +| Hash | Message | +|------|---------| +| `20dd6537` | docs(spec): split oversized contract owners | + +### Status + +[OK] **Completed** From 5be5540ccf1579310b8ccbb3861fcbc634fe3aed Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:12:44 +0800 Subject: [PATCH 10/12] test(browser): stabilize dynamic contrast sampling --- .trellis/spec/frontend/quality-guidelines.md | 17 +++++++---- .../check.jsonl | 4 +++ .../implement.jsonl | 4 +++ .../prd.md | 28 +++++++++++++++++++ .../task.json | 26 +++++++++++++++++ tests/browser/blue-themes.spec.ts | 17 ++++++++++- 6 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 .trellis/tasks/09-14-fix-merge-queue-blue-contrast/check.jsonl create mode 100644 .trellis/tasks/09-14-fix-merge-queue-blue-contrast/implement.jsonl create mode 100644 .trellis/tasks/09-14-fix-merge-queue-blue-contrast/prd.md create mode 100644 .trellis/tasks/09-14-fix-merge-queue-blue-contrast/task.json diff --git a/.trellis/spec/frontend/quality-guidelines.md b/.trellis/spec/frontend/quality-guidelines.md index c30be32a7..2246e9596 100644 --- a/.trellis/spec/frontend/quality-guidelines.md +++ b/.trellis/spec/frontend/quality-guidelines.md @@ -190,11 +190,18 @@ The 1x warm-frame p95 target is 33.4ms. Normalize only sub-nanosecond floating-p subtraction noise; never increase the frame budget or replace real motion with test-only no-animation code. Background machine load is reported, not hidden. -Static geometry/contrast assertions wait for actual settled state. Paused native -keyframes verify source/80ms press lead/252–420ms content handoff and reverse -tracks, alongside real-time mouse/keyboard/touch, interruption and resource checks. -Event dispatch/focus completion does not imply Router's state commit completed; -await the exact selected-state assertion rather than arbitrary sleeps. +Static geometry/contrast assertions wait for actual settled state. Raster +contrast helpers collect glyph geometry and foreground roles before hiding text +and capturing the backing pixels. On a page with conditional controls, await a +route-owned positive settled signal before sampling; an unmount or reflow between +those phases can otherwise pair stale text coordinates with a different control's +surface. Absence of a transient control alone is insufficient when it can also be +absent before work starts. Do not hide this race with a sleep, a lower contrast +budget or a palette change to the unrelated control. Paused native keyframes +verify source/80ms press lead/252–420ms content handoff and reverse tracks, +alongside real-time mouse/keyboard/touch, interruption and resource checks. Event +dispatch/focus completion does not imply Router's state commit completed; await +the exact selected-state assertion rather than arbitrary sleeps. Startup module delay/abort fixtures match exact URL pathnames independently of Vite cache-busting queries; still assert that interception actually occurred. Keep production-bundle startup tests separate from those dev-module fixtures. diff --git a/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/check.jsonl b/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/check.jsonl new file mode 100644 index 000000000..00ad7dbf6 --- /dev/null +++ b/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/check.jsonl @@ -0,0 +1,4 @@ +{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend quality-check entry point."} +{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Verify deterministic browser evidence without arbitrary waits."} +{"file": ".trellis/spec/frontend/appearance.md", "reason": "Verify contrast budgets and route coverage remain unchanged."} +{"file": ".trellis/spec/frontend/health.md", "reason": "Verify synchronization uses a route-owned settled signal."} diff --git a/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/implement.jsonl b/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/implement.jsonl new file mode 100644 index 000000000..9a44329b9 --- /dev/null +++ b/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/implement.jsonl @@ -0,0 +1,4 @@ +{"file": ".trellis/spec/frontend/index.md", "reason": "Route to the frontend quality, appearance, and health contract owners."} +{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Owns browser synchronization and raster evidence requirements."} +{"file": ".trellis/spec/frontend/appearance.md", "reason": "Owns dark-theme composited contrast coverage and budgets."} +{"file": ".trellis/spec/frontend/health.md", "reason": "Owns initial health-read lifecycle and conditional batch controls."} diff --git a/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/prd.md b/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/prd.md new file mode 100644 index 000000000..452f374c4 --- /dev/null +++ b/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/prd.md @@ -0,0 +1,28 @@ +# Fix merge queue dark blue contrast regression + +## Goal + +Restore compliant dark-theme contrast for the Health stop-check control, validate the WebKit regression, and return PR #188 to a green merge queue. + +## Requirements + +- Treat merge-queue run `34842498230` as a browser-test synchronization defect unless product evidence proves an actual palette defect. Preserve the current dark-theme tokens and the existing 4.5:1 text-contrast threshold. +- Before raster contrast sampling on `/health`, wait for the route-owned initial health read and its conditional batch controls to reach a stable state. Do not use an arbitrary delay. +- Keep all eight route samples, Chromium/WebKit coverage, and the shared contrast sampler unchanged unless broader evidence requires a different owner. +- Record the non-obvious two-phase raster-sampling pitfall in the owning frontend quality SPEC so future dynamic pages do not pair stale text geometry with a later layout. +- Validate the focused WebKit regression repeatedly, then run the repository frontend quality gates before pushing the PR branch. + +## Acceptance Criteria + +- [ ] The health-page contrast test cannot record the transient `停止检查` text and then sample pixels after that control has unmounted and the primary action has shifted into its coordinates. +- [ ] The dark-blue composited text test passes repeatedly in WebKit at 1232×700 without lowering contrast budgets or changing product colors. +- [ ] Frontend type checking, formatting, lint, unit tests, contract validation, and the relevant browser tests pass locally. +- [ ] PR #188 is pushed, its branch and merge-queue CI are green, and the PR is merged into `main`. + +## Notes + +- Failure evidence: WebKit reported `停止检查` with foreground RGB(232,245,255) over RGB(179,226,255), exactly the dark primary-action fill, at 1.24:1. The stop action itself uses the dark secondary-control surface. +- `sampleTextContrast` records text/color/coordinates, hides text, and only then captures the raster. The health batch can finish between those phases, unmounting the stop action and moving `检查全部软件` into the old coordinates. +- This is a lightweight, test-and-SPEC-only repair; no product behavior, native contract, or palette redesign is in scope. +- Local evidence: the focused WebKit case passed 10 consecutive runs, the complete blue-theme browser file passed 30/30 across four Chromium viewports plus WebKit, and `check:prearchive` passed the complete frontend, Rust, and repository-contract gate. +- Trellis archival precedes remote execution. PR checks, the replacement merge-group run, and the final `main` readback are post-archive evidence completed by the same session rather than claimed inside the work commit. diff --git a/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/task.json b/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/task.json new file mode 100644 index 000000000..c70d51cc5 --- /dev/null +++ b/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/task.json @@ -0,0 +1,26 @@ +{ + "id": "fix-merge-queue-blue-contrast", + "name": "fix-merge-queue-blue-contrast", + "title": "Fix merge queue dark blue contrast regression", + "description": "Restore compliant dark-theme contrast for the Health stop-check control, validate the WebKit regression, and return PR #188 to a green merge queue.", + "status": "in_progress", + "dev_type": null, + "scope": "frontend/browser-tests", + "package": null, + "priority": "P2", + "creator": "pythonrust", + "assignee": "pythonrust", + "createdAt": "2026-09-14", + "completedAt": null, + "branch": "dev/laiyongjie", + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/tests/browser/blue-themes.spec.ts b/tests/browser/blue-themes.spec.ts index d6874b767..4870f5541 100644 --- a/tests/browser/blue-themes.spec.ts +++ b/tests/browser/blue-themes.spec.ts @@ -75,7 +75,22 @@ test("dark blue text and controls remain readable on actual composited page and content: "::-webkit-scrollbar { width: 15px; height: 15px; }", }); const scope = `[data-testid="${route.split("?")[0]}-page"]`; - await expect(page.locator(scope)).toBeVisible(); + const routeScope = page.locator(scope); + await expect(routeScope).toBeVisible(); + if (route === "health") { + // Raster sampling records glyph geometry before it hides text and takes + // the screenshot. Let the initial read and its conditional stop action + // settle first, otherwise the primary action can move into stale points. + await expect(routeScope.locator(".fy-health-status")).toHaveText( + "本机检查正常", + ); + await expect( + routeScope.getByRole("button", { name: "检查全部软件" }), + ).toBeEnabled(); + await expect( + routeScope.getByRole("button", { name: "停止检查" }), + ).toHaveCount(0); + } const samples = await sampleTextContrast(page, scope); expect(samples.length).toBeGreaterThan(3); await info.attach(`dark-${route.split("?")[0]}`, { From 631e90fec3bda457966a409232f0027c8f0c54fd Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:12:58 +0800 Subject: [PATCH 11/12] chore(task): archive 09-14-fix-merge-queue-blue-contrast --- .../2026-09}/09-14-fix-merge-queue-blue-contrast/check.jsonl | 0 .../09-14-fix-merge-queue-blue-contrast/implement.jsonl | 0 .../2026-09}/09-14-fix-merge-queue-blue-contrast/prd.md | 0 .../2026-09}/09-14-fix-merge-queue-blue-contrast/task.json | 4 ++-- 4 files changed, 2 insertions(+), 2 deletions(-) rename .trellis/tasks/{ => archive/2026-09}/09-14-fix-merge-queue-blue-contrast/check.jsonl (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-fix-merge-queue-blue-contrast/implement.jsonl (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-fix-merge-queue-blue-contrast/prd.md (100%) rename .trellis/tasks/{ => archive/2026-09}/09-14-fix-merge-queue-blue-contrast/task.json (92%) diff --git a/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/check.jsonl b/.trellis/tasks/archive/2026-09/09-14-fix-merge-queue-blue-contrast/check.jsonl similarity index 100% rename from .trellis/tasks/09-14-fix-merge-queue-blue-contrast/check.jsonl rename to .trellis/tasks/archive/2026-09/09-14-fix-merge-queue-blue-contrast/check.jsonl diff --git a/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/implement.jsonl b/.trellis/tasks/archive/2026-09/09-14-fix-merge-queue-blue-contrast/implement.jsonl similarity index 100% rename from .trellis/tasks/09-14-fix-merge-queue-blue-contrast/implement.jsonl rename to .trellis/tasks/archive/2026-09/09-14-fix-merge-queue-blue-contrast/implement.jsonl diff --git a/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/prd.md b/.trellis/tasks/archive/2026-09/09-14-fix-merge-queue-blue-contrast/prd.md similarity index 100% rename from .trellis/tasks/09-14-fix-merge-queue-blue-contrast/prd.md rename to .trellis/tasks/archive/2026-09/09-14-fix-merge-queue-blue-contrast/prd.md diff --git a/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/task.json b/.trellis/tasks/archive/2026-09/09-14-fix-merge-queue-blue-contrast/task.json similarity index 92% rename from .trellis/tasks/09-14-fix-merge-queue-blue-contrast/task.json rename to .trellis/tasks/archive/2026-09/09-14-fix-merge-queue-blue-contrast/task.json index c70d51cc5..3c0b9c122 100644 --- a/.trellis/tasks/09-14-fix-merge-queue-blue-contrast/task.json +++ b/.trellis/tasks/archive/2026-09/09-14-fix-merge-queue-blue-contrast/task.json @@ -3,7 +3,7 @@ "name": "fix-merge-queue-blue-contrast", "title": "Fix merge queue dark blue contrast regression", "description": "Restore compliant dark-theme contrast for the Health stop-check control, validate the WebKit regression, and return PR #188 to a green merge queue.", - "status": "in_progress", + "status": "completed", "dev_type": null, "scope": "frontend/browser-tests", "package": null, @@ -11,7 +11,7 @@ "creator": "pythonrust", "assignee": "pythonrust", "createdAt": "2026-09-14", - "completedAt": null, + "completedAt": "2026-09-14", "branch": "dev/laiyongjie", "base_branch": "main", "worktree_path": null, From 3af5b2ec3a5fd81cae85446a4ad91bbb9675b339 Mon Sep 17 00:00:00 2001 From: Kafu <153478754+python-rust@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:13:05 +0800 Subject: [PATCH 12/12] chore: record journal --- .trellis/workspace/pythonrust/index.md | 5 +++-- .trellis/workspace/pythonrust/journal-2.md | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.trellis/workspace/pythonrust/index.md b/.trellis/workspace/pythonrust/index.md index 2b45d298d..cb41c805d 100644 --- a/.trellis/workspace/pythonrust/index.md +++ b/.trellis/workspace/pythonrust/index.md @@ -8,7 +8,7 @@ - **Active File**: `journal-2.md` -- **Total Sessions**: 89 +- **Total Sessions**: 90 - **Last Active**: 2026-09-14 @@ -19,7 +19,7 @@ | File | Lines | Status | |------|-------|--------| -| `journal-2.md` | ~827 | Active | +| `journal-2.md` | ~849 | Active | | `journal-1.md` | ~1987 | Archived | @@ -30,6 +30,7 @@ | # | Date | Title | Commits | Branch | |---|------|-------|---------|--------| +| 90 | 2026-09-14 | Stabilize merge-queue contrast sampling | `5be5540c` | `dev/laiyongjie` | | 89 | 2026-09-14 | Audit ahead-of-main commits and split SPEC owners | `20dd6537` | `dev/laiyongjie` | | 88 | 2026-09-14 | Simplify secondary pages and close validation gaps | `379bb0d113702421779e01eb4f49f4cf6c13c0aa`, `ee475a784f6995ecfd933b3a6b78fe1ae8950193` | `dev/laiyongjie` | | 87 | 2026-09-09 | Repair Grok PR 185 CI and archive superseded plans | `46e378e8`, `ac458fcb`, `98d57692` | `fix/grok-pr185-ci-closeout` | diff --git a/.trellis/workspace/pythonrust/journal-2.md b/.trellis/workspace/pythonrust/journal-2.md index 9fafb759c..f54951da4 100644 --- a/.trellis/workspace/pythonrust/journal-2.md +++ b/.trellis/workspace/pythonrust/journal-2.md @@ -825,3 +825,25 @@ Reviewed the six commits ahead of main, decomposed oversized Trellis SPEC owners ### Status [OK] **Completed** + + +## Session 90: Stabilize merge-queue contrast sampling + + +**Date**: 2026-09-14 +**Task**: Stabilize merge-queue contrast sampling +**Branch**: `dev/laiyongjie` + +### Summary + +Diagnosed PR #188 merge-group WebKit failure as a two-phase raster sampling race on the Health page, added route-owned settled-state synchronization, documented the contract in the frontend quality SPEC, and completed focused browser plus full prearchive validation. + +### Git Commits + +| Hash | Message | +|------|---------| +| `5be5540c` | test(browser): stabilize dynamic contrast sampling | + +### Status + +[OK] **Completed**