diff --git a/.agents/notes/proposed/architecture/2026-10-31-plugin-injected-toolchains.md b/.agents/notes/proposed/architecture/2026-10-31-plugin-injected-toolchains.md index ddda0d6dcc..12b89248f4 100644 --- a/.agents/notes/proposed/architecture/2026-10-31-plugin-injected-toolchains.md +++ b/.agents/notes/proposed/architecture/2026-10-31-plugin-injected-toolchains.md @@ -74,7 +74,7 @@ Core 基础切片先独立验证固定值,因此低层提供方超时不能撤 ### 4. 目录固定与手动添加的写入边界 -环境页按目录展示结果,子目录继承最近祖先的固定值。固定写到 `.lithe/toolchains/local.json` 的 `pins`;现有 `toolchains` 字段保留,草案不授权提前迁移旧数据。目录标识使用规范的工作区相对路径,根为 `.`,不能用绝对路径或 `..` 把固定作用域扩展到工作区外。 +环境页按目录展示结果,子目录继承最近祖先的固定值。固定写到 `.lithe/toolchains/local.json` 的 `pins`;现有 `toolchains` 字段保留,本提案不授权提前迁移旧数据。目录标识使用规范的工作区相对路径,根为 `.`,不能用绝对路径或 `..` 把固定作用域扩展到工作区外。 `local.json` 保存本机安装路径,因此首次明确保存时,平台存储适配器还要确保 `.lithe/.gitignore` 包含 `/toolchains/local.json`,保留已有内容。打开项目或设置页只读,不创建这两个文件;未修改、取消、失焦和切页不写入。保存必须检查读取基线,受保护地原子替换并回读;任一步失败都报告真实结果、保留草稿和既有数据,不能把部分写入报告成全部成功。 @@ -146,14 +146,14 @@ macOS 的声明式路线由 `MacToolchainDiscovery` 执行,宿主不认识任 | 阶段 | 交付内容 | 必须证明 | | --- | --- | --- | -| 1:提案 | 本笔记、隔离的清单贡献、查询和解析、`pins` 契约草案及 fixture | Java 风格定义、mise 提供方、未定义工具反例可读且一致;未实施的草案字段不能被误当成已支持能力 | +| 1:提案 | 本笔记、隔离的清单贡献、查询和解析、`pins` 契约草案及 fixture(草案和 fixture 已于 2026-11-05 删除,见下文) | Java 风格定义、mise 提供方、未定义工具反例可读且一致;未实施的草案字段不能被误当成已支持能力 | | 2:Core 与查询客户端,部分完成 | Core 已支持定义/答案校验、目录固定、四层解析、同时列出扫描规则与提供方的发现计划、设置页候选(`toolchains.candidates`)、只报告安装的提供方和版本校验;macOS 设置页已接入声明式扫描、版本探测、提供方查询客户端和输入监听;Windows 未接入 | 已有纯数据测试保护固定失效、来源与最低版本、冲突、路径和数量边界,以及只扫描、只由定义插件自供、两条路线报告同一 home 时只算一份;macOS 用真实临时目录和注入的探测证明扫描、工作区排除、只探测 Core 返回的文件和冲突;macOS 已用脚本后端与注入查询证明协商、原样回复、同一轮共享代次而操作 ID 各不相同、整份重查与合并,并用 `toolchains.resolve` 证明项目要求、未安装要求退到下一层、固定值生效与失效,以及超时和后端不可用的提供方带着各自原因留在结果里、由下一层决定;真实后端超时、进程回收与 Windows 仍须平台实现证明;不新增宿主工具表 | | 3:统一选择与设置界面,部分完成 | macOS 环境/来源页和只读加载器已接入;实际启动与展示共用快照、Java/Maven 定义迁移尚未完成 | 现有目录、插件撤销、错误及过期快照测试;手动添加、确认保存、冲突回读和消费者切换仍须后续验证;同项删除被替换的专属路径 | | 4:两个真实提供方 | mise 后端调用上游输出,SDKMAN 作为不同机制的验收插件,设置各自注入插件页 | 第二个提供方只改插件;SDKMAN 路径从 `MacRuntimeDiscovery` 迁出;原生工作流、离线/未信任/未安装/取消、进程清理与安装包哈希不变 | 阶段 3 清理 `ProjectRuntimeService` 的专属选择链和 `project-jdk`、`project-maven` 等被替代引用前,须按 [Java/Maven 迁移清单](2026-10-26-maven-java-domain-plugin-migration.md)核对真实消费者和数据迁移。Node、Go、Python 等在其插件声明定义之前保持当前实现,不新增特判;不能在本次提案删除仍在生产使用的代码,或用永久兼容别名伪装迁移完成。 -契约草案位于 `shared/contracts/plugins/v1/toolchains/proposal/`,其中尚未实现的消费者启动、查询客户端和界面行为继续保留未支持标记。Core 已实现的清单贡献和命令由本次正式契约单独说明,不把草案全集直接当作能力承诺;旧 `local-v1` schema 与 Run 配置读写路径保持不变,新 Core 固定解析入口只处理调用方传入的 JSON,不打开文件或保存忽略规则。合并后在插件文档仓库按合并的 `preview` 提交执行 `node scripts/contract.mjs sync --revision `;实际插件后端协议接入时同步 Node/Java SDK,并在合并前执行 SDK 端到端验证。每阶段的功能矩阵只记录实际实现和目标平台运行证据,不把本提案或 Core 单元测试当作 Windows 已完成。 +阶段 1 的契约草案 `shared/contracts/plugins/v1/toolchains/proposal/` 及其 fixture 已于 2026-11-05 删除。草案把已实现和未实现的行为写在一起,同步到 lithe-plugin-docs 后,插件开发者分不清哪些能用。其中已经实现的后端协商 `litheToolchains`、稳定失败码和错误回复规则已写进正式契约 `shared/contracts/plugins/v1/toolchains/README.md`;Core 测试改用 `rust/lithe-core/src/tests/toolchain_vectors.rs` 里的内联数据。消费者迁移、查询生命周期与缓存、固定值保存和版本管理器插件等未实现部分只在本笔记跟踪,实现时再写进正式契约;旧 `local-v1` schema 与 Run 配置读写路径保持不变,新 Core 固定解析入口只处理调用方传入的 JSON,不打开文件或保存忽略规则。合并后在插件文档仓库按合并的 `preview` 提交执行 `node scripts/contract.mjs sync --revision `;实际插件后端协议接入时同步 Node/Java SDK,并在合并前执行 SDK 端到端验证。每阶段的功能矩阵只记录实际实现和目标平台运行证据,不把本提案或 Core 单元测试当作 Windows 已完成。 ## 风险 @@ -169,7 +169,7 @@ macOS 的声明式路线由 `MacToolchainDiscovery` 执行,宿主不认识任 当前验证覆盖文档、契约、Core 纯数据边界和 macOS 设置页的扫描与布局测试,不声称运行时流程已验收: - `./scripts/agent-notes/verify-agent-notes.sh`:中文笔记格式、生命周期、链接与路径。 -- `./scripts/architecture/verify-shared-contracts.sh`:现行共享契约及 JSON 有效性;不能替代草案 schema 的专门校验或未来 Core 语义测试。 +- `./scripts/architecture/verify-shared-contracts.sh`:现行共享契约及 JSON 有效性;不检查 Core 语义,语义由下面的 Core 测试保护。 - `./scripts/platform-feature-matrix/verify-platform-feature-matrix.sh`:矩阵记录实际 Core 切片与平台待接入状态,不把纯数据测试标成产品运行时已验证。 - `cargo test --manifest-path rust/Cargo.toml -p lithe-core --lib toolchain_definitions -- --test-threads=1`:短 ID 来源、重复归属、嵌套字段失败即拒绝、只报告安装的提供方,以及定义插件为自己的工具声明提供方。 - `cargo test --manifest-path rust/Cargo.toml -p lithe-core --lib toolchain_pins -- --test-threads=1`:目录按组件继承、失效数据、跨平台路径语法、数量边界和 legacy 数据保留。 diff --git a/rust/lithe-core/src/tests/mod.rs b/rust/lithe-core/src/tests/mod.rs index 106e208163..665ea794a0 100644 --- a/rust/lithe-core/src/tests/mod.rs +++ b/rust/lithe-core/src/tests/mod.rs @@ -35,3 +35,4 @@ mod toolchain_definitions; mod toolchain_pins; mod toolchain_queries; mod toolchain_resolution; +mod toolchain_vectors; diff --git a/rust/lithe-core/src/tests/toolchain_pins.rs b/rust/lithe-core/src/tests/toolchain_pins.rs index d27fdbac3e..bfca269874 100644 --- a/rust/lithe-core/src/tests/toolchain_pins.rs +++ b/rust/lithe-core/src/tests/toolchain_pins.rs @@ -1,17 +1,9 @@ //! Deterministic local-pin validation and component-wise inheritance regressions. +use super::toolchain_vectors::local_pins; use crate::toolchain::pins; use serde_json::{json, Value}; -/// Loads inert contract data; none of its fictional paths are probed. -fn fixture() -> Value { - serde_json::from_str(include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../shared/contracts/plugins/v1/fixtures/toolchains/proposal/local.json" - ))) - .unwrap() -} - /// Exercises the pure input boundary with a selected directory and arbitrary tool. fn resolve(document: &Value, directory: &str, tool: &str) -> Value { pins::resolve(&json!({"document":document,"directory":directory,"tool":tool})).unwrap() @@ -23,8 +15,8 @@ fn pin(home: &str) -> Value { } #[test] -fn toolchain_pins_contract_fixture_inherits_the_nearest_ancestor() { - let document = fixture(); +fn toolchain_pins_inherit_the_nearest_ancestor() { + let document = local_pins(); pins::validate(&document).unwrap(); for (directory, expected) in [ (".", "."), @@ -41,7 +33,7 @@ fn toolchain_pins_contract_fixture_inherits_the_nearest_ancestor() { #[test] fn toolchain_pins_do_not_match_sibling_prefixes_or_other_tools() { - let document = fixture(); + let document = local_pins(); assert_eq!( resolve(&document, "apps/website/src", "java")["sourceDirectory"], "." @@ -79,7 +71,7 @@ fn toolchain_pins_retain_dormant_namespaced_values_and_legacy_data() { #[test] fn toolchain_pins_validate_every_entry_not_only_the_requested_tool() { - let mut document = fixture(); + let mut document = local_pins(); document["pins"]["unrelated"] = json!({"node":{"home":"relative","version":"22","provider":"host:manual"}}); let error = @@ -102,21 +94,21 @@ fn toolchain_pins_reject_non_normalized_directories_but_allow_dotted_names() { "C:/root", "src\nlib", ] { - let mut document = fixture(); + let mut document = local_pins(); document["pins"][directory] = json!({"java":pin("/opt/lithe-fixtures/java")}); assert!( pins::validate(&document).is_err(), "document key {directory:?}" ); assert!( - pins::resolve(&json!({"document":fixture(),"directory":directory,"tool":"java"})) + pins::resolve(&json!({"document":local_pins(),"directory":directory,"tool":"java"})) .is_err(), "requested directory {directory:?}" ); } for directory in [".config/tools", "foo.bar/.hidden", "a..b", "."] { assert!( - pins::resolve(&json!({"document":fixture(),"directory":directory,"tool":"java"})) + pins::resolve(&json!({"document":local_pins(),"directory":directory,"tool":"java"})) .is_ok(), "{directory}" ); @@ -175,7 +167,7 @@ fn toolchain_pins_reject_invalid_tools_providers_and_unknown_fields() { "a/b", ] { assert!( - pins::resolve(&json!({"document":fixture(),"directory":".","tool":tool})).is_err(), + pins::resolve(&json!({"document":local_pins(),"directory":".","tool":tool})).is_err(), "{tool:?}" ); let document = json!({"version":1,"toolchains":{},"pins":{".":{tool:pin("/opt/lithe-fixtures/java")}}}); @@ -190,20 +182,20 @@ fn toolchain_pins_reject_invalid_tools_providers_and_unknown_fields() { "dev..fixture:mise", "host:manual:extra", ] { - let mut document = fixture(); + let mut document = local_pins(); document["pins"]["."]["java"]["provider"] = json!(provider); assert!(pins::validate(&document).is_err(), "{provider}"); } - let mut document = fixture(); + let mut document = local_pins(); document["extra"] = json!(true); assert!(pins::validate(&document).is_err()); - let mut document = fixture(); + let mut document = local_pins(); document["pins"]["."]["java"]["extra"] = json!(true); assert!(pins::validate(&document).is_err()); let document = json!({"version":1,"toolchains":{"old":{"unknown":"path"}}}); assert!(pins::validate(&document).is_err()); assert!(pins::resolve( - &json!({"document":fixture(),"directory":".","tool":"java","unknown":true}) + &json!({"document":local_pins(),"directory":".","tool":"java","unknown":true}) ) .is_err()); } @@ -221,13 +213,13 @@ fn toolchain_pins_reject_missing_fields_and_wrong_shapes() { assert!(pins::validate(&document).is_err(), "{document}"); } for property in ["home", "version", "provider"] { - let mut document = fixture(); + let mut document = local_pins(); document["pins"]["."]["java"] .as_object_mut() .unwrap() .remove(property); assert!(pins::validate(&document).is_err(), "missing {property}"); - let mut document = fixture(); + let mut document = local_pins(); document["pins"]["."]["java"][property] = json!(42); assert!(pins::validate(&document).is_err(), "numeric {property}"); } @@ -252,7 +244,7 @@ fn toolchain_pins_enforce_collection_and_text_limits_before_resolution() { "plugin.toolchain.limitExceeded" ); for (property, length) in [("home", 4097), ("version", 129), ("provider", 257)] { - let mut document = fixture(); + let mut document = local_pins(); document["pins"]["."]["java"][property] = json!("x".repeat(length)); assert_eq!( pins::validate(&document).unwrap_err().code(), @@ -260,7 +252,7 @@ fn toolchain_pins_enforce_collection_and_text_limits_before_resolution() { "{property}" ); } - let mut document = fixture(); + let mut document = local_pins(); document["pins"]["x".repeat(2049)] = json!({}); assert_eq!( pins::validate(&document).unwrap_err().code(), diff --git a/rust/lithe-core/src/tests/toolchain_queries.rs b/rust/lithe-core/src/tests/toolchain_queries.rs index 2fefc1ff97..09f05bad62 100644 --- a/rust/lithe-core/src/tests/toolchain_queries.rs +++ b/rust/lithe-core/src/tests/toolchain_queries.rs @@ -1,11 +1,9 @@ //! Pure query validation with inert native filesystem facts and cross-platform paths. +use super::toolchain_vectors::{error_responses, query_request, query_response, HOME, WORKSPACE}; use crate::toolchain::{paths, query}; use serde_json::{json, Value}; -const HOME: &str = "/opt/lithe-fixtures/installs/java/21.0.4"; -const WORKSPACE: &str = "/opt/lithe-fixtures/workspace"; - fn contract(name: &str) -> Value { let source = match name { "java" => include_str!(concat!( @@ -16,10 +14,6 @@ fn contract(name: &str) -> Value { env!("CARGO_MANIFEST_DIR"), "/../../shared/contracts/plugins/v1/fixtures/toolchains/toolchain-provider.plugin.json" )), - "queries" => include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../shared/contracts/plugins/v1/fixtures/toolchains/proposal/query-cases.json" - )), _ => panic!("unknown contract fixture"), }; serde_json::from_str(source).unwrap() @@ -34,9 +28,8 @@ fn fact(path: &str, kind: &str, executable: bool) -> Value { } fn input() -> Value { - let fixture = contract("queries"); json!({"registry":{"plugins":[{"manifest":manifest(&contract("java")),"verifiedOfficial":true},{"manifest":manifest(&contract("mise")),"verifiedOfficial":false}]}, - "pluginID":"dev.lithe.fixture.mise","request":fixture["cases"][0]["request"],"response":fixture["cases"][0]["response"],"currentGeneration":1,"platform":"macos", + "pluginID":"dev.lithe.fixture.mise","request":query_request(),"response":query_response(),"currentGeneration":1,"platform":"macos", "filesystem":[fact(WORKSPACE,"directory",false),fact(HOME,"directory",false),fact(&format!("{HOME}/bin/java"),"file",true),fact(&format!("{WORKSPACE}/mise.toml"),"file",false)]}) } @@ -225,9 +218,8 @@ fn toolchain_queries_refuse_mismatched_rpc_and_operation_generations() { #[test] fn toolchain_queries_keep_every_error_mapping_unique_and_validate_the_pair() { let mut numbers = std::collections::BTreeSet::new(); - for response in contract("queries")["errorResponses"].as_array().unwrap() { + for mut response in error_responses() { let mut value = input(); - let mut response = response.clone(); response["id"] = value["request"]["id"].clone(); response["error"]["data"]["operationID"] = value["request"]["params"]["operationID"].clone(); diff --git a/rust/lithe-core/src/tests/toolchain_resolution.rs b/rust/lithe-core/src/tests/toolchain_resolution.rs index d529fa49a5..b644af9cc2 100644 --- a/rust/lithe-core/src/tests/toolchain_resolution.rs +++ b/rust/lithe-core/src/tests/toolchain_resolution.rs @@ -1,13 +1,12 @@ //! Exercises shared tool selection through production commands and inert native facts. +use super::toolchain_vectors::{local_pins, query_request, query_response, HOME, WORKSPACE}; use crate::toolchain::{ candidates_command, check_version_command, discovery_plan_command, failure_reply_command, resolve_command, }; use serde_json::{json, Value}; -const WORKSPACE: &str = "/opt/lithe-fixtures/workspace"; -const HOME: &str = "/opt/lithe-fixtures/installs/java/21.0.4"; const OLD: &str = "/opt/lithe-fixtures/installs/java/17.0.12"; /// Reads review data as test inputs, never as an installed plugin or live path. @@ -21,14 +20,6 @@ fn contract(name: &str) -> Value { env!("CARGO_MANIFEST_DIR"), "/../../shared/contracts/plugins/v1/fixtures/toolchains/toolchain-provider.plugin.json" )), - "query" => include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../shared/contracts/plugins/v1/fixtures/toolchains/proposal/query-cases.json" - )), - "local" => include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../shared/contracts/plugins/v1/fixtures/toolchains/proposal/local.json" - )), _ => panic!("unknown fixture"), }; serde_json::from_str(source).unwrap() @@ -71,8 +62,7 @@ fn resolve(input: &Value) -> Value { /// Captures a fake backend exchange that the resolver must decode itself. fn query(input: &Value) -> Value { - let case = &contract("query")["cases"][0]; - json!({"registry":input["registry"],"pluginID":"dev.lithe.fixture.mise","request":case["request"],"response":case["response"], + json!({"registry":input["registry"],"pluginID":"dev.lithe.fixture.mise","request":query_request(),"response":query_response(), "currentGeneration":1,"platform":"macos","filesystem":input["filesystem"]}) } @@ -215,7 +205,7 @@ fn toolchain_resolution_pins_inherit_nearest_ancestor_without_sibling_prefixes() let manual = "/opt/lithe-fixtures/manual/java/17.0.12"; installation(&mut value, HOME, "21.0.4"); installation(&mut value, manual, "17.0.12"); - value["local"] = contract("local"); + value["local"] = local_pins(); value["directory"] = json!("apps/web/src"); let result = resolve(&value); assert_eq!(result["outcome"]["selection"]["home"], manual); @@ -438,7 +428,7 @@ fn toolchain_commands_cross_the_json_dispatch_boundary() { ), ( "toolchains.resolvePins", - json!({"document":contract("local"),"directory":"apps/web/src","tool":"java"}), + json!({"document":local_pins(),"directory":"apps/web/src","tool":"java"}), "resolution", ), ( @@ -884,7 +874,7 @@ fn toolchain_resolution_environment_candidate_uses_its_declared_layout() { fn toolchain_resolution_disabled_definition_cannot_reactivate_a_saved_pin() { let mut value = input(); installation(&mut value, HOME, "21.0.4"); - value["local"] = contract("local"); + value["local"] = local_pins(); value["registry"]["plugins"] .as_array_mut() .unwrap() diff --git a/rust/lithe-core/src/tests/toolchain_vectors.rs b/rust/lithe-core/src/tests/toolchain_vectors.rs new file mode 100644 index 0000000000..4a84810844 --- /dev/null +++ b/rust/lithe-core/src/tests/toolchain_vectors.rs @@ -0,0 +1,77 @@ +//! Inline toolchain pin and query vectors shared by the Core toolchain tests. +//! +//! Every path lives under `/opt/lithe-fixtures`; no test creates or probes it. +//! The pin, query and resolution tests all read these values, so changing one +//! here changes the expectations of all three modules. + +use serde_json::{json, Value}; + +/// Java home that the query reply reports and the root pin selects. +pub(super) const HOME: &str = "/opt/lithe-fixtures/installs/java/21.0.4"; +/// Workspace root that the query request names. +pub(super) const WORKSPACE: &str = "/opt/lithe-fixtures/workspace"; +/// Manually pinned home for `apps/web`, older than [`HOME`]. +const MANUAL_HOME: &str = "/opt/lithe-fixtures/manual/java/17.0.12"; + +/// Builds a local pins document with a root pin, a manual pin on `apps/web` +/// and a pin on the hidden `.config/tools` directory. +/// +/// The nested pins let tests check that a directory inherits its nearest +/// ancestor and that a sibling sharing a name prefix does not. +pub(super) fn local_pins() -> Value { + json!({"version":1,"toolchains":{},"pins":{ + ".":{"java":{"home":HOME,"version":"21.0.4","provider":"dev.lithe.fixture.mise:mise"}}, + "apps/web":{"java":{"home":MANUAL_HOME,"version":"17.0.12","provider":"host:manual"}}, + ".config/tools":{"java":{"home":HOME,"version":"21.0.4","provider":"dev.lithe.fixture.mise:mise"}} + }}) +} + +/// Builds the `toolchains/query` request a host sends to the `mise` provider +/// for Java in the workspace root. +pub(super) fn query_request() -> Value { + json!({"jsonrpc":"2.0","id":1,"method":"toolchains/query","params":{ + "operationID":"query-1","generation":1,"providerID":"mise","authority":{"kind":"local"}, + "context":{"contextID":"context-1","generation":1}, + "workspace":{"rootID":"root-1","root":WORKSPACE,"directory":"."},"tools":["java"]}}) +} + +/// Builds a valid reply to [`query_request`]: one installed home and the +/// project requirement in `mise.toml` that selects it. +pub(super) fn query_response() -> Value { + json!({"jsonrpc":"2.0","id":1,"result":{ + "operationID":"query-1","generation":1,"providerID":"mise", + "installations":[{"tool":"java","version":"21.0.4","home":HOME}], + "statements":[{"tool":"java","scope":"project","source":format!("{WORKSPACE}/mise.toml"), + "request":"21","installation":HOME,"kind":"declaration","reader":"native"}], + "watch":[format!("{WORKSPACE}/mise.toml"),"/opt/lithe-fixtures/installs/java"]}}) +} + +/// Builds one JSON-RPC error reply per stable toolchain failure. +/// +/// The numeric codes are listed here independently of `query::rpc_code`, so a +/// test pairing each reply with its decoded code checks the mapping itself. +/// Ids and operation IDs are distinct placeholders; tests rewrite both to match +/// the request they answer. +pub(super) fn error_responses() -> Vec { + const FAILURES: [(i64, &str); 11] = [ + (-32601, "methodUnavailable"), + (-32602, "invalidParams"), + (-32800, "cancelled"), + (-32070, "permissionDenied"), + (-32071, "authorityUnsupported"), + (-32072, "timeout"), + (-32073, "providerUnavailable"), + (-32074, "invalidReply"), + (-32075, "limitExceeded"), + (-32076, "unsafePath"), + (-32077, "operationExpired"), + ]; + FAILURES + .iter() + .zip(100..) + .map(|(&(number, name), id)| { + json!({"jsonrpc":"2.0","id":id,"error":{"code":number,"message":"Fixture diagnostic.", + "data":{"code":format!("plugin.toolchain.{name}"),"operationID":format!("query-{id}"),"providerID":"mise"}}}) + }) + .collect() +} diff --git a/scripts/architecture/language-specifics-baseline.json b/scripts/architecture/language-specifics-baseline.json index b90e93a0a1..1a44cd9013 100644 --- a/scripts/architecture/language-specifics-baseline.json +++ b/scripts/architecture/language-specifics-baseline.json @@ -296,8 +296,8 @@ "rust/lithe-core/src/tests/spring.rs": {"words":151}, "rust/lithe-core/src/tests/support.rs": {"words":1}, "rust/lithe-core/src/tests/toolchain_pins.rs": {"words":33}, - "rust/lithe-core/src/tests/toolchain_queries.rs": {"words":16,"nameComparisons":1}, - "rust/lithe-core/src/tests/toolchain_resolution.rs": {"words":91,"nameComparisons":1}, + "rust/lithe-core/src/tests/toolchain_queries.rs": {"words":15,"nameComparisons":1}, + "rust/lithe-core/src/tests/toolchain_resolution.rs": {"words":90,"nameComparisons":1}, "rust/lithe-core/src/tests/vcs.rs": {"words":1}, "rust/lithe-core/src/toolchain/pins.rs": {"words":1}, "rust/lithe-core/src/vcs/model.rs": {"words":1}, @@ -335,16 +335,6 @@ "shared/contracts/plugins/v1/fixtures/toolchains/tool-definition.plugin.json": {"words":17}, "shared/contracts/plugins/v1/fixtures/toolchains/toolchain-provider.plugin.json": {"words":2}, "shared/contracts/plugins/v1/plugin-manifest.schema.json": {"words":11,"schemaEnums":13}, - "shared/contracts/plugins/v1/toolchains/proposal/README.md": {"words":5}, - "shared/contracts/plugins/v1/fixtures/toolchains/proposal/java-tool-definition.json": {"words":19}, - "shared/contracts/plugins/v1/fixtures/toolchains/proposal/local.json": {"words":6}, - "shared/contracts/plugins/v1/fixtures/toolchains/proposal/manifest-rejections.json": {"words":18}, - "shared/contracts/plugins/v1/fixtures/toolchains/proposal/mise-provider.json": {"words":2}, - "shared/contracts/plugins/v1/fixtures/toolchains/proposal/query-cases.json": {"words":40}, - "shared/contracts/plugins/v1/fixtures/toolchains/proposal/resolution-cases.json": {"words":74}, - "shared/contracts/plugins/v1/toolchains/proposal/local.schema.json": {"words":2}, - "shared/contracts/plugins/v1/toolchains/proposal/manifest.md": {"words":10}, - "shared/contracts/plugins/v1/toolchains/proposal/protocol.md": {"words":1}, "shared/contracts/plugins/v1/toolchains/README.md": {"words":1}, "windows/tauri/src-tauri/src/debug.rs": {"words":16}, "windows/tauri/src-tauri/src/logging.rs": {"words":4}, diff --git a/shared/contracts/plugins/v1/README.md b/shared/contracts/plugins/v1/README.md index 0a3c32a6ee..412388f88b 100644 --- a/shared/contracts/plugins/v1/README.md +++ b/shared/contracts/plugins/v1/README.md @@ -15,7 +15,6 @@ This directory is the cross-platform source of truth: | `plugin-index.schema.json` | JSON shape of a marketplace source index | | `fixtures/` | Valid example manifests and golden cases, one folder per contract area (below). Manifests are named after the capability they exercise rather than a published plugin. Case files name their base manifest by file name, so file names are unique across the folders | | `toolchains/README.md` | Implemented Core foundation: plugin definitions/provider declarations, verified-origin registry, reply validation, directory pins, snapshot resolution, version matching and discovery plans; macOS uses it for its read-only settings pages and for Build and run tools, Windows is not connected | -| `toolchains/proposal/` | Broader design draft, including unimplemented query transport, native lifecycle/persistence, consumer migration, settings UI and manager plugins; examples are review projections, not installable manifests | | Fixture folder | Contents | | --- | --- | @@ -31,7 +30,7 @@ This directory is the cross-platform source of truth: | `fixtures/vcs/` | A version control provider | | `fixtures/keymap-import/` | Two independent shortcut import providers sharing one packaged backend | | `fixtures/project-generators/` | Project generators: template files (`templates/`), a scaffolding command, and an archive download; `project-generator-tool-field.plugin.json` with `templates/tool-field/` picks tool installations with `tool` fields (`projectGenerators` 2); `project-plans.json` holds golden `project.*` form evaluations, generation plans, and name checks, and a case's optional `manifest` names the fixture it runs against (the version 1 generator by default) | -| `fixtures/toolchains/` | Production-shape fixtures for tool definitions and a packaged toolchain provider; they require `toolDefinitions` and `toolchainProviders`, which only macOS advertises. `proposal/` holds the examples and cases of the toolchains draft | +| `fixtures/toolchains/` | Production-shape fixtures for tool definitions and a packaged toolchain provider; they require `toolDefinitions` and `toolchainProviders`, which only macOS advertises | | `fixtures/package-trust/` | `publisher-bundled.plugin.json` and `signature-vectors.json`: public cross-platform vectors for the package publisher signature: the RFC 8032 test key, a fixed package with non-ASCII file names, its canonical payload, RFC 8032 deterministic signature, and content digest. Both platforms must build the same canonical payload and digest and accept the recorded signature; signers that add randomness, such as CryptoKit, check the signature by verification instead of byte comparison | | `fixtures/marketplace/` | Marketplace index, availability, source URL, and install review golden cases | @@ -52,8 +51,8 @@ fields are rejected; editors associate the schema by file name instead. ## Plugin-owned toolchains (Core foundation) -The [toolchains draft](toolchains/proposal/README.md) records the replacement -design discussed in [PR #1581](https://github.com/1lck/Lithe-IDEA/pull/1581#issuecomment-6541366112). +The design follows the rewrite plan in +[PR #1581](https://github.com/1lck/Lithe-IDEA/pull/1581#issuecomment-6541366112). All tool definitions belong to enabled plugins; the host supplies only generic query, validation, resolution, persistence, and presentation mechanisms. It does not add a built-in tool-definition table or interpret version-manager @@ -75,8 +74,8 @@ whose `toolchain` executable names a plugin-defined tool rather than a host toolchain; settings detections never grant launch authority. Windows does not advertise the capabilities. Saving pins, `toolchainConsumers`, generic runtime launches and live backend activation remain unsupported, and Java and language-server consumers keep their previous adapters. Version-manager -implementations such as mise belong to their own plugin packages. Proposal -envelopes remain separate from production manifests. See [the implemented slice](toolchains/README.md). +implementations such as mise belong to their own plugin packages. See +[the implemented slice](toolchains/README.md). ## Shortcut import providers (Core foundation) diff --git a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/java-tool-definition.json b/shared/contracts/plugins/v1/fixtures/toolchains/proposal/java-tool-definition.json deleted file mode 100644 index 501ec9f844..0000000000 --- a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/java-tool-definition.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "proposalVersion": 1, - "pluginID": "dev.lithe.fixture.java-support", - "requires": { "toolDefinitions": 1, "executableResolution": 2, "toolchainConsumers": 1 }, - "permissions": [], - "toolDefinitions": [ - { - "id": "java", - "displayName": "JDK", - "icon": { "host": "plugin" }, - "executableLayouts": [ - { "path": "bin/java", "platforms": ["macos", "linux"] }, - { "path": "bin/java.exe", "platforms": ["windows"] } - ], - "executables": [ - { "id": "pinned", "kind": "setting", "settingID": "executable", "layer": "project" }, - { "id": "project-request", "kind": "toolchain", "scope": "project" }, - { "id": "default", "kind": "setting", "settingID": "executable", "layer": "global" }, - { "id": "global-request", "kind": "toolchain", "scope": "global" }, - { "id": "home", "kind": "environment", "variable": "JAVA_HOME", "path": "bin/java", "platforms": ["macos", "linux"] }, - { "id": "home-windows", "kind": "environment", "variable": "JAVA_HOME", "path": "bin/java.exe", "platforms": ["windows"] }, - { "id": "shell", "kind": "command", "command": "java" }, - { "id": "detected", "kind": "toolchain", "scope": "installed" } - ], - "version": { "arguments": ["-version"], "pattern": "version \\\"(?:1\\.)?(\\d+(?:\\.\\d+)*)" }, - "homeLayouts": [ - { "match": "*.jdk", "home": "Contents/Home", "platforms": ["macos"] }, - { "match": "*", "home": "." } - ], - "discovery": [ - { "root": { "kind": "absolute", "path": "/Library/Java/JavaVirtualMachines" }, "pattern": "*.jdk", "platforms": ["macos"] }, - { "root": { "kind": "home" }, "pattern": "Library/Java/JavaVirtualMachines/*.jdk", "platforms": ["macos"] } - ], - "homeVariable": "JAVA_HOME", - "commands": ["java"], - "wrappers": [], - "exports": [ { "name": "JAVA_HOME", "value": "${home}" }, { "name": "LITHE_JAVA_PATH", "value": "${executable}" } ], - "pathPrepend": ["${home}/bin"], - "launch": ["${executable}", "${runtimeArguments}", "-jar", "${entry}", "${arguments}"] - } - ], - "consumers": [ - { - "id": "fixture-language-server", - "requiresTools": [ { "tool": "java", "minimumVersion": "21" } ], - "onToolchainChange": "restart", - "launch": { "kind": "runtime", "tool": "java", "entry": "server/fixture-language-server.jar" } - } - ] -} diff --git a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/local.json b/shared/contracts/plugins/v1/fixtures/toolchains/proposal/local.json deleted file mode 100644 index 7bf8520535..0000000000 --- a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/local.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": 1, - "toolchains": {}, - "pins": { - ".": { - "java": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "version": "21.0.4", - "provider": "dev.lithe.fixture.mise:mise" - } - }, - "apps/web": { - "java": { - "home": "/opt/lithe-fixtures/manual/java/17.0.12", - "version": "17.0.12", - "provider": "host:manual" - } - }, - ".config/tools": { - "java": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "version": "21.0.4", - "provider": "dev.lithe.fixture.mise:mise" - } - } - } -} diff --git a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/manifest-rejections.json b/shared/contracts/plugins/v1/fixtures/toolchains/proposal/manifest-rejections.json deleted file mode 100644 index 89adc06e6e..0000000000 --- a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/manifest-rejections.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "proposalVersion": 1, - "description": "Proposed rejection scenarios, not current Core tests. Apply RFC 6902 patch to base; schema checks shape, registry checks enabled definition references and manifest semantics, hostTrust checks independently verified installation provenance. Context is supplied by the test host, never trusted plugin JSON.", - "cases": [ - { - "name": "provider capability requires effective permission", - "base": "mise-provider.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/permissions", "value": ["backend.launch"] }], - "expected": { "status": "rejected", "code": "plugin.permission.undeclared" } - }, - { - "name": "export cannot replace PATH", - "base": "java-tool-definition.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/exports/0/name", "value": "PATH" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "export cannot set loader injection variable", - "base": "java-tool-definition.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/exports/0/name", "value": "DYLD_INSERT_LIBRARIES" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "PATH prepend cannot escape home", - "base": "java-tool-definition.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/pathPrepend/0", "value": "${home}/../bin" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "watch glob cannot recurse without a bound", - "base": "mise-provider.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolchainProviders/0/watchFiles/0/pattern", "value": "**/mise.toml" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "probe cannot pass an arbitrary script argument", - "base": "java-tool-definition.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/version/arguments", "value": ["-c", "project-code"] }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "launch interpolation is argv not shell text", - "base": "java-tool-definition.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/launch/1", "value": "--args=${arguments}" }], - "expected": { "status": "rejected", "code": "plugin.template.invalid" } - }, - { - "name": "provider backend cannot bootstrap from discovered runtime", - "base": "mise-provider.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/backend/launch", "value": { "kind": "runtime", "tool": "node", "entry": "provider/main.mjs" } }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "consumer reference to undefined tool is not a syntax error", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [{ "op": "replace", "path": "/consumers/0/requiresTools", "value": [{ "tool": "java", "minimumVersion": "21" }, { "tool": "dev.lithe.fixture.java-support:absent" }] }], - "context": { "enabledToolIDs": ["java"] }, - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "undefinedTool" } - }, - { - "name": "community plugin cannot claim reserved short definition", - "base": "java-tool-definition.json", - "phase": "hostTrust", - "patch": [], - "context": { "verifiedOrigin": "communityPackage" }, - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "reservedToolID" } - }, - { - "name": "community definition namespace belongs to its plugin", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/id", "value": "dev.lithe.fixture.other:java" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "foreignToolNamespace" } - }, - { - "name": "later definition cannot replace existing owner", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [], - "context": { "registeredTools": [{ "id": "java", "pluginID": "dev.lithe.fixture.first-java" }] }, - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "duplicateToolID" } - }, - { - "name": "capability list cannot advertise unused provider", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [{ "op": "add", "path": "/requires/toolchainProviders", "value": 1 }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "unusedCapability" } - }, - { - "name": "pattern needs a numeric version capture", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/version/pattern", "value": "version.*" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "missingVersionCapture" } - }, - { - "name": "ordering cannot reference another contribution set", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [{ "op": "add", "path": "/toolDefinitions/0/executables/0/insertBefore", "value": "another-set" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "unknownCandidateAnchor" } - } - ] -} diff --git a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/mise-provider.json b/shared/contracts/plugins/v1/fixtures/toolchains/proposal/mise-provider.json deleted file mode 100644 index 4ec3e07b90..0000000000 --- a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/mise-provider.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "proposalVersion": 1, - "pluginID": "dev.lithe.fixture.mise", - "requires": { "toolchainProviders": 1, "backend": 2, "serverProcess": 1 }, - "permissions": ["backend.launch", "toolchains.provide"], - "toolchainProviders": [ - { - "id": "mise", - "displayName": "mise", - "tools": ["java", "node", "maven", "pnpm", "go"], - "watchFiles": [ - { "pattern": "mise.toml", "reader": "native" }, - { "pattern": "mise.local.toml", "reader": "native" }, - { "pattern": ".mise.toml", "reader": "native" }, - { "pattern": ".tool-versions", "reader": "compatible" }, - { "pattern": ".mise/config.toml", "reader": "native" }, - { "pattern": ".mise/config.local.toml", "reader": "native" } - ] - } - ], - "backend": { - "launch": { "kind": "packaged", "path": "bin/mise-provider" }, - "arguments": ["--stdio"] - } -} diff --git a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/query-cases.json b/shared/contracts/plugins/v1/fixtures/toolchains/proposal/query-cases.json deleted file mode 100644 index e876e4d082..0000000000 --- a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/query-cases.json +++ /dev/null @@ -1,852 +0,0 @@ -{ - "proposalVersion": 1, - "status": "review-only", - "cases": [ - { - "name": "installed project requirement", - "request": { - "jsonrpc": "2.0", - "id": 1, - "method": "toolchains/query", - "params": { - "operationID": "query-1", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 1, - "result": { - "operationID": "query-1", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/installs/java/21.0.4" - } - ], - "statements": [ - { - "tool": "java", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "21", - "installation": "/opt/lithe-fixtures/installs/java/21.0.4", - "kind": "declaration", - "reader": "native" - } - ], - "watch": [ - "/opt/lithe-fixtures/workspace/mise.toml", - "/opt/lithe-fixtures/installs/java" - ] - } - }, - "filesystemEvidence": [ - { - "path": "/opt/lithe-fixtures/installs/java/21.0.4", - "canonical": "/opt/lithe-fixtures/installs/java/21.0.4", - "kind": "directory" - }, - { - "path": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "canonical": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "kind": "file", - "executable": true - } - ], - "expected": { - "validation": "accepted", - "selectedHome": "/opt/lithe-fixtures/installs/java/21.0.4" - } - }, - { - "name": "uninstalled request is retained as unmet", - "request": { - "jsonrpc": "2.0", - "id": 2, - "method": "toolchains/query", - "params": { - "operationID": "query-2", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "node" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 2, - "result": { - "operationID": "query-2", - "generation": 1, - "providerID": "mise", - "installations": [], - "statements": [ - { - "tool": "node", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "99", - "installation": null, - "kind": "declaration", - "reader": "native" - } - ], - "watch": [ - "/opt/lithe-fixtures/workspace/mise.toml" - ] - } - }, - "expected": { - "validation": "accepted", - "reason": { - "code": "requestedNotInstalled", - "tool": "node", - "provider": "dev.lithe.fixture.mise:mise", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "99" - }, - "continueToLowerLayer": true - } - }, - { - "name": "provider home inside workspace is unsafe", - "request": { - "jsonrpc": "2.0", - "id": 3, - "method": "toolchains/query", - "params": { - "operationID": "query-3", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 3, - "result": { - "operationID": "query-3", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/workspace/vendor/jdk" - } - ], - "statements": [ - { - "tool": "java", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "21", - "installation": "/opt/lithe-fixtures/workspace/vendor/jdk", - "kind": "declaration", - "reader": "native" - } - ], - "watch": [] - } - }, - "expected": { - "validation": "rejected", - "code": "plugin.toolchain.unsafePath", - "rpcCode": -32076 - } - }, - { - "name": "cancelled query ignores late success", - "request": { - "jsonrpc": "2.0", - "id": 4, - "method": "toolchains/query", - "params": { - "operationID": "query-4", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 4, - "result": { - "operationID": "query-4", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/installs/java/21.0.4" - } - ], - "statements": [], - "watch": [] - } - }, - "events": [ - "dispatched", - "cancelRequested", - "localCompletion", - "lateReply" - ], - "expected": { - "completionCount": 1, - "code": "plugin.toolchain.cancelled", - "rpcCode": -32800, - "publishReply": false, - "cacheReply": false - } - }, - { - "name": "global query generation rejects old result", - "request": { - "jsonrpc": "2.0", - "id": 5, - "method": "toolchains/query", - "params": { - "operationID": "query-5", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": null, - "workspace": null, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 5, - "result": { - "operationID": "query-5", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/installs/java/21.0.4" - } - ], - "statements": [], - "watch": [] - } - }, - "events": [ - "dispatchedGeneration1", - "watchChanged", - "dispatchedGeneration2", - "replyGeneration1" - ], - "expected": { - "publishReply": false, - "cacheReply": false, - "requery": "allProviders" - } - }, - { - "name": "remote query never falls back to local", - "request": { - "jsonrpc": "2.0", - "id": 6, - "method": "toolchains/query", - "params": { - "operationID": "query-6", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "remote", - "id": "ssh-1" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/srv/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "expected": { - "validation": "rejected", - "code": "plugin.toolchain.authorityUnsupported", - "rpcCode": -32071, - "localProcessesStarted": 0 - } - }, - { - "name": "missing definition retains display-only requirement", - "request": { - "jsonrpc": "2.0", - "id": 7, - "method": "toolchains/query", - "params": { - "operationID": "query-7", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "maven" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 7, - "result": { - "operationID": "query-7", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "maven", - "version": "3.9.9", - "home": "/opt/lithe-fixtures/installs/maven/3.9.9" - } - ], - "statements": [ - { - "tool": "maven", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "3.9.9", - "installation": "/opt/lithe-fixtures/installs/maven/3.9.9", - "kind": "declaration", - "reader": "native" - } - ], - "watch": [] - } - }, - "enabledDefinitions": [ - "java" - ], - "expected": { - "validation": "acceptedWithFilteredAuthority", - "installations": [], - "reason": { - "code": "definitionUnavailable", - "tool": "maven", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "3.9.9" - }, - "enableActionSource": "verifiedInstalledRegistry" - } - }, - { - "name": "external spelling cannot hide workspace symlink", - "request": { - "jsonrpc": "2.0", - "id": 8, - "method": "toolchains/query", - "params": { - "operationID": "query-8", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 8, - "result": { - "operationID": "query-8", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/linked-jdk" - } - ], - "statements": [], - "watch": [] - } - }, - "filesystemEvidence": [ - { - "path": "/opt/lithe-fixtures/linked-jdk", - "canonical": "/opt/lithe-fixtures/workspace/vendor/jdk", - "kind": "directory" - } - ], - "expected": { - "validation": "rejected", - "code": "plugin.toolchain.unsafePath", - "rpcCode": -32076 - } - }, - { - "name": "statement cannot reference an unlisted home", - "request": { - "jsonrpc": "2.0", - "id": 9, - "method": "toolchains/query", - "params": { - "operationID": "query-9", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 9, - "result": { - "operationID": "query-9", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/installs/java/21.0.4" - } - ], - "statements": [ - { - "tool": "java", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "21", - "installation": "/opt/lithe-fixtures/other-jdk", - "kind": "declaration", - "reader": "native" - } - ], - "watch": [] - } - }, - "expected": { - "validation": "rejected", - "code": "plugin.toolchain.invalidReply", - "rpcCode": -32074 - } - }, - { - "name": "query timeout remains different from cancellation", - "request": { - "jsonrpc": "2.0", - "id": 10, - "method": "toolchains/query", - "params": { - "operationID": "query-10", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "events": [ - "dispatched", - "advanceManualClockToDeadline", - "lateReply" - ], - "expected": { - "completionCount": 1, - "code": "plugin.toolchain.timeout", - "rpcCode": -32072, - "publishReply": false - } - }, - { - "name": "create missing watched file re-queries all providers", - "request": { - "jsonrpc": "2.0", - "id": 11, - "method": "toolchains/query", - "params": { - "operationID": "query-11", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "events": [ - "fingerprintMissingMiseToml", - "createMiseToml", - "watchEvent" - ], - "enabledProviders": [ - "host:system", - "dev.lithe.fixture.mise:mise", - "dev.lithe.fixture.sdkman:sdkman" - ], - "expected": { - "requery": "allProviders", - "incremental": false - } - }, - { - "name": "delete watched input re-queries all providers", - "request": { - "jsonrpc": "2.0", - "id": 12, - "method": "toolchains/query", - "params": { - "operationID": "query-12", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "events": [ - "fingerprintExistingToolVersions", - "deleteToolVersions", - "watchEvent" - ], - "expected": { - "requery": "allProviders", - "incremental": false - } - }, - { - "name": "provider disable invalidates pending reply", - "request": { - "jsonrpc": "2.0", - "id": 13, - "method": "toolchains/query", - "params": { - "operationID": "query-13", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 13, - "result": { - "operationID": "query-13", - "generation": 1, - "providerID": "mise", - "installations": [], - "statements": [], - "watch": [] - } - }, - "events": [ - "dispatched", - "providerDisabled", - "lateReply" - ], - "expected": { - "publishReply": false, - "cacheReply": false, - "ownedListenersRemaining": 0 - } - } - ], - "errorResponses": [ - { - "jsonrpc": "2.0", - "id": 100, - "error": { - "code": -32601, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.methodUnavailable", - "operationID": "query-100", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 101, - "error": { - "code": -32602, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.invalidParams", - "operationID": "query-101", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 102, - "error": { - "code": -32800, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.cancelled", - "operationID": "query-102", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 103, - "error": { - "code": -32070, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.permissionDenied", - "operationID": "query-103", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 104, - "error": { - "code": -32071, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.authorityUnsupported", - "operationID": "query-104", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 105, - "error": { - "code": -32072, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.timeout", - "operationID": "query-105", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 106, - "error": { - "code": -32073, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.providerUnavailable", - "operationID": "query-106", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 107, - "error": { - "code": -32074, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.invalidReply", - "operationID": "query-107", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 108, - "error": { - "code": -32075, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.limitExceeded", - "operationID": "query-108", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 109, - "error": { - "code": -32076, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.unsafePath", - "operationID": "query-109", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 110, - "error": { - "code": -32077, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.operationExpired", - "operationID": "query-110", - "providerID": "mise" - } - } - } - ] -} diff --git a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/resolution-cases.json b/shared/contracts/plugins/v1/fixtures/toolchains/proposal/resolution-cases.json deleted file mode 100644 index 9dd4a35a53..0000000000 --- a/shared/contracts/plugins/v1/fixtures/toolchains/proposal/resolution-cases.json +++ /dev/null @@ -1,571 +0,0 @@ -{ - "proposalVersion": 1, - "status": "review-only", - "cases": [ - { - "name": "nearest ancestor pin applies component-wise", - "input": { - "directory": "apps/web/src", - "localFixture": "local.json" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": "apps/web/src", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/manual/java/17.0.12", - "executable": "/opt/lithe-fixtures/manual/java/17.0.12/bin/java", - "version": "17.0.12", - "provider": "host:manual", - "layer": "pinned", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/manual/java/17.0.12" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/manual/java/17.0.12/bin" - ] - } - }, - "trace": [ - { - "candidateID": "pinned", - "layer": "pinned", - "state": "resolved", - "source": "apps/web" - } - ], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "similarly prefixed sibling does not inherit child pin", - "input": { - "directory": "apps/website", - "localFixture": "local.json" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": "apps/website", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "executable": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "version": "21.0.4", - "provider": "dev.lithe.fixture.mise:mise", - "layer": "pinned", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/java/21.0.4/bin" - ] - } - }, - "trace": [ - { - "candidateID": "pinned", - "layer": "pinned", - "state": "resolved", - "source": "." - } - ], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "nonempty unavailable pin stops fallback", - "input": { - "pinHome": "/opt/lithe-fixtures/missing", - "installedHomes": [ - "/opt/lithe-fixtures/installs/java/21.0.4" - ] - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "terminalFailure", - "reason": { - "code": "notFound" - } - }, - "trace": [ - { - "candidateID": "pinned", - "layer": "pinned", - "state": "terminalFailure", - "reason": { - "code": "notFound" - } - } - ], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "uninstalled project version remains visible after fallback", - "input": { - "statement": { - "tool": "java", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "99", - "installation": null, - "kind": "declaration", - "reader": "native" - }, - "loginShellHome": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "executable": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "version": "21.0.4", - "provider": "host:system", - "layer": "global", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/java/21.0.4/bin" - ] - } - }, - "trace": [ - { - "candidateID": "project-request", - "layer": "project", - "state": "skip", - "reason": { - "code": "requestedNotInstalled", - "tool": "java", - "provider": "dev.lithe.fixture.mise:mise", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "99" - } - }, - { - "candidateID": "shell-command", - "layer": "global", - "state": "resolved" - } - ], - "unmet": [ - { - "code": "requestedNotInstalled", - "tool": "java", - "provider": "dev.lithe.fixture.mise:mise", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "99" - } - ], - "conflicts": [] - } - }, - { - "name": "conflicting globals skip providers and use login shell", - "input": { - "providerGlobals": [ - { - "provider": "dev.lithe.fixture.mise:mise", - "home": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - { - "provider": "dev.lithe.fixture.sdkman:sdkman", - "home": "/opt/lithe-fixtures/sdkman/java/17.0.12" - } - ], - "loginShellHome": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "executable": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "version": "21.0.4", - "provider": "host:system", - "layer": "global", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/java/21.0.4/bin" - ] - } - }, - "trace": [ - { - "candidateID": "global-request", - "layer": "global", - "state": "skip", - "reason": { - "code": "providerConflict" - } - }, - { - "candidateID": "shell-command", - "layer": "global", - "state": "resolved" - } - ], - "unmet": [], - "conflicts": [ - { - "scope": "global", - "providers": [ - "dev.lithe.fixture.mise:mise", - "dev.lithe.fixture.sdkman:sdkman" - ], - "action": "chooseProvider" - } - ] - } - }, - { - "name": "automatic compares numeric components not text", - "input": { - "probedVersions": [ - "9.0.9", - "21.0.4", - "17.0.12" - ], - "minimumVersion": "17" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "executable": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "version": "21.0.4", - "provider": "dev.lithe.fixture.mise:mise", - "layer": "automatic", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/java/21.0.4/bin" - ] - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "manual-only newer installation is not automatic", - "input": { - "installedHomes": [ - "/opt/lithe-fixtures/installs/java/21.0.4" - ], - "manualInstallations": [ - { - "home": "/opt/lithe-fixtures/manual/java/99", - "version": "99" - } - ] - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "executable": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "version": "21.0.4", - "provider": "dev.lithe.fixture.mise:mise", - "layer": "automatic", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/java/21.0.4/bin" - ] - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "child declaration beats more distant wrapper", - "input": { - "directory": "apps/web", - "projectCandidates": [ - { - "directory": ".", - "kind": "wrapper", - "path": "mvnw" - }, - { - "directory": "apps/web", - "kind": "declaration", - "home": "/opt/lithe-fixtures/installs/maven/3.9.9" - } - ] - }, - "expected": { - "operationID": "resolve-1", - "tool": "maven", - "directory": "apps/web", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/maven/3.9.9", - "executable": "/opt/lithe-fixtures/installs/maven/3.9.9/bin/mvn", - "version": "3.9.9", - "provider": "dev.lithe.fixture.mise:mise", - "layer": "project", - "projectCode": false, - "environment": {}, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/maven/3.9.9/bin" - ] - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "same-directory wrapper precedes manager statement after user action", - "input": { - "directory": ".", - "userActionAuthorized": true, - "wrapper": "/opt/lithe-fixtures/workspace/mvnw", - "providerHome": "/opt/lithe-fixtures/installs/maven/3.9.9" - }, - "expected": { - "operationID": "resolve-1", - "tool": "maven", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": null, - "executable": "/opt/lithe-fixtures/workspace/mvnw", - "version": null, - "provider": "dev.lithe.fixture.maven:wrapper", - "layer": "project", - "projectCode": true, - "environment": {}, - "pathPrepend": [] - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "native reader beats compatible reader for the same file", - "input": { - "source": "/opt/lithe-fixtures/workspace/.nvmrc", - "readers": [ - { - "provider": "dev.lithe.fixture.mise:mise", - "reader": "compatible", - "home": "/opt/lithe-fixtures/installs/node/22.0.0" - }, - { - "provider": "dev.lithe.fixture.nvm:nvm", - "reader": "native", - "home": "/opt/lithe-fixtures/nvm/node/20.0.0" - } - ] - }, - "expected": { - "operationID": "resolve-1", - "tool": "node", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/nvm/node/20.0.0", - "executable": "/opt/lithe-fixtures/nvm/node/20.0.0/bin/node", - "version": "20.0.0", - "provider": "dev.lithe.fixture.nvm:nvm", - "layer": "project", - "projectCode": false, - "environment": {}, - "pathPrepend": [ - "/opt/lithe-fixtures/nvm/node/20.0.0/bin" - ] - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "disabled definition leaves pin but no executable authority", - "input": { - "localFixture": "local.json", - "enabledDefinitions": [] - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "skip", - "reason": { - "code": "definitionUnavailable", - "tool": "java" - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "unknown probed version cannot satisfy automatic minimum", - "input": { - "installedHomes": [ - "/opt/lithe-fixtures/installs/java/21.0.4" - ], - "probeOutput": "unknown", - "minimumVersion": "17" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "skip", - "reason": { - "code": "versionUnsatisfied" - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "equal newest versions at distinct homes require explicit choice", - "input": { - "installations": [ - { - "provider": "dev.lithe.fixture.mise:mise", - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "normalizedVersion": "21.0.4" - }, - { - "provider": "dev.lithe.fixture.mise:mise", - "home": "/opt/lithe-fixtures/other-installs/java/21.0.4", - "normalizedVersion": "21.0.4" - } - ] - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "skip", - "reason": { - "code": "ambiguousInstallation", - "tool": "java" - } - }, - "trace": [ - { - "candidateID": "installed", - "layer": "automatic", - "state": "skip", - "reason": { - "code": "ambiguousInstallation", - "tool": "java" - } - } - ], - "unmet": [], - "conflicts": [ - { - "scope": "automatic", - "providers": [ - "dev.lithe.fixture.mise:mise" - ], - "installations": [ - "/opt/lithe-fixtures/installs/java/21.0.4", - "/opt/lithe-fixtures/other-installs/java/21.0.4" - ], - "action": "chooseInstallation" - } - ] - } - } - ], - "invalidLocalDocuments": [ - { - "name": "parent segment", - "value": { - "version": 1, - "toolchains": {}, - "pins": { - "apps/../other": {} - } - } - }, - { - "name": "absolute directory key", - "value": { - "version": 1, - "toolchains": {}, - "pins": { - "/apps": {} - } - } - }, - { - "name": "relative home", - "value": { - "version": 1, - "toolchains": {}, - "pins": { - ".": { - "java": { - "home": "vendor/jdk", - "version": "21", - "provider": "host:manual" - } - } - } - } - } - ] -} diff --git a/shared/contracts/plugins/v1/toolchains/README.md b/shared/contracts/plugins/v1/toolchains/README.md index 23b03ef66d..a11e308c8d 100644 --- a/shared/contracts/plugins/v1/toolchains/README.md +++ b/shared/contracts/plugins/v1/toolchains/README.md @@ -16,10 +16,9 @@ toolchain adapter and does not advertise `toolDefinitions` or `toolchainProviders`, so Core refuses packages that require them there. Publisher signatures provide independent origin evidence when present; legacy unsigned records never do. -The [broader proposal](proposal/README.md) still describes unsupported -consumer migration, native lifecycle, persistence and manager-plugin work. -The active shape is [plugin-manifest.schema.json](../plugin-manifest.schema.json), -not the proposal envelopes. +Consumer migration, the native query lifecycle and cache, pin saving and +version-manager plugins are not specified yet. The manifest shape is +[plugin-manifest.schema.json](../plugin-manifest.schema.json). ## Manifest admission and registry @@ -121,6 +120,46 @@ providers:[{pluginID,provider}]}` in stable order. Duplicate plugin/tool ownersh rejects the batch without replacing earlier owners. Limits are 128 plugins, 512 aggregate definitions and 256 aggregate providers. +## Provider backend negotiation + +A provider answers over its plugin backend's JSON-RPC connection, the same +`serverProcess` transport other backend requests use. The backend need not +declare a language, language server, Build provider or callable function. + +The host starts the backend with `lsp.startServer` and passes the manifest's +provider IDs as `toolchainProviders`. Only a plugin-owned session that declares +at least one provider is offered the protocol, in the client capabilities of +`initialize`: + +```json +{"experimental": {"litheToolchains": {"versions": [1], "providers": ["first", "second"]}}} +``` + +`providers` lists the declared IDs in sorted order. The backend selects in the +server capabilities of its `initialize` result: + +```json +{"experimental": {"litheToolchains": {"version": 1, "providers": ["first"]}}} +``` + +`version` must be an offered version and `providers` a non-empty list of +offered IDs without repeats; other fields are ignored. Any other answer, including +an unoffered version, an undeclared, repeated or non-string ID, or an empty list, +enables no provider at all rather than a partial set, and no query is sent. On +success Core emits a `toolchainProviders` runtime event with `result: {providers}`. + +The host sends each query with `lsp.toolchainRequest` +`{sessionId, operationId?, params, timeoutMilliseconds?}`, where `params` is the +`toolchains/query` params described below; Core writes the request with that +method and its own JSON-RPC ID. A `params.providerID` the backend did not select +is refused with `not_supported` and details `plugin.toolchain.providerUnavailable`, +and nothing is written. `timeoutMilliseconds` is clamped to 1-60,000; without it +the session's request timeout applies. Completion carries `result.value: {id, +response}`: the wire ID and the complete, unvalidated response, error replies +included. The host passes it with the same `params` to `decodeReply`. +Cancellation and late results follow +[`lsp.toolchainRequest`](https://github.com/1lck/Lithe-IDEA/blob/preview/shared/contracts/rust-core-api.md). + ## Provider reply validation `decodeReply` takes `{registry,pluginID,request,response,currentGeneration, @@ -164,10 +203,46 @@ remote authority, unsafe paths and malformed error mappings reject it. The snapshot contains `{installations,statements,watch,diagnostics}`; normalized entries carry qualified `:` provenance and project source -directory. JSON-RPC errors are checked against the distinct semantic/numeric map -in the [protocol proposal](proposal/protocol.md#stable-failure-mapping). -Cancellation, deadlines, cache invalidation and whole-generation requery remain -native-client implementation work, not behavior performed by this decoder. +directory. Cancellation, deadlines, cache invalidation and whole-generation +requery remain native-client implementation work, not behavior performed by this +decoder. + +### Error replies + +A provider that cannot answer returns a JSON-RPC `error` instead of `result`. +`error` has exactly `{code,message,data}`, and `data` has `{code,operationID, +providerID}` plus an optional `details`: + +- `code` is a number from the table below and `data.code` is the stable code + on its row. Each number has exactly one stable code. +- `operationID` and `providerID` equal those of the request `params`. +- `message` is non-empty diagnostic text of at most 1,024 UTF-8 bytes without + control characters. It is shown as provider text and never selects behavior. +- `details`, when present, is an object of at most 4,096 bytes serialized. It + must not carry credentials. + +`decodeReply` then refuses with the stable code and the provider's `message`. +An error reply that breaks any of these rules, including an unknown number or a +number that does not match `data.code`, is refused as +`plugin.toolchain.invalidReply`. + +| JSON-RPC | Stable code | Meaning | +| --- | --- | --- | +| `-32601` | `plugin.toolchain.methodUnavailable` | The backend does not serve the query method | +| `-32602` | `plugin.toolchain.invalidParams` | Malformed request or invalid declared input | +| `-32800` | `plugin.toolchain.cancelled` | The caller cancelled the request | +| `-32070` | `plugin.toolchain.permissionDenied` | A permission, review or enabled-state prerequisite is missing | +| `-32071` | `plugin.toolchain.authorityUnsupported` | The provider cannot serve the requested authority | +| `-32072` | `plugin.toolchain.timeout` | The local deadline passed | +| `-32073` | `plugin.toolchain.providerUnavailable` | The backend failed to start, disconnected, or its upstream query failed | +| `-32074` | `plugin.toolchain.invalidReply` | Malformed reply, or an installation reference to another tool | +| `-32075` | `plugin.toolchain.limitExceeded` | A reply, scan or watch budget was exceeded | +| `-32076` | `plugin.toolchain.unsafePath` | A path is not absolute, escapes its containment or fails executable checks | +| `-32077` | `plugin.toolchain.operationExpired` | The context or generation was already stale when the query was sent | + +A missing installation, an incompatible version or conflicting requirements are +not errors: report them in `result` (an unmet requirement has +`installation:null`) and let the resolver give the reason. When a query gets no reply at all, the host asks `failureReply` for one. `params` is the query's own `params` and `code` is `plugin.toolchain.timeout` (the local @@ -297,7 +372,9 @@ Production-shape examples are [tool-definition.plugin.json](../fixtures/toolchai and [toolchain-provider.plugin.json](../fixtures/toolchains/toolchain-provider.plugin.json). Core acceptance vectors are in [toolchain_definitions.rs](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_definitions.rs), -with sibling pins/query/resolution suites. Passing them is not native product -verification. Contract changes must be mirrored after merge with +with sibling pins/query/resolution suites. The pin document, query exchange and +one error reply per stable code that those suites share are inline in +[toolchain_vectors.rs](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_vectors.rs). +Passing them is not native product verification. Contract changes must be mirrored after merge with `node scripts/contract.mjs sync --revision ` in [lithe-plugin-docs](https://github.com/Lithe-IDEA/lithe-plugin-docs). diff --git a/shared/contracts/plugins/v1/toolchains/proposal/README.md b/shared/contracts/plugins/v1/toolchains/proposal/README.md deleted file mode 100644 index e250eed01f..0000000000 --- a/shared/contracts/plugins/v1/toolchains/proposal/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# Plugin-injected toolchains (draft) - -> **Status: broader proposal; a Core-only foundation is now implemented.** -> The implemented subset is specified in [the active contract](../README.md). -> This directory remains a review artifact for -> [PR #1581's replacement plan](https://github.com/1lck/Lithe-IDEA/pull/1581#issuecomment-6541366112). -> It is not an extension of the currently accepted `plugin.json` format. -> The active manifest admits the implemented definition/provider fields, not -> these projection envelopes or the whole proposal. Native runtime selection, -> consumer migration, query transport, persistence and UI remain unimplemented. - -## Ownership - -A **tool definition** describes a tool's identity, executable layouts, version -probe, discovery locations, launch template, and environment exports. Only an -enabled plugin contributes a definition. The host has no built-in definitions, -reserved-name alias table, or tool-name-dependent launch logic in the proposed -path. Officially signed plugins may define short IDs; community plugins define -IDs in their own plugin namespace and may provide installations for any enabled -definition, including official short IDs. Verified package identity, not a -manifest assertion or ID prefix, establishes official status. - -A **toolchain provider** reports installed tools and, when it declares watched -files, the project's or user's requirements. It covers installation shapes the -frozen declarative `discovery` primitives cannot express; a plugin may provide -installations of a tool it defines. Both routes feed one automatic candidate set. -Version-manager configuration, version aliases, and file precedence -remain upstream/provider-owned. The host never interprets opaque version -requests such as `lts`. A **consumer** references a defined tool and declares -whether a changed selection needs a restart, resynchronization, or the next run. -These are independent contributions, not additional language/build requirements. - -The host owns a single generic resolver and query client, directory-scoped pins, -and the environment/sources presentation skeleton. Provider-specific settings -are injected through that provider plugin's existing settings page. Definitions -and provider replies are untrusted input; permission to provide toolchains does -not authorize replacing unrelated host commands. - -## Review Artifacts - -| File | Purpose | -| --- | --- | -| [manifest.md](manifest.md) | Definition, provider, consumer, trust, and capability contribution rules | -| [manifest-contributions.schema.json](manifest-contributions.schema.json) | Standalone draft contribution projection, not a complete plugin manifest | -| [protocol.md](protocol.md) | Query lifecycle, validation, four-layer selection, storage, and stable failures | -| [query.schema.json](query.schema.json) | Query request/reply shapes | -| [resolution.schema.json](resolution.schema.json) | Resolver outcomes and structured reasons | -| [local.schema.json](local.schema.json) | Proposed extension of the machine-local document with directory pins | -| `fixtures/toolchains/proposal/` | Illustrative contributions, queries, resolution outcomes, pins, and rejections | - -Schemas describe JSON shape. Signature verification, duplicate-definition -registration, enabled-plugin ownership, path containment, executable evidence, -and lifecycle state require separate semantic validation during implementation. -A shape-valid reply is never sufficient permission to launch a program. -Example filesystem paths are fictional and must not be probed on the reader's -machine; filesystem observations in cases are inputs for future test doubles. - -## Required Behavior - -Every tool uses the existing shared executable-candidate mechanism, extended -with `toolchain` candidates and structured `skip` reasons. There is no parallel -Java, Maven, or generic-command selection chain in the proposed path. - -| Layer | Selection rule | -| --- | --- | -| Pin | Nearest ancestor directory's explicit pin; an unusable pin is a terminal failure, never automatic fallback | -| Project | Nearest declaring directory; at equal depth wrapper before environment before version statement; for the same file native before compatible reader | -| Global | Lithe's explicit global default, then provider defaults; conflicting provider defaults require a user choice, using login-shell discovery until chosen | -| Automatic | Newest eligible provider installation satisfying the minimum version; manually added installations are excluded | - -An uninstalled project requirement skips with an explicit reason and stays -visible as unmet even if a lower layer succeeds. Provider installations cannot -come from the workspace; project wrappers remain project code and require the -consumer's existing explicit-action/consent policy. Queries, discovery, settings -inspection, and passive project import must not execute project code. - -The built-in **system provider** is a mechanism, not a contributor of definitions: -it scans only enabled definitions' declared locations. Adding a manager or tool -must not require a new parser, directory constant, or tool-name branch in Core. -The first implementation must prove this with two different providers. - -Each query has an operation identity, cancellation, a bounded deadline, and a -snapshot generation. New, changed, or deleted declared watch inputs invalidate -the entire generation and requery all enabled providers. Late replies cannot -repopulate revoked state. Disabling/uninstalling the defining plugin removes its -tool rows, consumers, settings, and exports, but retains stored pins for a later -reenable. Unknown/disabled definitions grant no executable authority; only bounded -display diagnostics may remain to explain the missing plugin. - -Reading `.lithe/toolchains/local.json` never creates it. Explicitly confirmed pin -edits use conflict-protected atomic replacement, then reread the result; the first -write preserves `.lithe/.gitignore` and adds `/toolchains/local.json`. Manually -added installations live in platform-owned application storage and are pin-only. -Installed app bundles and plugin packages remain read-only throughout. - -## Staged Landing And Compatibility - -1. **Proposal:** review schemas, examples, trust boundaries, and - the decision record. No host capability is enabled and no production format - is relaxed merely by this draft. Only the active contract allocates supported - Core capability versions; native hosts have not advertised them. -2. **Core and query client:** implement definition/answer validation, candidate - extensions, pin inheritance, full-generation invalidation, and macOS query - lifecycle. Preserve strict rejection by unsupported hosts. System discovery - consumes plugin declarations, never a built-in definition data file. -3. **Selection and UI:** integrate consumers and the environment/sources pages, - move Java definitions into Java Support and coordinate Maven definitions with - [PR #1558](https://github.com/1lck/Lithe-IDEA/pull/1558). Remove migrated legacy - special cases in the same slice. Other tools keep existing behavior until - their official plugins declare definitions; do not add new compatibility - aliases or tool-specific branches. -4. **Provider acceptance:** mise delegates to its upstream machine-readable CLI; - SDKMAN supplies the second, different provider without Core changes. Each - owns its own settings and bootstrap runtime. Backend-bearing providers still - require restart in the initial implementation; this proposal does not relax - live activation. - -The existing executable-resolution v1 contract does not permit project tool -overrides and supports only the documented consumer positions. These new -semantics need explicit capability admission and implemented adapters; do not -retroactively change v1 or advertise support based on schema changes alone. -Runtime minimum-version checking must use the definition's version probe rather -than a new per-tool branch. No current API is removed by this proposal. - -The Chinese architecture decision and acceptance criteria are recorded in the -[proposed Agent Note](https://github.com/1lck/Lithe-IDEA/blob/preview/.agents/notes/proposed/architecture/2026-10-31-plugin-injected-toolchains.md). -The UI reference is the -[interactive prototype](https://gist.github.com/xiaoyumuxi/2ae889a6592599b13998bc811c647942). -This directory specifies host-readable contracts, not a plugin-author tutorial -or SDK implementation; those belong in `lithe-plugin-docs`. - -After the proposal merges, sync the byte-identical contract mirror in -`Lithe-IDEA/lithe-plugin-docs` using -`node scripts/contract.mjs sync --revision ` and commit -there. Draft status must survive the sync. SDK protocol compatibility and both -SDK end-to-end suites become implementation-stage gates; this proposal does not -claim that either SDK already implements these messages. diff --git a/shared/contracts/plugins/v1/toolchains/proposal/local.schema.json b/shared/contracts/plugins/v1/toolchains/proposal/local.schema.json deleted file mode 100644 index 481d584736..0000000000 --- a/shared/contracts/plugins/v1/toolchains/proposal/local.schema.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/toolchains/proposal/local.schema.json", - "title": "Proposed local toolchain pins extension (not implemented)", - "description": "Review-only complete document shape for .lithe/toolchains/local.json. The active toolchain-local-v1.schema.json remains unchanged and does not accept pins.", - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "toolchains" - ], - "properties": { - "version": { - "const": 1 - }, - "toolchains": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": false, - "properties": { - "home": { - "type": "string", - "minLength": 1 - }, - "executable": { - "type": "string", - "minLength": 1 - }, - "javaHome": { - "type": "string", - "minLength": 1 - } - } - }, - "description": "Existing v1 entries are preserved byte-semantically; new resolution does not manufacture aliases to legacy project-jdk/project-maven IDs." - }, - "pins": { - "type": "object", - "maxProperties": 1024, - "propertyNames": { - "$ref": "#/$defs/directory" - }, - "additionalProperties": { - "type": "object", - "maxProperties": 64, - "propertyNames": { - "$ref": "#/$defs/toolID" - }, - "additionalProperties": { - "$ref": "#/$defs/pin" - } - } - } - }, - "$defs": { - "directory": { - "type": "string", - "maxLength": 2048, - "pattern": "^(?:\\.|(?!\\.\\.?(?:/|$))[^/\\\\:]+(?:/(?!\\.\\.?(?:/|$))[^/\\\\:]+)*)$", - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "toolID": { - "$ref": "manifest-contributions.schema.json#/$defs/toolID" - }, - "nativePath": { - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^(?:/|[A-Za-z]:[\\\\/]|\\\\\\\\)", - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "pin": { - "type": "object", - "additionalProperties": false, - "required": [ - "home", - "version", - "provider" - ], - "properties": { - "home": { - "$ref": "#/$defs/nativePath" - }, - "version": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "provider": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "pattern": "^[a-z][a-z0-9.-]*:[a-z][a-z0-9-]*$", - "description": "Qualified pluginID:providerID, or reserved host:manual / host:system; not an activation instruction." - } - } - } - } -} diff --git a/shared/contracts/plugins/v1/toolchains/proposal/manifest-contributions.schema.json b/shared/contracts/plugins/v1/toolchains/proposal/manifest-contributions.schema.json deleted file mode 100644 index a86dde1299..0000000000 --- a/shared/contracts/plugins/v1/toolchains/proposal/manifest-contributions.schema.json +++ /dev/null @@ -1,260 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/toolchains/proposal/manifest-contributions.schema.json", - "title": "PROPOSAL: toolchain contribution projections, not a plugin manifest", - "description": "A standalone review envelope for proposed manifest fields. Passing this schema does not make a plugin installable and does not prove signature, registry, filesystem or executable safety. See manifest.md.", - "type": "object", - "additionalProperties": false, - "required": ["proposalVersion", "pluginID", "requires", "permissions"], - "properties": { - "proposalVersion": { "const": 1 }, - "pluginID": { "type": "string", "pattern": "^[a-z0-9]+(?:[.-][a-z0-9]+)*$", "maxLength": 128 }, - "requires": { - "type": "object", "additionalProperties": false, "minProperties": 1, - "properties": { - "toolDefinitions": { "const": 1 }, - "toolchainProviders": { "const": 1 }, - "toolchainConsumers": { "const": 1 }, - "executableResolution": { "const": 2 }, - "backend": { "const": 2 }, - "serverProcess": { "const": 1 } - } - }, - "permissions": { - "type": "array", "maxItems": 16, "uniqueItems": true, - "items": { "enum": ["toolchains.provide", "backend.launch"] } - }, - "toolDefinitions": { "type": "array", "minItems": 1, "maxItems": 32, "items": { "$ref": "#/$defs/toolDefinition" } }, - "toolchainProviders": { "type": "array", "minItems": 1, "maxItems": 8, "items": { "$ref": "#/$defs/provider" } }, - "backend": { "$ref": "#/$defs/backend" }, - "consumers": { - "description": "Review projection of fields on existing language/build/run/debug contributions; NOT a proposed top-level manifest field.", - "type": "array", "minItems": 1, "maxItems": 64, "items": { "$ref": "#/$defs/consumer" } - } - }, - "anyOf": [ { "required": ["toolDefinitions"] }, { "required": ["toolchainProviders"] }, { "required": ["consumers"] } ], - "allOf": [ - { - "if": { "required": ["toolDefinitions"] }, - "then": { "properties": { "requires": { "required": ["toolDefinitions", "executableResolution"] } } } - }, - { - "if": { "required": ["toolchainProviders"] }, - "then": { - "required": ["backend"], - "properties": { - "requires": { "required": ["toolchainProviders", "backend", "serverProcess"] }, - "permissions": { "allOf": [ { "contains": { "const": "toolchains.provide" } }, { "contains": { "const": "backend.launch" } } ] } - } - } - }, - { - "if": { "required": ["consumers"] }, - "then": { "properties": { "requires": { "required": ["toolchainConsumers"] } } } - } - ], - "$defs": { - "identifier": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,63}$" }, - "toolID": { "type": "string", "pattern": "^(?:[a-z0-9]+(?:[.-][a-z0-9]+)*:)?[a-z][a-z0-9-]{0,63}$", "maxLength": 192 }, - "label": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^\\u0000-\\u001f\\u007f]+$" }, - "relativePath": { - "type": "string", "minLength": 1, "maxLength": 256, - "pattern": "^(?!/)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))[^/\\\\:*?\\u0000-\\u001f\\u007f]+(?:/[^/\\\\:*?\\u0000-\\u001f\\u007f]+)*$" - }, - "boundedGlob": { - "description": "At most eight segments; only single-segment * is supported, never **, ?, character classes, or traversal. Semantic validation also bounds expansion.", - "type": "string", "minLength": 1, "maxLength": 256, - "pattern": "^(?!/)(?!.*\\*\\*)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))[^/\\\\:?\\[\\]{}\\u0000-\\u001f\\u007f]+(?:/[^/\\\\:?\\[\\]{}\\u0000-\\u001f\\u007f]+){0,7}$" - }, - "command": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$" }, - "safeEnvironmentName": { "type": "string", "maxLength": 64, "pattern": "^(?!PATH$)(?!DYLD_)(?!LD_)[A-Z_][A-Z0-9_]*$" }, - "platforms": { "type": "array", "minItems": 1, "maxItems": 3, "uniqueItems": true, "items": { "enum": ["macos", "windows", "linux"] } }, - "version": { - "type": "object", "additionalProperties": false, "required": ["arguments", "pattern"], - "properties": { - "arguments": { "type": "array", "minItems": 1, "maxItems": 8, "items": { "type": "string", "pattern": "^--?[A-Za-z][A-Za-z0-9-]*$", "maxLength": 64 } }, - "pattern": { "description": "Rust-regex-compatible, first capture is a dotted numeric version. Validity, capture count and runtime bounds require semantic checks.", "type": "string", "minLength": 1, "maxLength": 512 } - } - }, - "executableLayout": { - "type": "object", "additionalProperties": false, "required": ["path"], - "properties": { "path": { "$ref": "#/$defs/relativePath" }, "platforms": { "$ref": "#/$defs/platforms" } } - }, - "homeLayout": { - "type": "object", "additionalProperties": false, "required": ["match", "home"], - "properties": { - "match": { "allOf": [ { "$ref": "#/$defs/boundedGlob" }, { "pattern": "^[^/]+$" } ] }, - "home": { "oneOf": [ { "const": "." }, { "$ref": "#/$defs/relativePath" } ] }, - "platforms": { "$ref": "#/$defs/platforms" } - } - }, - "discovery": { - "type": "object", "additionalProperties": false, "required": ["root", "pattern"], - "properties": { - "root": { - "oneOf": [ - { "type": "object", "additionalProperties": false, "required": ["kind"], "properties": { "kind": { "const": "home" } } }, - { "type": "object", "additionalProperties": false, "required": ["kind", "variable"], "properties": { "kind": { "const": "environment" }, "variable": { "$ref": "#/$defs/safeEnvironmentName" } } }, - { "type": "object", "additionalProperties": false, "required": ["kind", "path"], "properties": { "kind": { "const": "absolute" }, "path": { "type": "string", "minLength": 2, "maxLength": 256, "pattern": "^(?:/|[A-Za-z]:[\\\\/])[^\\u0000-\\u001f\\u007f]*$" } } } - ] - }, - "pattern": { "$ref": "#/$defs/boundedGlob" }, - "platforms": { "$ref": "#/$defs/platforms" } - } - }, - "wrapper": { - "type": "object", "additionalProperties": false, "required": ["path"], - "properties": { - "path": { "$ref": "#/$defs/relativePath" }, - "requires": { "type": "array", "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/relativePath" } }, - "platforms": { "$ref": "#/$defs/platforms" } - } - }, - "candidateMetadata": { - "type": "object", - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "priority": { "type": "integer", "minimum": -2147483648, "maximum": 2147483647 }, - "insertBefore": { "$ref": "#/$defs/identifier" }, - "insertAfter": { "$ref": "#/$defs/identifier" }, - "platforms": { "$ref": "#/$defs/platforms" } - }, - "not": { "anyOf": [ { "required": ["priority", "insertBefore"] }, { "required": ["priority", "insertAfter"] }, { "required": ["insertBefore", "insertAfter"] } ] } - }, - "candidate": { - "$ref": "#/$defs/candidateMetadata", "unevaluatedProperties": false, - "oneOf": [ - { - "required": ["kind", "settingID", "layer"], - "properties": { "kind": { "const": "setting" }, "settingID": { "const": "executable" }, "layer": { "enum": ["project", "global"] } } - }, - { - "required": ["kind", "scope"], - "properties": { "kind": { "const": "toolchain" }, "scope": { "enum": ["project", "global", "installed"] } } - }, - { - "required": ["kind", "path"], - "properties": { - "kind": { "const": "projectFile" }, - "path": { "$ref": "#/$defs/relativePath" }, - "requires": { "type": "array", "maxItems": 8, "items": { "$ref": "#/$defs/relativePath" } }, - "projectKind": { "enum": ["wrapper", "environment"], "default": "wrapper" }, - "home": { "oneOf": [ { "const": "." }, { "$ref": "#/$defs/relativePath" } ] } - }, - "allOf": [ - { - "if": { "required": ["projectKind"], "properties": { "projectKind": { "const": "environment" } } }, - "then": { "required": ["home"] }, - "else": { "not": { "required": ["home"] } } - } - ] - }, - { - "required": ["kind", "variable", "path"], - "properties": { "kind": { "const": "environment" }, "variable": { "$ref": "#/$defs/safeEnvironmentName" }, "path": { "$ref": "#/$defs/relativePath" } } - }, - { - "required": ["kind", "command"], - "properties": { "kind": { "const": "command" }, "command": { "$ref": "#/$defs/command" } } - }, - { - "required": ["kind", "runtime"], - "properties": { "kind": { "const": "runtime" }, "runtime": { "$ref": "#/$defs/toolID" } } - } - ] - }, - "launchTokens": { - "type": "array", "minItems": 1, "maxItems": 32, - "prefixItems": [ { "const": "${executable}" } ], - "items": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^(?:\\$\\{(?:home|executable|entry|runtimeArguments|arguments)\\}|[^$\\u0000-\\u001f\\u007f]+)$" } - }, - "toolDefinition": { - "type": "object", "additionalProperties": false, - "required": ["id", "displayName", "executableLayouts", "executables", "version", "commands", "launch"], - "properties": { - "id": { "$ref": "#/$defs/toolID" }, - "displayName": { "$ref": "#/$defs/label" }, - "icon": { "$ref": "../../plugin-manifest.schema.json#/$defs/uiIcon" }, - "executableLayouts": { "type": "array", "minItems": 1, "maxItems": 8, "items": { "$ref": "#/$defs/executableLayout" } }, - "executables": { "type": "array", "minItems": 1, "maxItems": 32, "items": { "$ref": "#/$defs/candidate" } }, - "version": { "$ref": "#/$defs/version" }, - "homeLayouts": { "type": "array", "maxItems": 8, "items": { "$ref": "#/$defs/homeLayout" } }, - "discovery": { "type": "array", "maxItems": 16, "items": { "$ref": "#/$defs/discovery" } }, - "homeVariable": { "$ref": "#/$defs/safeEnvironmentName" }, - "commands": { "type": "array", "minItems": 1, "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/command" } }, - "wrappers": { "type": "array", "maxItems": 8, "items": { "$ref": "#/$defs/wrapper" } }, - "exports": { - "type": "array", "maxItems": 16, - "items": { - "type": "object", "additionalProperties": false, "required": ["name", "value"], - "properties": { "name": { "$ref": "#/$defs/safeEnvironmentName" }, "value": { "enum": ["${home}", "${executable}"] } } - } - }, - "pathPrepend": { - "type": "array", "maxItems": 8, "uniqueItems": true, - "items": { "type": "string", "maxLength": 256, "pattern": "^\\$\\{home\\}(?:/(?!\\.{1,2}(?:/|$))[^/\\\\:$*?\\u0000-\\u001f\\u007f]+)*$" } - }, - "launch": { "$ref": "#/$defs/launchTokens" } - } - }, - "provider": { - "type": "object", "additionalProperties": false, "required": ["id", "displayName", "tools"], - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "displayName": { "$ref": "#/$defs/label" }, - "tools": { "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": { "$ref": "#/$defs/toolID" } }, - "watchFiles": { - "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, - "items": { - "type": "object", "additionalProperties": false, "required": ["pattern", "reader"], - "properties": { "pattern": { "$ref": "#/$defs/boundedGlob" }, "reader": { "enum": ["native", "compatible"] } } - } - } - } - }, - "backend": { - "type": "object", "additionalProperties": false, "required": ["launch"], - "properties": { - "launch": { - "oneOf": [ - { - "type": "object", "additionalProperties": false, "required": ["kind", "path"], - "properties": { "kind": { "const": "packaged" }, "path": { "$ref": "#/$defs/relativePath" } } - }, - { - "type": "object", "additionalProperties": false, "required": ["kind", "tool", "packagedRuntime", "entry"], - "properties": { - "kind": { "const": "runtime" }, "tool": { "$ref": "#/$defs/toolID" }, - "packagedRuntime": { "$ref": "#/$defs/relativePath" }, "entry": { "$ref": "#/$defs/relativePath" }, - "runtimeArguments": { "type": "array", "maxItems": 16, "items": { "type": "string", "maxLength": 256, "pattern": "^[^$\\u0000-\\u001f\\u007f]*$" } } - } - } - ] - }, - "arguments": { "type": "array", "maxItems": 16, "items": { "type": "string", "maxLength": 256, "pattern": "^[^$\\u0000-\\u001f\\u007f]*$" } } - } - }, - "consumer": { - "type": "object", "additionalProperties": false, "required": ["id", "requiresTools", "onToolchainChange"], - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "requiresTools": { - "type": "array", "minItems": 1, "maxItems": 16, - "items": { "type": "object", "additionalProperties": false, "required": ["tool"], "properties": { "tool": { "$ref": "#/$defs/toolID" }, "minimumVersion": { "type": "string", "pattern": "^[0-9]+(?:\\.[0-9]+){0,7}$", "maxLength": 64 } } } - }, - "onToolchainChange": { "enum": ["restart", "resync", "nextRun"] }, - "launch": { - "type": "object", "additionalProperties": false, "required": ["kind", "tool", "entry"], - "properties": { - "kind": { "const": "runtime" }, "tool": { "$ref": "#/$defs/toolID" }, "entry": { "$ref": "#/$defs/relativePath" }, - "runtimeArguments": { "type": "array", "maxItems": 16, "items": { "type": "string", "maxLength": 256 } } - } - }, - "runtimeDependencies": { - "type": "array", "maxItems": 16, - "items": { "type": "object", "additionalProperties": false, "required": ["tool"], "properties": { "tool": { "$ref": "#/$defs/toolID" } } } - } - } - } - } -} diff --git a/shared/contracts/plugins/v1/toolchains/proposal/manifest.md b/shared/contracts/plugins/v1/toolchains/proposal/manifest.md deleted file mode 100644 index 2f45a1c476..0000000000 --- a/shared/contracts/plugins/v1/toolchains/proposal/manifest.md +++ /dev/null @@ -1,318 +0,0 @@ -# Toolchain manifest contributions (proposal) - -**Status: full proposal, not a claim of native host support.** The implemented -Core subset is specified by [the active contract](../README.md). This -document follows the -[final scope of PR #1581](https://github.com/1lck/Lithe-IDEA/pull/1581#issuecomment-6541366112). -It is a host contract draft, not a plugin author tutorial. The current manifest -schema now admits only that subset; generic consumer/runtime migration remains -unsupported. Do not install these projection examples as `plugin.json`. - -[`manifest-contributions.schema.json`](manifest-contributions.schema.json) is a -standalone **review projection**, not a replacement manifest schema and not an -`allOf` extension of the current closed schema. Its `proposalVersion`, `pluginID` -and `consumers` envelope fields organize fixtures; they are not new production -manifest fields. `toolDefinitions`, `toolchainProviders`, and the consumer fields -below would be integrated into the existing manifest only when Core and the -corresponding host adapters implement and advertise the capabilities. The -production `schemaVersion: 2` and existing API levels are not changed by this -proposal. A future implementation must not silently accept these fields under an -older capability. - -## Three contributors, one tool registry - -| Contributor | Owns | Does not own | -| --- | --- | --- | -| Tool definition | Identity, executable layout, version probe, bounded discovery, candidate order, exported environment, argv launch template | Version-manager aliases, private configuration formats, provider priority rules | -| Toolchain provider | Installed tools and project/global requirements, computed by its own backend or upstream manager | Tool definitions, arbitrary command replacement, host UI or resolver code | -| Consumer | Explicit tool references and minimum versions, directory, response to a changed selection | Discovery paths, alternate secret selection chains | - -There are **no host-owned tool definitions, built-in definition data files, -short-name aliases, or tool-name switches**. Java support defines `java`; a Maven -plugin defines `maven`. Node, Python and other official plugins migrate only when -they supply their own definitions. This draft does not remove existing behavior -or authorize new hardcoded fallbacks during that staged migration. - -The enabled registry is populated from installed, enabled plugins. A namespaced -definition ID is `:`; a plugin cannot define another -plugin's namespace. Short IDs such as `java` are reserved for packages whose -official identity the **host independently verified during installation**. A -manifest's vendor or signature-policy text is not evidence of that identity. -Community providers may answer short IDs; defining and answering are different -authorities. The first registered owner of an ID remains the owner; a later -duplicate is rejected, never used as an override. Stable enumeration order makes -diagnostics reproducible, not a priority policy for conflicting runtime facts. - -## Proposed capability admission - -The names and versions here are review assignments, not host advertisements: - -| Capability | Required exactly when | -| --- | --- | -| `toolDefinitions: 1` | The plugin contributes definitions | -| `toolchainProviders: 1` | The plugin contributes providers | -| `executableResolution: 2` | A definition uses the new shared candidate projection, including `toolchain` or `setting.layer` | -| `toolchainConsumers: 1` | A consumer uses generic tool references and change policy | -| Existing `backend: 2`, `serverProcess: 1` | A provider uses the plugin's backend | - -Each actual contribution still declares its existing language/build/run/debug -capabilities and permissions. The review envelope lists only capabilities relevant -to this proposal. Missing **or unused** capabilities are rejected, just as the -current manifest contract requires. There is no broad new API-level increment to -pretend that an unimplemented launch adapter supports the draft. - -## Tool definitions - -The [Java-support-style projection](../../fixtures/toolchains/proposal/java-tool-definition.json) supplies -the tool name and every executable policy. Its short ID is valid only in an -independently official installation context; the fixture itself is not signed. - -| Field | Draft meaning and limits | -| --- | --- | -| `id`, `displayName`, `icon` | Tool ID, one-line label of at most 64 characters, and existing shared `uiIcon` format; normal icon package checks still apply | -| `executableLayouts` | 1-8 exact tool-home-relative executable paths, optionally filtered by platform, in fallback order; native executable checks apply | -| `executables` | 1-32 **resolver candidates**, not executable filenames; see below | -| `version` | 1-8 flag-shaped literal probe arguments and a Rust-regex-compatible expression of at most 512 bytes; first capture yields the normalized version | -| `homeLayouts` | At most 8 ordered, single-component name matches mapping a selected installation directory to `.` or a relative home suffix; for example `*.jdk` maps to `Contents/Home` | -| `discovery` | At most 16 bounded scans with root `home`, named `environment`, or an absolute system directory; the host executes data, not a tool-specific scanner. This primitive set is frozen; shapes it cannot express use a provider | -| `homeVariable`, `commands` | Metadata identifying the login-shell input and 1-8 bare commands belonging to this tool | -| `wrappers` | At most 8 exact project-relative wrapper paths, required marker files, and platform filters | -| `exports` | At most 16 unique environment names, each assigned exactly `${home}` or `${executable}` | -| `pathPrepend` | At most 8 paths under `${home}`; no traversal, shell interpolation, or replacement of PATH | -| `launch` | At most 32 argv tokens; the first is exactly `${executable}` | - -`homeLayouts` applies only when normalizing a manual selection or a system -discovery match. A provider's `installations[].home` is already the final tool -home; applying a layout again could append the same suffix twice. - -`executableLayouts` and `executables` deliberately have different roles. The first -locates the executable **within an already chosen installation**. The second -selects which source may supply that installation or executable. A candidate does -not gain permission to launch a same-named command for another tool. - -`homeVariable`, `commands`, and `wrappers` are checked metadata for the same -candidate plan, **not alternate discovery or launch paths**: every `environment` -candidate must use the declared home variable and one declared executable layout; -every `command` must belong to `commands`; every wrapper `projectFile` must match -one wrapper, including its `requires` markers and platform applicability. A -`projectFile` with `projectKind: "environment"` instead declares its own exact -`path`, `requires` markers and mandatory project-relative `home`; its path must -equal that home plus an applicable executable layout. It is not a wrapper or -an external provider installation. Conversely, -declared wrappers and shell inputs must have their corresponding candidate(s). -Any mismatch rejects the definition, so the UI and resolver cannot drift. Only -the candidate plan is executed. An implementation may normalize these references -to one internal representation; it must not maintain two independent pipelines. - -### Version probing and argv safety - -The first version capture must match `[0-9]+(?:\.[0-9]+){0,7}`, at most 64 bytes. -Core compares unsigned numeric components, padding absent trailing components -with zeros. The Java fixture's optional `1.` prefix is declared by that plugin: -`1.8.0_422` produces `8.0`, while `21.0.4` produces `21.0.4`. The host has no Java -8 branch. Provider display text is provenance; auto-selection uses the validated -probe capture, not a parsed alias such as `temurin-21`. - -The host caps probe stdout and stderr at 64 KiB each, applies a 5-second monotonic -deadline, and cancels the child tree on timeout or cancellation. A failed probe -does not provide a sortable version. It cannot satisfy a consumer's minimum -version by assumption. Equal normalized versions with distinct homes are shown -as an explicit ambiguity requiring a choice; no plugin ID or directory name wins. - -Arguments match `-x`/`--long-flag` shape only; no positional commands, `=value`, -scripts, or environment templates. This is a syntax restriction, **not proof that -an executable is side-effect-free**. Installation review must list each executable, -probe arguments, scan roots, export names, and provider backend the package can -run. A future tool whose upstream version command cannot satisfy this restriction -needs a reviewed generic extension; do not add a tool-name exception. - -Launch is an argv vector, never a shell command. `${home}`, `${executable}` and -`${entry}` are whole scalar tokens; `${runtimeArguments}` and `${arguments}` are -whole-token list expansions, at most once each. No inline interpolation, unknown -variable, shell evaluation, shell launcher, or empty executable is allowed. -`${entry}` is independently validated inside the consumer package. Empty argument -lists expand to zero tokens. Arguments and paths with spaces remain single argv -elements. On Windows the existing platform argv-quoting adapter owns native -serialization; the plugin cannot supply shell quoting. - -Exports reject `PATH`, `DYLD_*`, and `LD_*` names. Existing host process-environment -filtering still applies; the permission does not authorize injecting credentials, -loader variables or arbitrary environment expressions. `pathPrepend` is checked -after canonicalization: symlinks may not escape the selected home. Exports from a -rejected candidate never leak into the next candidate's environment. - -## One candidate resolver, four product layers - -The draft extends the -[existing candidate model](../../README.md#shared-executable-candidates-executableresolution-12) -rather than building a second runtime resolver: - -- `setting` gains mandatory `layer: "project" | "global"` in this projection. - `settingID: "executable"` names a generic framework slot, not a plugin private - path-setting ID. Project values come from nearest-ancestor `pins`; global values - are Lithe-owned application preferences. An invalid nonempty fixed/default - value is `terminalFailure`, never an automatic fallback. -- `toolchain` gains `scope: "project" | "global" | "installed"`. The tool is the - containing definition; a candidate cannot inject facts for a different ID. -- Existing `projectFile`, `environment`, `command`, and `runtime` source kinds - retain their source meanings; `runtime` references an explicit tool ID and - dependency cycles are rejected. A `projectFile` only runs through the existing - consumer trust/explicit-user-action boundary, never passive discovery/probing. -- Under the new capability only, `projectFile.projectKind` distinguishes - `wrapper` (default, executable-only) from `environment` (explicit `home`). A - wrapper produces `home: null`; it never invents a tool home from the workspace - root. The environment's home and program are relative to the candidate's - source-directory anchor, which also determines project specificity. The - existing `environment` kind still means an external login-shell variable, - not a project environment. Old executable-resolution v1 is unchanged. -- IDs, signed priority, insertion anchors, deterministic topological ordering, - platform filtering, and validation of the whole set remain the current model. - Anchors cannot cross contributions or plugins. Consumer adjustments apply only - to their own candidate set, not to another tool's definition or global registry. - -The normalized candidate list must preserve these four barriers: - -1. Fixed: nearest ancestor pin in the chosen directory, invalid means terminal. -2. Project: nearest applicable directory, then wrapper > environment > declaration; - for the same source file native reader > compatible reader. Equal claims are - an explicit unresolved conflict, not an ID-based tie-break. Uninstalled requests - return `skip` **with structured reason**, and remain visible as unsatisfied. -3. Global: Lithe default > unambiguous provider global > login shell. Conflicting - provider globals require a user choice; until then, skip them with a conflict - reason and use login-shell candidates. No hidden provider-ID precedence. -4. Automatic: eligible installations from both routes, provider answers and the - generic system scanner driven only by `discovery`; one canonical home counts - once. Select the highest valid normalized version satisfying the consumer - minimum, excluding manually added pin-only homes. - -Array order is the default source order inside these barriers. Ordering metadata -must not move candidates across a barrier or reverse project specificity/reader -precedence; such a definition is rejected. Tool definitions may omit unavailable -sources, but cannot silently redefine the four-layer policy. Absence is `skip`, -not a fabricated value. See the adjacent protocol/selection proposal for exact -query results and reason shapes; no current `skip` consumer changes in this stage. - -## Providers, file watching, and bootstrap - -The [mise projection](../../fixtures/toolchains/proposal/mise-provider.json) declares `id`, `displayName`, -`tools`, and `watchFiles`. `watchFiles` is optional: without it a provider reports -installations only and any returned statement is rejected. That shape lets the -plugin defining a tool answer for its own installations when declarative -`discovery` cannot express them. Provider IDs are local to the plugin/session; registry -identity is the pair `(verified pluginID, provider id)`. There are at most eight -providers, each naming 1-32 tool IDs. The query set is the intersection of requested -and declared IDs; this may include dormant IDs solely for requirement diagnostics. -Only its subset with enabled definitions can supply executable candidates. -Dormant unknown IDs do not inject commands; the host may preserve the requirements -as diagnostics so it can offer enabling the installed definition plugin. A -consumer with a missing required definition cannot launch. - -`toolchains.provide` means the backend may supply candidates which influence the -program Lithe launches, **not merely read directories**. Its installation review -must say so explicitly. It does not authorize installation/download, network, -trusting workspace code, reading secrets, writing native configuration, or -replacing arbitrary commands. Normal backend permission and sandbox restrictions -continue to apply. - -`watchFiles` entries are `{pattern, reader: "native" | "compatible"}`. The host -validates each project statement's source and reader against the declaration; -returned facts cannot promote a compatible reader to native. Native reader -ownership conflicts are explicit and not resolved by plugin registration order. -Patterns are relative, at most eight path components and 256 bytes. Only `*` -inside a single component is supported; no `**`, `?`, classes, braces, traversal, -absolute paths or following a symlink outside the watched root. The host applies -each pattern directly at the selected directory and at most 64 ancestor -directories, including enclosing configuration above the opened workspace; no -recursive `**` scan is implied. Providers also return potential external/global -inputs, including missing paths, in `watch`. Watching an enclosing source does -not expand the workspace identity or editable pin scope. Expansion across the -whole query round is bounded to 256 watch roots and 4,096 enumerated entries, -not just successful matches, as well as the existing workspace scan budget. -Limit hits are reported, never treated as a complete snapshot. The host observes -the parent of missing matches so creation and deletion also invalidate the snapshot. - -Any declared watched file or returned absolute input change cancels the generation -and re-queries **all enabled providers**; no mixed-generation incremental result -is published. Remote workspaces query their own authority or are explicitly -unsupported, never the local machine's managers. Providers own private version-file -formats, `lts`, `latest`, `system`, configuration hierarchy and manager semantics. -The host neither parses those files nor independently guesses their precedence. - -The provider uses the plugin's existing `backend`; no per-tool process framework -and no LSP language feature is required. It must negotiate the typed toolchain -capability over the existing base JSON-RPC transport before receiving queries. -`toolchainProviders: 1` proposes an additional **host-query startup trigger** for -that backend. Existing `backend: 2` alone does not start the fixture today: its -current view/command/`onWorkspaceOpen` admission rules remain unchanged until this -new capability is implemented. The future host starts at most one backend for -the plugin when it needs a query, not one process per tool or directory; after -startup it negotiates support before dispatch. A query is not an implicit -`onWorkspaceOpen` activation declaration. -The backend is either `launch.kind: "packaged"` (a signed/package-checked native -executable), or `launch.kind: "runtime"` with mandatory `packagedRuntime`. The -latter starts exactly that packaged executable and uses the referenced active -tool definition's argv launch template; it never calls the resolver for its own -runtime. This also holds when a plugin provides installations of a tool it -defines: the backend never runs on the tool it is discovering. The launch definition must be registered independently of provider -queries and must be available before backend start. Missing definitions or -dependency cycles fail admission/start explicitly. A packaged runtime is not a -host fallback if a definition is disabled. Native packaged executables are the -recommended bootstrap, and are what the mise fixture demonstrates. - -Provider backends stay within the current restart-required activation policy. -This proposal does not relax live activation. Disabling/uninstalling cancels -queries, invalidates generations, stops the owned backend, and removes active -definitions, dependent consumer rows, exports, and plugin settings together. -Local pins remain stored for re-enable; dormant pins are not allowed to re-create -an executable definition or export. - -## Consumer field projections - -`consumers[]` exists only in this review envelope. Its fields project additions -onto existing language-server, build-provider, execution-provider, debug-adapter -or run-configuration contributions: - -- `launch.kind: "runtime"` accepts any registered `tool` ID; no `java | node` - enum or inferred filename mapping remains on the migrated path. -- `runtimeDependencies[].tool` likewise accepts any registered ID, with existing - uniqueness/no-duplicate-launch-runtime checks retained. -- `requiresTools: [{tool, minimumVersion?}]` explicitly lists every tool a - consumer uses. `launch.tool` and dependencies must be included, each once. -- `onToolchainChange` is `restart`, `resync`, or `nextRun`; an absent corresponding - lifecycle adapter makes that contribution unsupported, not silently ignored. - -Definitions must be known for a required consumer to launch. A syntactically valid -ID alone grants no authority. If its installed defining plugin is disabled, show -the requirement, known owner, and plugin-page enable action; do not launch a stale -pin. Optional provider facts for missing definitions remain diagnostic data only. - -The environment/source page skeleton renders only enabled registry data, scoped -to each directory/module. Tool/plugin-specific settings are contributed on that -plugin's settings page. Manually added homes live in platform application storage, -can only be pinned, never enter auto-selection, and are validated through the -same declared layouts/probe before a confirmed pin is written. - -## Validation boundaries and fixtures - -The standalone schema proves shape and bounded syntax only. Registry semantics -also check capability exactness, ownership, references, candidate graph/layers, -regular-expression validity, version captures, metadata consistency and template -usage. The native host separately proves verified signature provenance, package -containment, canonical paths, executable status, workspace exclusion, cancellation -and limits. None of those checks can be replaced by JSON Schema. - -[`manifest-rejections.json`](../../fixtures/toolchains/proposal/manifest-rejections.json) separates these -phases. Each case applies RFC 6902 `patch` to its `base`; `context` is **host test -input**, never a plugin field. `phase: "schema"` must fail schema validation; -`registry` and `hostTrust` cases intentionally remain valid JSON shape and describe -the later semantic rejection. Codes reuse existing `plugin.manifest.invalid`, -`plugin.permission.undeclared`, and `plugin.template.invalid` categories; `reason` -is structured diagnostic detail, not a second numeric JSON-RPC error mapping. -These are proposed acceptance/rejection vectors, not tests of current Core. - -The implementation stage must add executable Core and host tests before declaring -support, then run both SDK end-to-end suites for actual backend protocol changes. -After merging contract changes, synchronize the byte-for-byte mirror using -`node scripts/contract.mjs sync --revision ` in -[lithe-plugin-docs](https://github.com/Lithe-IDEA/lithe-plugin-docs); do not hand-edit -the mirror or place SDK parsing helpers in the Lithe host. diff --git a/shared/contracts/plugins/v1/toolchains/proposal/protocol.md b/shared/contracts/plugins/v1/toolchains/proposal/protocol.md deleted file mode 100644 index 532cc4464f..0000000000 --- a/shared/contracts/plugins/v1/toolchains/proposal/protocol.md +++ /dev/null @@ -1,371 +0,0 @@ -# Proposed toolchain query, resolution and local pins - -**Status: broader proposal; the macOS settings pages query providers read-only, and SDK support is not implemented.** -The Core-only validation/resolution subset is specified by -[the active contract](../README.md). These files remain review vectors, -not an advertised native capability or an SDK guide. The existing local-toolchain -writer/schema is unchanged. The design follows the -[latest #1581 review](https://github.com/1lck/Lithe-IDEA/pull/1581#issuecomment-6541366112). - -## Ownership and transport - -A tool definition says how to recognize, probe and use a tool. A provider -answers which installations and requirements exist. A consumer explicitly -references a tool ID. The host supplies query orchestration and one resolver; -it never parses a manager's version files, aliases or requested-version syntax. -There are no built-in tool definitions, short-name aliases or tool-name -branches. The generic system scanner is a host provider whose entire scan plan -comes from enabled definitions' `discovery` contributions. Plugin providers -cover installation shapes those frozen scan primitives cannot express; both -routes feed one automatic candidate set, and one canonical home counts once. - -The logical **and wire** method is `toolchains/query`. This independent typed -extension reuses the existing LSP base JSON-RPC transport and ServerProcess -lifecycle, not the language-intelligence domain. A toolchain backend need not -declare a language, language server, Build provider or private callable -function. Initialization offers `experimental.litheToolchains: {versions: [1], -providers: ["mise"]}`; the backend selects `{version: 1, providers: ["mise"]}`. -The host intersects the returned local provider IDs with the verified manifest. -An unoffered version, duplicate/undeclared provider ID, or malformed answer fails -negotiation rather than enabling a partial registry; no query is sent before -negotiation succeeds. -This draft does not upgrade or relax any existing `litheFunctions` version. - -An installed, enabled provider requires `toolchains.provide` and explicit -installation review of the programs it may invoke. This permission can affect -which external program the host launches; it is not described as a harmless -filesystem read. Plugin identity, signature/trust, package digest and enabled -state come from the host registry, never from a reply. The provider's stable -identity is `pluginID:providerID`; the unqualified `providerID` on the wire is -bound to that already authenticated session. - -## Query and reply - -[`query.schema.json`](query.schema.json) defines JSON-RPC envelopes. A request -contains `operationID`, `generation`, `providerID`, `authority`, `context`, -`workspace`, and `tools`. The host round generation also applies to global-only -queries, which have no workspace context. Tools are unique and sorted by ID. `workspace: null` asks only about -global state. Otherwise `workspace.root` is the absolute native root in the -query's authority, `rootID` is the host's opaque identity, and `directory` is -the directory being resolved relative to that root (`.` for the root). A -non-null workspace requires the existing `{contextID, generation}` host context -reference. These native paths are transient machine-local bindings, not portable -project identities. The opened workspace, not its containing repository, sets -the default scope. - -The host queries the intersection of requested tool IDs and the provider's -declared `tools`. IDs with no enabled definition may still be queried for -display-only requirement diagnostics. They cannot create installations, exports, -launches or tool rows. IDs outside that intersection are discarded entirely. -A failed or empty provider reply is not proof that there is no requirement. - -A successful result echoes `operationID`, `generation` and `providerID`, then returns: - -| Field | Meaning | -| --- | --- | -| `installations` | `{tool, version, home}` for external installations; `home` is absolute, `version` is provider display data, not a manager selector interpreted by Core. | -| `statements` | `{tool, scope, source, request, installation, kind, reader}` requirements; `request` is opaque, `installation` is a listed home or `null` for unmet. | -| `watch` | Absolute native input paths to fingerprint, including absent inputs and installation roots. This does not authorize reading contents, executing or writing these paths. | - -`scope` is `project` or `global`; `source` is the absolute originating file. -`kind` is `environment` or `declaration`. Wrappers are separate `projectFile` -candidates supplied by the tool definition, never provider installations. -`reader` is `native` or `compatible`, matching that file's declared -`watchFiles[].reader`; a backend cannot claim native ownership arbitrarily. -If more than one enabled provider declares native ownership of the same source, -the host reports an unresolved conflict rather than sorting plugin IDs. - -For a project statement, match the declared watch pattern relative to each -candidate source-directory anchor, with at most 64 ancestors of the requested -native directory considered. The matching anchor determines project specificity, -not the source file's physical parent: `.mise/config.toml` at `/project` belongs -to `/project`, not `/project/.mise`. If more than one pattern/anchor matches the -same canonical source, use the nearest matching anchor and then native-reader -precedence; indistinguishable conflicting claims remain explicit conflicts. -An upstream may identify an enclosing project file above the opened workspace; -its absolute source is evidence only and never expands the workspace identity -or the editable pins scope. Global sources need not be ancestors. A global-only -query cannot return project statements. `installation: null` means the manager -knows the request but cannot supply a usable installed choice. The host does not -translate `lts`, `system`, `ref:`, `prefix:` or any other request syntax. - -## Validation and limits - -The host passes untrusted replies through Core's deterministic shape and policy -validation before publishing them. Native adapters supply canonical paths, -file identities, existence, type and executable evidence; Core must not assume a -string prefix or JSON Schema proves any of these facts. Every candidate is -revalidated before launch to close stale-cache and symlink replacement windows. -The eventual launch adapter retains its existing atomic/native identity checks. - -- A reply is at most 1 MiB of UTF-8 JSON: at most 512 installations, 256 - statements and 256 watch paths. Each native path is at most 4096 characters; - operation/tool/provider identifiers and request strings have schema bounds. - Oversized replies fail as a whole. Deduplication cannot hide a limit breach. -- Installation homes are absolute directories. Executables come only from the - enabled tool definition's `executableLayouts`; the provider cannot replace - them with arbitrary argv, executables or environment variables. The selected - executable must exist, be a regular executable in the current authority, and - remain inside the canonical home. Providers return the final tool home; - `homeLayouts` applies to manual/system discovery inputs, not a second rewrite - of a provider home. -- Neither a provider installation's canonical home nor its executable may be - inside any root of the current workspace, including a symlink into a root. - An unsafe installation rejects the provider reply with `unsafePath`; it is - not silently promoted to a project wrapper. A provider can report an unmet - requirement instead of returning a project-contained home. -- A non-null statement installation must reference a validated installation of - the same tool in this reply. Unknown or disabled definition IDs lose all - installation authority. Their statements retain only bounded diagnostic facts - (`tool`, `scope`, `source`, `request`), never a usable path. -- Apply byte/count/string bounds and structural validation before filtering. - Then discard installation entries without requested, declared and active - definition IDs **before native filesystem validation**. Only remaining - installations undergo existence/executable/containment checks. Consequently, - an unknown tool's unusable home cannot invalidate an otherwise valid reply; - malformed JSON or a bounds violation still can. -- The host's installed-plugin registry supplies any missing-definition plugin - name and Enable action. Provider data cannot invent plugin names, install - links, enable targets or arbitrary commands. Enabling requires user action. -- Paths cannot contain controls, unnormalized parent traversal or mismatched - authority. Native case and volume rules determine equality and containment; - do not lowercase all paths. Canonical duplicates are collapsed after bounds - checking. Missing watch paths are valid; missing installations are not. -- Lists are normalized deterministically: installations by tool, canonical - home, then version; statements by tool, scope, canonical source, kind, reader, - request, then installation; watch paths by canonical identity. Sorting is not - a tie-breaking rule for conflicting provider requirements. - -Project wrappers and environments are **project code**, not exceptions to the -provider boundary. A tool definition's `projectFile`/wrapper candidate may -resolve a workspace-contained program only through the consumer's existing -explicit user-action/consent flow (there is no persistent workspace trust state -in the current product). Passive import or opening settings does not -execute wrappers or probe project environments. All resulting selections carry -`projectCode: true`, and automatic background consumers must reject them unless -their existing trust boundary explicitly permits execution. Returning -`kind: environment` from a provider cannot waive this rule. - -The new capability's `projectFile.projectKind` is `wrapper` by default or -`environment` with an explicit source-directory-relative `home`. The latter's -`path` must equal that home plus an applicable declared executable layout; no -tool-name or `bin`-parent heuristic supplies a home. Specificity uses the -candidate's source-directory anchor, not the executable's parent directory. -A project-environment candidate can be declared as -`{"kind":"projectFile","projectKind":"environment","home":".venv","path":".venv/bin/python"}`; -the tool's executable layout supplies `bin/python`, not the host. -A wrapper is executable-only and returns `selection.home: null`. It emits no -`${home}` exports or PATH entries; a launch template requiring `${home}` skips -with `homeUnavailable` rather than pretending the repository is an installation. - -Project environments may contain executable symlinks (for example a venv's -interpreter link to an external interpreter). This is a distinct project-code -policy, not a provider exception: the lexical home must remain within the -workspace, the lexical program must match its declared layout, and the adapter -must validate the resolved executable and the complete link chain's identities -at the authorized launch boundary. Preserve the declared lexical launch path; -substituting the canonical target can change environment semantics. A changed -link or missing native identity check rejects the launch. No link grants -background execution authority. Without the consumer's explicit-action/consent -admission, skip with `projectCodeRequiresAction`, never `requestedNotInstalled`. - -## Query lifecycle, cache and watching - -Each query round captures authority, workspace/root/context generation, enabled -definition/provider digests, tool set, environment snapshot and a monotonic -round generation. It queries **every enabled provider** relevant to that round; -each request owns a unique operation ID and JSON-RPC ID. Replies must match -both IDs, round generation and the bound provider. A round publishes one atomic snapshot after -all requests finish, fail, or reach their deadlines. It can preserve a previous -valid snapshot while visibly loading, but cannot label old state as current. -Partial failures stay visible; absence of a reply never means an empty provider. - -The default per-query deadline is 5 seconds including backend startup; -host configuration can choose 100-30000 milliseconds. Cancellation sends -`$/cancelRequest` with the JSON-RPC ID and completes local ownership immediately. -An owned backend unresponsive to cancellation gets a bounded 2-second cleanup -grace before the existing process-tree termination path. A shared backend's -unrelated operations are not stopped to cancel one query. Session shutdown, -disable, uninstall or workspace replacement cancels all owned operations and -listeners; callbacks recheck generation before changing any state. Every -operation completes once. A reply after cancellation, timeout, generation -change or provider removal is discarded, never cached or published. - -The cache key includes the captured identities above. Its input fingerprint -combines returned `watch` paths with the manifest's `watchFiles` declarations. -The host watches parent directories and the bounded set of matching inputs so -creation, deletion, atomic replacement and rename are observable even when a -file did not exist in the previous reply. Missing/existing, type, identity, -size, modification time and canonical target are fingerprinted; native watcher -events invalidate even if a replacement happens to preserve metadata. A lost -watcher event/overflow invalidates the round rather than claiming the cache is -fresh. No file contents or manager syntax are parsed by this cache. - -Manifest patterns have at most eight components and no recursive `**`; matching -is bounded to workspace/context ancestor directories and explicitly returned -absolute watch roots. The host caps each round at 256 watch roots and 4096 -enumerated directory entries, not only successful matches; exceeding this -produces a visible limit failure, not a recursive scan -of the user's home. Multiple file events may coalesce in one scheduling turn. -**Any** matching file change invalidates the entire snapshot, cancels a pending -round, and re-queries **all** providers. There are no per-provider or per-tool -incremental updates. Providers remain responsible for returning every external -global/configuration input they read or would consult if created, including -absent inputs during a global-only query. Returning only existing files is not -sufficient to detect the first creation of a higher-priority configuration. - -Local and remote authorities never share caches, shell environment, native -paths or providers. The first implementation is local only. A remote request -fails with `authorityUnsupported` until that authority has an actual query and -filesystem adapter. It never probes the local placeholder, shell, local pins -or local installations as a fallback. Static definitions may be reused only as -inert data, not as permission to run a local scanner for a remote workspace. - -## One four-layer resolver - -[`resolution.schema.json`](resolution.schema.json) describes normalized output, -not a second resolver implementation. Tool definitions supply their ordered -`executables` candidates; the reusable executable resolver extends its existing -`skip`, `resolved`, `terminalFailure` result. `setting.layer` distinguishes -project pins from application defaults; `toolchain.scope` is `project`, -`global`, or `installed`. Existing per-owner candidate ordering and insertion -rules remain; a consumer cannot reorder another plugin's candidate set or move -a lower layer above a pin. The following layer order is invariant: - -| Layer | Selection and failure behavior | -| --- | --- | -| 1 Fixed | Nearest ancestor pin for this directory and tool; invalid/missing/unexecutable/probe-failing/incompatible nonempty pin is `terminalFailure`, never fallback. | -| 2 Project | Nearest originating directory first; within it wrapper > environment > version declaration. For the same source, native reader > compatible reader. Uninstalled requirements are `skip` with a reason, retained as unmet while lower layers continue. | -| 3 Global | Lithe's explicitly saved default > selected provider global statement > login-shell home variable/commands. Conflicting provider globals require user choice; before choosing, skip provider globals and follow login Shell, not plugin-ID order. | -| 4 Automatic | Highest compatible probed installed release from all providers plus the definition-driven system scanner; excludes manual-only registrations. | - -Project distance is compared **before** project candidate category: a child -version file beats a wrapper in a more distant parent. After native/compatible -precedence, differing equally specific project statements are an explicit -conflict requiring user choice; while unresolved they skip with a conflict -reason. Equal normalized installations are deduplicated, not conflicts. Chosen -provider identities are application-owned preferences scoped by authority, -workspace/directory and tool, never implicit ID ordering. - -Consequently, project sources must expand into one bounded project-layer plan -before its first candidate is attempted. A sequential implementation that -searches every ancestor for the wrapper candidate before considering the next -`toolchain` candidate violates this contract. The existing resolver consumes -the normalized plan; source enumeration is read-only and must not execute or -probe shadowed wrappers/environments to establish their precedence. - -The `skip.reason` object is mandatory. `requestedNotInstalled` includes `tool`, -`provider`, `source`, and opaque `request`; `definitionUnavailable` always includes -`tool` and also includes `source` and `request` when it describes an observed -statement rather than an inactive definition or dormant pin. `providerConflict` is accompanied by a -`conflicts` entry listing qualified identities. Reasons are structured facts -rendered through shared i18n, not English sentences assembled in Core. A -successful fallback retains all applicable unmet requirements and conflicts. -Empty layers remain `empty` trace rows; layers masked by success are `shadowed` -and must not be probed just to populate the settings display. Expanders show -only the effective layer by default; Show All exposes the trace without -changing execution behavior. No selection means `skip`, not invented success. - -The version probe's declared regular expression supplies a normalized numeric -dotted release in its first capture (at most eight unsigned 32-bit components, -64 characters). Compare components numerically with absent trailing components -as zero; there is no Java-8, `v` prefix, vendor, alias or pre-release special -case. No valid comparable capture means not eligible for automatic selection; -an explicit choice still cannot claim to satisfy a consumer's declared minimum -without a successful probe. The provider's `version` is preserved as display -provenance, never trusted instead of the definition's probe. A wrapper may have -`selection.version: null` until the existing user-authorized execution flow can -probe it; consumers requiring a version cannot treat `null` as compatible. -If the newest normalized release has different canonical homes, automatic -resolution reports `ambiguousInstallation`, an `automatic` conflict listing -the distinct canonical homes, and a `chooseInstallation` action; it requires an -explicit choice; sorting homes or plugin IDs must not silently decide which binary runs. -Multiple reports of the same canonical home are one installation. - -Environment exports and PATH prepend entries are generated solely by enabled -definitions using the selected validated home/executable. A failed candidate's -environment is discarded before trying the next. Consumer `requiresTools`, -`launch.tool` and `onToolchainChange` bind directory-scoped usage and explain -whether a switch needs restart, resync or applies next run. Disabling a -definition withdraws its rows, consumers, exports and injected settings page; -it does not delete saved pins. Existing processes do not silently restart. -An executable-only wrapper can export `${executable}`, but cannot export or -prepend paths based on an absent home. Its consumer still owns wrapper-specific -launch arguments; it cannot silently reuse a runtime entry template requiring -installation facts that the wrapper does not provide. - -## Local pins and manual installations - -[`local.schema.json`](local.schema.json) is an isolated proposed extension of -the machine-local `.lithe/toolchains/local.json`, retaining `version: 1` and -legacy `toolchains` while adding `pins`. It does not modify the active v1 -schema or promise that an old host accepts new fields. The implementation phase -must gate writers on the new capability and migrate the corresponding readers -together; it must not write this draft shape for an old reader. - -`pins` maps normalized workspace-relative directory keys to tool-ID maps. -`.` is the opened root; absolute keys, backslashes, empty segments, `.`, `..` -segments and trailing separators are rejected. Dotted directory names are valid. -For `apps/web/src`, check that directory, `apps/web`, `apps`, then `.`, never -`apps/website` by string prefix. Each pin stores `{home, version, provider}`; -provider is a qualified identity or the reserved generic `host:manual` / -`host:system`, not a backend activation instruction. An inherited pin applies -until a nearer explicit value replaces it. Removing an override reveals the -ancestor; it does not delete that ancestor or the whole local file. - -Opening a project or settings only reads. An edit is a draft until explicit -Save/Apply confirms the target directory, tool and machine-local path. The -platform adapter checks the read revision and rejects stale writes, preserves -unrelated legacy entries/pins and permissions, atomically replaces the file, -then reads it again to display the effective choice. No write on focus loss, -navigation, startup, cancellation or unchanged inherited values. Read failures -are errors, not empty defaults. Reject symlinked local targets and any write -escaping the selected workspace's machine-local store. - -The first explicit save adds `/toolchains/local.json` exactly once to -`.lithe/.gitignore`, preserving other content and using conflict protection. -There is no transaction spanning two files: secure the ignore entry before -publishing the new local file; an ignore-write failure aborts the save. If local -file publication fails afterward, the harmless ignore entry may remain and the -draft remains unsaved. Neither target is created merely by opening a project. - -Manual Add validates a user-selected directory via `homeLayouts`, -`executableLayouts`, and the declared bounded version probe. It registers the -installation only in platform application storage, outside app bundles, -installed plugin packages and the project. Such registrations can be explicitly -pinned but do not participate in automatic discovery. A workspace-contained -directory is not converted into a trusted external installation by manual Add; -project wrappers/environments retain their separate project-code boundary. -Pins survive plugin disable/uninstall and reactivate only when that definition -is enabled again. No tool is downloaded, installed, trusted or repaired by query. - -## Stable failure mapping - -These are proposed additions, not assignments in the current runtime. Existing -host-service codes `-32010` through `-32021` keep their meanings. Toolchain -custom failures reserve `-32070` through `-32077` in the server-error range; -each numeric code has exactly one semantic code. JSON-RPC error `data` includes -`code`, `operationID`, and `providerID`; optional details are bounded to 4 KiB -serialized and contain no credentials. Native errors are translated to shared -i18n at the host boundary; plugin diagnostic text remains unmodified but is -not a policy selector. Timeout is not cancellation, and stale replies do not -generate a second completion. - -| JSON-RPC | Semantic code | Meaning | -| --- | --- | --- | -| `-32601` | `plugin.toolchain.methodUnavailable` | Query method/capability not negotiated. | -| `-32602` | `plugin.toolchain.invalidParams` | Malformed request or invalid declared input. | -| `-32800` | `plugin.toolchain.cancelled` | Caller cancelled the owned request. | -| `-32070` | `plugin.toolchain.permissionDenied` | Permission/review/enabled-state prerequisite absent. | -| `-32071` | `plugin.toolchain.authorityUnsupported` | No adapter for this authority. | -| `-32072` | `plugin.toolchain.timeout` | Monotonic local deadline exceeded. | -| `-32073` | `plugin.toolchain.providerUnavailable` | Backend failed to start, disconnected, or failed its upstream query. | -| `-32074` | `plugin.toolchain.invalidReply` | Malformed reply or inconsistent same-tool installation reference. | -| `-32075` | `plugin.toolchain.limitExceeded` | Reply/scan/watch budget exceeded. | -| `-32076` | `plugin.toolchain.unsafePath` | Invalid absolute path, containment or executable safety evidence. | -| `-32077` | `plugin.toolchain.operationExpired` | Context/generation was already invalid when a query was dispatched. | - -Request-local failures (missing installation, incompatible version, conflicting -requirements) are resolver reasons, not JSON-RPC transport errors. The query -fixture includes the numeric mapping; runtime tests, SDK end-to-end tests and -both native host lifecycle tests are required in later implementation phases. diff --git a/shared/contracts/plugins/v1/toolchains/proposal/query.schema.json b/shared/contracts/plugins/v1/toolchains/proposal/query.schema.json deleted file mode 100644 index a2c7f60893..0000000000 --- a/shared/contracts/plugins/v1/toolchains/proposal/query.schema.json +++ /dev/null @@ -1,621 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/toolchains/proposal/query.schema.json", - "title": "Proposed toolchain query protocol v1 (not implemented)", - "description": "Review-only JSON-RPC request, successful response or error response. Semantic validation and authority checks are specified in protocol.md.", - "oneOf": [ - { - "$ref": "#/$defs/request" - }, - { - "$ref": "#/$defs/response" - }, - { - "$ref": "#/$defs/errorResponse" - } - ], - "$defs": { - "identifier": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "toolID": { - "$ref": "manifest-contributions.schema.json#/$defs/toolID" - }, - "providerID": { - "$ref": "manifest-contributions.schema.json#/$defs/identifier" - }, - "nativePath": { - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^(?:/|[A-Za-z]:[\\\\/]|\\\\\\\\)", - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - }, - "description": "Absolute native path in this query's authority; canonical containment requires native filesystem evidence, not this lexical pattern." - }, - "directory": { - "type": "string", - "maxLength": 2048, - "pattern": "^(?:\\.|(?!\\.\\.?(?:/|$))[^/\\\\:]+(?:/(?!\\.\\.?(?:/|$))[^/\\\\:]+)*)$", - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - }, - "description": "Workspace-relative directory using /; . means the opened root. Each other component must not be . or ..; control characters are forbidden semantically." - }, - "authority": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "kind" - ], - "properties": { - "kind": { - "const": "local" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "kind", - "id" - ], - "properties": { - "kind": { - "const": "remote" - }, - "id": { - "$ref": "#/$defs/identifier" - } - } - } - ] - }, - "context": { - "type": "object", - "additionalProperties": false, - "required": [ - "contextID", - "generation" - ], - "properties": { - "contextID": { - "$ref": "#/$defs/identifier" - }, - "generation": { - "type": "integer", - "minimum": 1 - } - } - }, - "workspace": { - "type": "object", - "additionalProperties": false, - "required": [ - "rootID", - "root", - "directory" - ], - "properties": { - "rootID": { - "$ref": "#/$defs/identifier" - }, - "root": { - "$ref": "#/$defs/nativePath" - }, - "directory": { - "$ref": "#/$defs/directory" - } - } - }, - "params": { - "type": "object", - "additionalProperties": false, - "required": [ - "operationID", - "generation", - "providerID", - "authority", - "context", - "workspace", - "tools" - ], - "properties": { - "operationID": { - "$ref": "#/$defs/identifier" - }, - "providerID": { - "$ref": "#/$defs/providerID" - }, - "authority": { - "$ref": "#/$defs/authority" - }, - "context": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/context" - } - ] - }, - "workspace": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/workspace" - } - ] - }, - "tools": { - "type": "array", - "minItems": 1, - "maxItems": 64, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/toolID" - } - }, - "generation": { - "type": "integer", - "minimum": 1, - "description": "Host query-round generation, including workspace:null queries; echoed unchanged." - } - }, - "allOf": [ - { - "if": { - "properties": { - "workspace": { - "type": "object" - } - } - }, - "then": { - "properties": { - "context": { - "$ref": "#/$defs/context" - } - } - } - } - ] - }, - "installation": { - "type": "object", - "additionalProperties": false, - "required": [ - "tool", - "version", - "home" - ], - "properties": { - "tool": { - "$ref": "#/$defs/toolID" - }, - "version": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "home": { - "$ref": "#/$defs/nativePath" - } - } - }, - "statement": { - "type": "object", - "additionalProperties": false, - "required": [ - "tool", - "scope", - "source", - "request", - "installation", - "kind", - "reader" - ], - "properties": { - "tool": { - "$ref": "#/$defs/toolID" - }, - "scope": { - "enum": [ - "project", - "global" - ] - }, - "source": { - "$ref": "#/$defs/nativePath" - }, - "request": { - "type": "string", - "minLength": 1, - "maxLength": 512, - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "installation": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/nativePath" - } - ] - }, - "kind": { - "enum": [ - "environment", - "declaration" - ] - }, - "reader": { - "enum": [ - "native", - "compatible" - ] - } - } - }, - "result": { - "type": "object", - "additionalProperties": false, - "required": [ - "operationID", - "generation", - "providerID", - "installations", - "statements", - "watch" - ], - "properties": { - "operationID": { - "$ref": "#/$defs/identifier" - }, - "providerID": { - "$ref": "#/$defs/providerID" - }, - "installations": { - "type": "array", - "maxItems": 512, - "items": { - "$ref": "#/$defs/installation" - } - }, - "statements": { - "type": "array", - "maxItems": 256, - "items": { - "$ref": "#/$defs/statement" - } - }, - "watch": { - "type": "array", - "maxItems": 256, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/nativePath" - } - }, - "generation": { - "type": "integer", - "minimum": 1, - "description": "Host query-round generation, including workspace:null queries; echoed unchanged." - } - } - }, - "request": { - "type": "object", - "additionalProperties": false, - "required": [ - "jsonrpc", - "id", - "method", - "params" - ], - "properties": { - "jsonrpc": { - "const": "2.0" - }, - "id": { - "oneOf": [ - { "type": "integer", "minimum": 1 }, - { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^\\u0000-\\u001f\\u007f]*$" } - ] - }, - "method": { - "const": "toolchains/query" - }, - "params": { - "$ref": "#/$defs/params" - } - } - }, - "response": { - "type": "object", - "additionalProperties": false, - "required": [ - "jsonrpc", - "id", - "result" - ], - "properties": { - "jsonrpc": { - "const": "2.0" - }, - "id": { - "oneOf": [ - { "type": "integer", "minimum": 1 }, - { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^\\u0000-\\u001f\\u007f]*$" } - ] - }, - "result": { - "$ref": "#/$defs/result" - } - } - }, - "errorResponse": { - "type": "object", - "additionalProperties": false, - "required": [ - "jsonrpc", - "id", - "error" - ], - "properties": { - "jsonrpc": { - "const": "2.0" - }, - "id": { - "oneOf": [ - { "type": "integer", "minimum": 1 }, - { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^\\u0000-\\u001f\\u007f]*$" } - ] - }, - "error": { - "type": "object", - "additionalProperties": false, - "required": [ - "code", - "message", - "data" - ], - "properties": { - "code": { - "enum": [ - -32601, - -32602, - -32800, - -32070, - -32071, - -32072, - -32073, - -32074, - -32075, - -32076, - -32077 - ] - }, - "message": { - "type": "string", - "minLength": 1, - "maxLength": 1024 - }, - "data": { - "type": "object", - "additionalProperties": false, - "required": [ - "code", - "operationID", - "providerID" - ], - "properties": { - "code": { - "enum": [ - "plugin.toolchain.methodUnavailable", - "plugin.toolchain.invalidParams", - "plugin.toolchain.cancelled", - "plugin.toolchain.permissionDenied", - "plugin.toolchain.authorityUnsupported", - "plugin.toolchain.timeout", - "plugin.toolchain.providerUnavailable", - "plugin.toolchain.invalidReply", - "plugin.toolchain.limitExceeded", - "plugin.toolchain.unsafePath", - "plugin.toolchain.operationExpired" - ] - }, - "operationID": { - "$ref": "#/$defs/identifier" - }, - "providerID": { - "$ref": "#/$defs/providerID" - }, - "details": { - "type": "object", - "maxProperties": 8 - } - } - } - }, - "allOf": [ - { - "oneOf": [ - { - "properties": { - "code": { - "const": -32601 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.methodUnavailable" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32602 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.invalidParams" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32800 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.cancelled" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32070 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.permissionDenied" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32071 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.authorityUnsupported" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32072 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.timeout" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32073 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.providerUnavailable" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32074 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.invalidReply" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32075 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.limitExceeded" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32076 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.unsafePath" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32077 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.operationExpired" - } - } - } - } - } - ] - } - ] - } - } - } - } -} diff --git a/shared/contracts/plugins/v1/toolchains/proposal/resolution.schema.json b/shared/contracts/plugins/v1/toolchains/proposal/resolution.schema.json deleted file mode 100644 index 2a101c2000..0000000000 --- a/shared/contracts/plugins/v1/toolchains/proposal/resolution.schema.json +++ /dev/null @@ -1,362 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/toolchains/proposal/resolution.schema.json", - "title": "Proposed shared toolchain resolution result v1 (not implemented)", - "description": "Review-only normalized resolver output. Host-owned snapshots and native filesystem evidence are inputs; providers never produce this object directly.", - "type": "object", - "additionalProperties": false, - "required": [ - "operationID", - "tool", - "directory", - "outcome", - "trace", - "unmet", - "conflicts" - ], - "properties": { - "operationID": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "tool": { - "$ref": "#/$defs/toolID" - }, - "directory": { - "$ref": "#/$defs/directory" - }, - "outcome": { - "$ref": "#/$defs/outcome" - }, - "trace": { - "type": "array", - "maxItems": 256, - "items": { - "$ref": "#/$defs/trace" - } - }, - "unmet": { - "type": "array", - "maxItems": 256, - "items": { - "$ref": "#/$defs/reason" - } - }, - "conflicts": { - "type": "array", - "maxItems": 64, - "items": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "scope", - "providers", - "action" - ], - "properties": { - "scope": { - "enum": [ - "project", - "global" - ] - }, - "providers": { - "type": "array", - "minItems": 2, - "maxItems": 32, - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1, - "maxLength": 256 - } - }, - "action": { - "const": "chooseProvider" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "scope", - "providers", - "installations", - "action" - ], - "properties": { - "scope": { - "const": "automatic" - }, - "providers": { - "type": "array", - "minItems": 1, - "maxItems": 32, - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1, - "maxLength": 256 - } - }, - "installations": { - "type": "array", - "minItems": 2, - "maxItems": 512, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/nativePath" - } - }, - "action": { - "const": "chooseInstallation" - } - } - } - ] - } - } - }, - "$defs": { - "directory": { - "type": "string", - "maxLength": 2048, - "pattern": "^(?:\\.|(?!\\.\\.?(?:/|$))[^/\\\\:]+(?:/(?!\\.\\.?(?:/|$))[^/\\\\:]+)*)$", - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "toolID": { - "$ref": "manifest-contributions.schema.json#/$defs/toolID" - }, - "layer": { - "enum": [ - "pinned", - "project", - "global", - "automatic" - ] - }, - "reason": { - "type": "object", - "additionalProperties": false, - "required": [ - "code" - ], - "properties": { - "code": { - "enum": [ - "notConfigured", - "notFound", - "notExecutable", - "probeFailed", - "versionUnsatisfied", - "requestedNotInstalled", - "definitionUnavailable", - "providerConflict", - "unsafePath", - "providerUnavailable", - "ambiguousInstallation", - "homeUnavailable", - "projectCodeRequiresAction" - ] - }, - "tool": { - "$ref": "#/$defs/toolID" - }, - "provider": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "source": { - "type": "string", - "minLength": 1, - "maxLength": 4096 - }, - "request": { - "type": "string", - "minLength": 1, - "maxLength": 512 - } - }, - "description": "Structured facts, not assembled English. The host renders its shared i18n message with these arguments. Required facts per code are specified in protocol.md." - }, - "selection": { - "type": "object", - "additionalProperties": false, - "required": [ - "home", - "executable", - "version", - "provider", - "layer", - "projectCode", - "environment", - "pathPrepend" - ], - "properties": { - "home": { - "oneOf": [ - { "type": "null" }, - { "$ref": "#/$defs/nativePath" } - ] - }, - "executable": { - "$ref": "#/$defs/nativePath" - }, - "version": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 128 - }, - "provider": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "layer": { - "$ref": "#/$defs/layer" - }, - "projectCode": { - "type": "boolean" - }, - "environment": { - "type": "object", - "maxProperties": 32, - "additionalProperties": { - "type": "string", - "maxLength": 4096 - }, - "propertyNames": { - "$ref": "manifest-contributions.schema.json#/$defs/safeEnvironmentName" - } - }, - "pathPrepend": { - "type": "array", - "maxItems": 16, - "items": { - "$ref": "#/$defs/nativePath" - } - } - } - }, - "outcome": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "kind", - "selection" - ], - "properties": { - "kind": { - "const": "resolved" - }, - "selection": { - "$ref": "#/$defs/selection" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "kind", - "reason" - ], - "properties": { - "kind": { - "const": "skip" - }, - "reason": { - "$ref": "#/$defs/reason" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "kind", - "reason" - ], - "properties": { - "kind": { - "const": "terminalFailure" - }, - "reason": { - "$ref": "#/$defs/reason" - } - } - } - ] - }, - "trace": { - "type": "object", - "additionalProperties": false, - "required": [ - "candidateID", - "layer", - "state" - ], - "properties": { - "candidateID": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "layer": { - "$ref": "#/$defs/layer" - }, - "state": { - "enum": [ - "skip", - "resolved", - "terminalFailure", - "shadowed", - "empty" - ] - }, - "reason": { - "$ref": "#/$defs/reason" - }, - "source": { - "type": "string", - "minLength": 1, - "maxLength": 4096 - } - }, - "allOf": [ - { - "if": { - "properties": { - "state": { - "enum": [ - "skip", - "terminalFailure" - ] - } - } - }, - "then": { - "required": [ - "reason" - ] - } - } - ] - }, - "nativePath": { - "$ref": "query.schema.json#/$defs/nativePath" - } - } -}