From c9ec1447af882e9cbeb2210b0987daf9177df807 Mon Sep 17 00:00:00 2001 From: Jacob Magar Date: Mon, 25 May 2026 01:45:09 -0400 Subject: [PATCH 1/5] chore: branch baseline for code-mode-v2 drop-lab-actions --- ...026-05-25-code-mode-v2-drop-lab-actions.md | 904 ++++++++++++++++++ 1 file changed, 904 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-25-code-mode-v2-drop-lab-actions.md diff --git a/docs/superpowers/plans/2026-05-25-code-mode-v2-drop-lab-actions.md b/docs/superpowers/plans/2026-05-25-code-mode-v2-drop-lab-actions.md new file mode 100644 index 00000000..f7ab300d --- /dev/null +++ b/docs/superpowers/plans/2026-05-25-code-mode-v2-drop-lab-actions.md @@ -0,0 +1,904 @@ +# Code Mode v2 — Drop Lab-Action Support Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remove all Lab-action (`lab::.`) support from Code Mode in `crates/lab/src/dispatch/gateway/code_mode.rs` so the surface accepts only `upstream::::` IDs, with a structured `unknown_tool` envelope that tells agents to use `tool_execute` for Lab actions instead. + +**Architecture:** Deletion-heavy refactor over a single file (`code_mode.rs` ~1844 LOC) with light edits to `mcp/server.rs` dispatch handlers and `cli/gateway.rs`. The `CodeModeBroker::search/schema/execute` shape stays; only Lab-action code paths are removed. Stdio parent-broker protocol (`CodeModeRunnerInput`/`Output` enums) is untouched. Bead reference: `lab-elme3.1` under epic `lab-elme3`. + +**Tech Stack:** Rust 2024, tokio, rmcp, serde, cargo-nextest. Crate `crates/lab` (binary `labby`) feature `code_mode` (always on; the JS-runner feature gate comes in bead 5a). + +--- + +## File Structure + +| File | Responsibility | Edit type | +|---|---|---| +| `crates/lab/src/dispatch/gateway/code_mode.rs` | Code Mode broker, ID parsing, runner protocol, schema construction, TS bindings | Heavy delete + edit | +| `crates/lab/src/mcp/server.rs` | MCP tool registration + dispatch for `code_search`/`code_schema`/`code_execute` | Light edit | +| `crates/lab/src/cli/gateway.rs` | CLI Code Mode subcommands (`gateway code search|schema|exec`) | Light edit (if any `lab::` branches exist) | +| `crates/lab/tests/code_mode_runner.rs` | Stdio protocol integration tests | NO CHANGE (protocol unchanged) | + +Pre-implementation audit (Task 0): confirm we don't break any `destructive`-metadata enforcement elsewhere. The bead specifies removal of `CodeModeCaller::can_execute_action(destructive)` because Lab actions had destructive metadata; the audit verifies no other caller relies on this. + +--- + +## Task 0: Pre-flight audit + +**Files:** +- Read-only + +- [ ] **Step 1: Branch off main** + +Run: +```bash +cd /home/jmagar/workspace/lab +git checkout main && git pull --ff-only +git checkout -b code-mode-v2-drop-lab-actions +``` + +Expected: clean branch, no uncommitted changes. + +- [ ] **Step 2: Capture baseline test inventory** + +Run: +```bash +cargo nextest list -p labby --all-features 2>&1 | grep -E "code_mode|code_search|code_schema|code_execute" | tee /tmp/baseline-code-mode-tests.txt +``` + +Expected: a list of every Code Mode test currently registered. Keep this as the baseline so we can confirm which tests we explicitly delete and which we expect to keep passing. + +- [ ] **Step 3: Grep for destructive-metadata callers** + +Run: +```bash +rg -n "can_execute_action|destructive" crates/lab/src/dispatch/gateway/ crates/lab/src/mcp/ | tee /tmp/destructive-callers.txt +``` + +Expected: a list of every site that reads `destructive` metadata or calls `can_execute_action`. Confirm: +- `CodeModeCaller::can_execute_action` callers are ONLY inside `code_mode.rs::execute()` Lab-action dispatch path +- Upstream tool destructive-confirmation (the `params.confirm == true` gate) is enforced INSIDE `code_mode_call_upstream_tool()` or in the parent broker, not via `can_execute_action`. If it IS via `can_execute_action`, this task is wrong — STOP and revisit the plan. + +- [ ] **Step 4: Snapshot the current file size** + +Run: +```bash +wc -l crates/lab/src/dispatch/gateway/code_mode.rs +``` + +Expected: ~1844 lines. This is the baseline; we'll see ~600 LOC deletion by end of plan. + +- [ ] **Step 5: Commit branch state** + +```bash +git add -A +git commit --allow-empty -m "chore: branch baseline for code-mode-v2 drop-lab-actions" +``` + +--- + +## Task 1: Write failing test — `code_search` returns only upstream candidates + +**Files:** +- Modify: `crates/lab/src/dispatch/gateway/code_mode.rs` (test module at bottom of file) + +- [ ] **Step 1: Add the failing test** + +Inside the existing `#[cfg(test)] mod tests { ... }` block at the bottom of `crates/lab/src/dispatch/gateway/code_mode.rs`, add: + +```rust +#[tokio::test] +async fn code_search_returns_only_upstream_candidates() { + let registry = completion_test_registry(); + let broker = CodeModeBroker::new(®istry, None); + + let results = broker + .search("movie.search", 10, CodeModeCaller::TrustedLocal, CodeModeSurface::Cli) + .await + .expect("search ok"); + + for candidate in &results { + assert!( + !candidate.id.starts_with("lab::"), + "found lab:: candidate after drop: {}", + candidate.id + ); + } +} +``` + +(`completion_test_registry()` is an existing helper in the test module; if not, use the closest existing factory and adapt.) + +- [ ] **Step 2: Run the test to verify it fails** + +Run: +```bash +cargo nextest run -p labby --all-features dispatch::gateway::code_mode::tests::code_search_returns_only_upstream_candidates +``` + +Expected: FAIL — results currently include built-in Lab action candidates from `search_builtin_candidates()`. The assertion fires on the first `lab::` candidate. + +- [ ] **Step 3: Commit the failing test** + +```bash +git add crates/lab/src/dispatch/gateway/code_mode.rs +git commit -m "test: code_search returns only upstream candidates (failing)" +``` + +--- + +## Task 2: Delete `search_builtin_candidates` and wire `search()` to upstream-only + +**Files:** +- Modify: `crates/lab/src/dispatch/gateway/code_mode.rs:348-396` (delete `search_builtin_candidates`) +- Modify: `crates/lab/src/dispatch/gateway/code_mode.rs:246-289` (rewire `CodeModeBroker::search`) + +- [ ] **Step 1: Delete `search_builtin_candidates`** + +Open `crates/lab/src/dispatch/gateway/code_mode.rs`, locate the function `search_builtin_candidates` (around lines 348-396). Delete the entire function body, including its `#[doc]` lines and any private helpers used only by it (`compare_code_mode_search_candidates` may also become unused — check with `rg`). + +- [ ] **Step 2: Update `CodeModeBroker::search`** + +Locate `impl<'a> CodeModeBroker<'a>` (around line 232). The current `search` method merges built-in + upstream candidates. Rewrite to upstream-only: + +```rust +pub async fn search( + &self, + query: &str, + top_k: usize, + _caller: CodeModeCaller, + _surface: CodeModeSurface, +) -> Result, ToolError> { + let Some(manager) = self.gateway_manager else { + return Ok(Vec::new()); + }; + + let top_k = top_k.max(1).min(50); + match manager.search_tools(query, top_k, true).await { + Ok(upstream_results) => Ok(upstream_results + .into_iter() + .map(|r| { + CodeModeSearchCandidate::upstream_tool( + &r.upstream, + &r.name, + &r.description, + r.score, + r.input_schema, + ) + }) + .collect()), + Err(err) => { + // Preserve index_warming → empty-result fallback (was: builtin fallback) + if err.kind() == "index_warming" { + return Ok(Vec::new()); + } + Err(err) + } + } +} +``` + +The `_caller` and `_surface` parameters become unused locally but are kept in the signature for forward compatibility with bead #2. + +- [ ] **Step 3: Run the failing test — it should now pass** + +Run: +```bash +cargo nextest run -p labby --all-features dispatch::gateway::code_mode::tests::code_search_returns_only_upstream_candidates +``` + +Expected: PASS. + +- [ ] **Step 4: Run the full Code Mode test suite to see fallout** + +Run: +```bash +cargo nextest run -p labby --all-features code_mode 2>&1 | tee /tmp/run-after-task2.txt +``` + +Expected: some tests that referenced `search_builtin_candidates` or `lab::` IDs now fail to compile. We'll delete those in Task 3. + +- [ ] **Step 5: Commit** + +```bash +git add crates/lab/src/dispatch/gateway/code_mode.rs +git commit -m "feat(code-mode): drop search_builtin_candidates; code_search upstream-only" +``` + +--- + +## Task 3: Delete `lab::` ID parsing branch + +**Files:** +- Modify: `crates/lab/src/dispatch/gateway/code_mode.rs:39-85` (`CodeModeToolId::parse`) +- Modify: `crates/lab/src/dispatch/gateway/code_mode.rs:17-27` (`CodeModeToolRef` enum) +- Modify: `crates/lab/src/dispatch/gateway/code_mode.rs:88-90` (`lab_action_id` helper) + +- [ ] **Step 1: Write the failing test for `lab::` rejection** + +Add to the test module: + +```rust +#[test] +fn parse_rejects_lab_action_id() { + let err = CodeModeToolId::parse("lab::radarr.movie.search") + .expect_err("lab:: ids should be rejected"); + match err { + ToolError::Sdk { sdk_kind, .. } => { + assert_eq!(sdk_kind, "invalid_code_mode_id"); + } + other => panic!("expected invalid_code_mode_id, got {other:?}"), + } +} +``` + +Run: +```bash +cargo nextest run -p labby --all-features dispatch::gateway::code_mode::tests::parse_rejects_lab_action_id +``` + +Expected: FAIL — current parser accepts `lab::`. + +- [ ] **Step 2: Simplify `CodeModeToolRef` enum** + +Locate the enum (around line 17). Replace with the upstream-only variant: + +```rust +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CodeModeToolRef { + UpstreamTool { upstream: String, tool: String }, +} +``` + +(Single-variant enum is fine; bead #2 may remove it entirely or keep for future-proofing.) + +- [ ] **Step 3: Simplify `CodeModeToolId::parse`** + +Replace the function body (lines 39-85) with: + +```rust +impl CodeModeToolId { + pub fn parse(raw: &str) -> Result { + let raw = raw.trim(); + if raw.is_empty() { + return Err(invalid_code_mode_id("Code Mode tool id must not be empty")); + } + + // lab:: ids are no longer supported; emit unknown_tool with hint + if raw.starts_with("lab::") { + return Err(ToolError::Sdk { + sdk_kind: "unknown_tool".to_string(), + message: "lab:: IDs are not supported by Code Mode".to_string(), + }); + } + + if let Some(rest) = raw.strip_prefix("upstream::") { + let (upstream, tool) = rest.split_once("::").ok_or_else(|| { + invalid_code_mode_id("upstream Code Mode ids must use upstream::::") + })?; + if upstream.trim().is_empty() || tool.trim().is_empty() { + return Err(invalid_code_mode_id( + "upstream Code Mode ids must include upstream and tool", + )); + } + return Ok(Self { + raw: raw.to_string(), + reference: CodeModeToolRef::UpstreamTool { + upstream: upstream.trim().to_string(), + tool: tool.trim().to_string(), + }, + }); + } + + Err(invalid_code_mode_id( + "Code Mode ids must start with upstream::", + )) + } +} +``` + +Note: `lab::` returns `unknown_tool` (not `invalid_code_mode_id`) because that's the agent-actionable envelope per the bead's locked decision. Update the test from Step 1 accordingly: + +```rust +#[test] +fn parse_rejects_lab_action_id() { + let err = CodeModeToolId::parse("lab::radarr.movie.search") + .expect_err("lab:: ids should be rejected"); + match err { + ToolError::Sdk { sdk_kind, message } => { + assert_eq!(sdk_kind, "unknown_tool"); + assert!(message.contains("lab::")); + } + other => panic!("expected unknown_tool, got {other:?}"), + } +} +``` + +- [ ] **Step 4: Delete `lab_action_id` helper** + +Locate and delete the `lab_action_id` free function (around line 88-90). `upstream_tool_id` stays — still used. + +- [ ] **Step 5: Delete `CodeModeSearchCandidate::lab_action` constructor** + +Locate the `impl CodeModeSearchCandidate` block (around line 104). Delete the `lab_action` constructor; keep `upstream_tool`. + +- [ ] **Step 6: Run the parser test** + +Run: +```bash +cargo nextest run -p labby --all-features dispatch::gateway::code_mode::tests::parse_rejects_lab_action_id +``` + +Expected: PASS. + +- [ ] **Step 7: Run all parser tests** + +Run: +```bash +cargo nextest run -p labby --all-features dispatch::gateway::code_mode::tests::parses_ +``` + +Expected: `parses_lab_action_id` test (existing) FAILS — delete it in Task 4. `parses_upstream_tool_id` PASSES. + +- [ ] **Step 8: Commit** + +```bash +git add crates/lab/src/dispatch/gateway/code_mode.rs +git commit -m "feat(code-mode): reject lab:: IDs in parse; emit unknown_tool envelope" +``` + +--- + +## Task 4: Delete Lab-action paths in `schema()` and `execute()` + +**Files:** +- Modify: `crates/lab/src/dispatch/gateway/code_mode.rs` — `CodeModeBroker::schema`, `code_mode_call_lab_action`, `code_mode_schema_for_lab_action`, `action_input_schema`, `typescript_binding` (Lab-action call site) + +- [ ] **Step 1: Write failing test — schema rejects `lab::` ID** + +Add to test module: + +```rust +#[tokio::test] +async fn schema_rejects_lab_action_id() { + let registry = completion_test_registry(); + let broker = CodeModeBroker::new(®istry, None); + + let err = broker + .schema("lab::radarr.movie.search", CodeModeCaller::TrustedLocal, CodeModeSurface::Cli) + .await + .expect_err("schema should reject lab:: id"); + + match err { + ToolError::Sdk { sdk_kind, .. } => assert_eq!(sdk_kind, "unknown_tool"), + other => panic!("expected unknown_tool, got {other:?}"), + } +} +``` + +Run: +```bash +cargo nextest run -p labby --all-features dispatch::gateway::code_mode::tests::schema_rejects_lab_action_id +``` + +Expected: FAIL — schema() currently dispatches `LabAction` to `code_mode_schema_for_lab_action`. + +- [ ] **Step 2: Simplify `CodeModeBroker::schema`** + +Locate `schema` method on the broker (around line 291). Rewrite: + +```rust +pub async fn schema( + &self, + id: &str, + _caller: CodeModeCaller, + _surface: CodeModeSurface, +) -> Result { + let parsed = CodeModeToolId::parse(id)?; + let Some(manager) = self.gateway_manager else { + return Err(ToolError::Sdk { + sdk_kind: "unknown_tool".to_string(), + message: "no gateway manager configured".to_string(), + }); + }; + match parsed.reference { + CodeModeToolRef::UpstreamTool { upstream, tool } => { + self.schema_for_upstream_tool(manager, &upstream, &tool).await + } + } +} +``` + +Note: with the parser rejecting `lab::` IDs upstream, the `match` is exhaustive over the now-single variant. + +- [ ] **Step 3: Delete Lab-action helpers** + +Search and delete: +- `code_mode_schema_for_lab_action` (around lines 980-998) +- `code_mode_call_lab_action` (around lines 730-770 — the dispatch path) +- `action_input_schema` (around lines 1361-1390 — `ActionSpec` → JSON Schema projection) +- `typescript_binding` (around lines 1435-1443 — keep ONLY if used by upstream; otherwise delete with `typescript_type`/`object_typescript_type`/`typescript_property_name`) + +Run: +```bash +rg -n "typescript_binding|action_input_schema|code_mode_schema_for_lab_action|code_mode_call_lab_action" crates/lab/src/ +``` + +Expected: zero remaining matches in `code_mode.rs` (test references aside — handled in next step). + +- [ ] **Step 4: Run the schema test** + +Run: +```bash +cargo nextest run -p labby --all-features dispatch::gateway::code_mode::tests::schema_rejects_lab_action_id +``` + +Expected: PASS. + +- [ ] **Step 5: Update `CodeModeBroker::execute` `callTool` dispatch** + +Locate the parent-broker dispatch helper that routes a `callTool` `id` to either Lab dispatch or upstream MCP (`code_mode_call_tool_id` around line 697). Simplify to upstream-only: + +```rust +async fn call_tool_id(&self, id: &str, params: Value) -> Result { + let parsed = CodeModeToolId::parse(id)?; + let Some(manager) = self.gateway_manager else { + return Err(ToolError::Sdk { + sdk_kind: "unknown_tool".to_string(), + message: "no gateway manager configured".to_string(), + }); + }; + match parsed.reference { + CodeModeToolRef::UpstreamTool { upstream, tool } => { + self.call_upstream_tool(manager, &upstream, &tool, params).await + } + } +} +``` + +The `lab::` rejection happens inside `CodeModeToolId::parse` — agent's JS `callTool` sees a structured error. + +- [ ] **Step 6: Commit** + +```bash +git add crates/lab/src/dispatch/gateway/code_mode.rs +git commit -m "feat(code-mode): drop Lab-action paths in schema/execute" +``` + +--- + +## Task 5: Simplify policy types — drop `expose_builtin_services` and `can_execute_action` + +**Files:** +- Modify: `crates/lab/src/dispatch/gateway/code_mode.rs` — `CodeModeSurface`, `CodeModeCaller` + +- [ ] **Step 1: Update `CodeModeSurface` enum** + +Locate the `CodeModeSurface` enum (search `pub enum CodeModeSurface`). Remove `expose_builtin_services`: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CodeModeSurface { + Mcp { allow_destructive_actions: bool }, + Cli, +} +``` + +- [ ] **Step 2: Delete `CodeModeCaller::can_execute_action`** + +Locate `impl CodeModeCaller` (around line 70). Delete the `can_execute_action` method. Keep `can_read`, `can_execute`, `subject`. + +- [ ] **Step 3: Fix call sites in `mcp/server.rs`** + +Run: +```bash +rg -n "expose_builtin_services|can_execute_action" crates/lab/src/ +``` + +Expected: matches in `crates/lab/src/mcp/server.rs` (the code_search/code_schema/code_execute dispatch handlers around lines 1300-1640). + +For each match in `mcp/server.rs`: +- In `CodeModeSurface::Mcp { ... }` constructions, remove the `expose_builtin_services: false` (or true) field. Result: `CodeModeSurface::Mcp { allow_destructive_actions: }`. +- Remove any `if !caller.can_execute_action(...)` branches; the upstream confirm flag check (`params.confirm == true`) lives inside `code_mode_call_upstream_tool` and is the single remaining gate. + +- [ ] **Step 4: Fix call sites in `cli/gateway.rs`** + +Run: +```bash +rg -n "expose_builtin_services|can_execute_action|CodeModeSurface" crates/lab/src/cli/ +``` + +Expected: matches in `cli/gateway.rs`. Same fix: drop `expose_builtin_services` field. + +- [ ] **Step 5: Compile** + +Run: +```bash +cargo check --manifest-path crates/lab/Cargo.toml --all-features 2>&1 | tail -20 +``` + +Expected: clean compile. If any errors remain, they're cleanup misses — fix in place. + +- [ ] **Step 6: Commit** + +```bash +git add crates/lab/src/dispatch/gateway/code_mode.rs crates/lab/src/mcp/server.rs crates/lab/src/cli/gateway.rs +git commit -m "refactor(code-mode): drop expose_builtin_services field and can_execute_action method" +``` + +--- + +## Task 6: MCP dispatch handlers — remove built-in candidate merging + update descriptions + +**Files:** +- Modify: `crates/lab/src/mcp/server.rs:1300-1431` (code_search dispatch) +- Modify: `crates/lab/src/mcp/server.rs:1119-1167` (code_search + code_schema tool descriptions) + +- [ ] **Step 1: Read current code_search dispatch** + +Run: +```bash +sed -n '1300,1450p' crates/lab/src/mcp/server.rs | head -150 +``` + +- [ ] **Step 2: Remove built-in merge in code_search dispatch** + +In `crates/lab/src/mcp/server.rs` around lines 1391-1410, the current code calls `self.search_builtin_code_mode_candidates(...)` and merges with upstream results. Since the broker now returns upstream-only and `search_builtin_code_mode_candidates` is gone (deleted in Task 2), this code is dead — remove the merge logic and call the broker directly. + +Replace the call_tool branch for `CODE_SEARCH_TOOL_NAME` with a direct `CodeModeBroker::new(&self.registry, self.gateway_manager.as_deref()).search(...)` and serialize the result. + +- [ ] **Step 3: Update tool descriptions** + +In `crates/lab/src/mcp/server.rs` around line 1140-1145 (`code_search` Tool::new) and around line 1163-1166 (`code_schema` Tool::new), update the wording: + +Before: +```text +Schema-first Code Mode discovery for Lab and proxied upstream tools. +``` + +After: +```text +Schema-first Code Mode discovery for proxied upstream MCP tools. +``` + +For `code_schema`: +Before: +```text +Lab ids return the ActionSpec-derived action contract; upstream ids return the upstream JSON Schema exposed by the gateway. +``` + +After: +```text +Returns the upstream JSON Schema exposed by the gateway for a given upstream:: tool id. +``` + +- [ ] **Step 4: Run the MCP integration tests** + +Run: +```bash +cargo nextest run -p labby --all-features mcp::server 2>&1 | tail -20 +``` + +Expected: all green, or only failures explicitly tied to Lab-action paths (those will be deleted in Task 7). + +- [ ] **Step 5: Commit** + +```bash +git add crates/lab/src/mcp/server.rs +git commit -m "refactor(mcp): code_search/code_schema dispatch upstream-only; description wording" +``` + +--- + +## Task 7: Delete stale unit tests + add the rest of the new coverage + +**Files:** +- Modify: `crates/lab/src/dispatch/gateway/code_mode.rs` — test module (around lines 1527-1843) + +- [ ] **Step 1: Inventory remaining stale tests** + +Run: +```bash +rg -n "fn .*lab_action|fn .*builtin|fn .*action_input_schema|fn .*typescript_binding" crates/lab/src/dispatch/gateway/code_mode.rs +``` + +Expected: a handful of test functions targeting deleted code. List them in `/tmp/stale-tests.txt`. + +- [ ] **Step 2: Delete the stale tests** + +For each test in the list, delete the function. Common ones: +- `parses_lab_action_id` — replaced by `parse_rejects_lab_action_id` from Task 3 +- `builds_search_candidate_for_lab_action` — `CodeModeSearchCandidate::lab_action` no longer exists +- `builds_lab_schema_response` — `CodeModeSchemaResponse::lab_action` may still exist (used in tests only) — KEEP for now until bead #2 deletes `CodeModeSchemaResponse` entirely +- `builds_action_input_schema_and_typescript_binding` — `action_input_schema` gone +- `search_expands_builtin_matches_to_action_candidates` — `search_builtin_candidates` gone +- `execute_strips_confirm_before_dispatch` — KEEP if it tests upstream confirm stripping; DELETE if Lab-action-only + +- [ ] **Step 3: Add `code_execute_callTool_lab_id_returns_unknown_tool`** + +Add to the test module: + +```rust +#[tokio::test] +async fn code_execute_callTool_lab_id_returns_unknown_tool() { + let registry = completion_test_registry(); + let broker = CodeModeBroker::new(®istry, None); + + let response = broker + .execute( + r#"await callTool("lab::radarr.movie.search", {query:"Matrix"})"#, + CodeModeCaller::TrustedLocal, + CodeModeSurface::Cli, + crate::config::CodeModeConfig { + enabled: true, + timeout_ms: 5_000, + max_tool_calls: 2, + }, + ) + .await + .expect("execute returns response, even with failed inner call"); + + let first = response.calls.first().expect("one call recorded"); + let kind = first + .result + .get("kind") + .and_then(Value::as_str) + .or_else(|| first.result.pointer("/error/kind").and_then(Value::as_str)); + assert_eq!(kind, Some("unknown_tool")); +} +``` + +- [ ] **Step 4: Run the new tests** + +Run: +```bash +cargo nextest run -p labby --all-features dispatch::gateway::code_mode::tests +``` + +Expected: ALL PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/lab/src/dispatch/gateway/code_mode.rs +git commit -m "test(code-mode): delete Lab-action tests; add unknown_tool envelope coverage" +``` + +--- + +## Task 8: Update the `unknown_tool` hint to the expanded form (research-derived) + +**Files:** +- Modify: `crates/lab/src/dispatch/gateway/code_mode.rs` — the `unknown_tool` ToolError construction(s) where `lab::` IDs are rejected + +- [ ] **Step 1: Locate every `unknown_tool` construction in `code_mode.rs` for `lab::` rejection** + +Run: +```bash +rg -n 'sdk_kind.*"unknown_tool"\|"lab::"' crates/lab/src/dispatch/gateway/code_mode.rs +``` + +Expected: two or three sites (in `parse`, in `schema`, in `call_tool_id` if applicable). + +- [ ] **Step 2: Define a single hint constant** + +Add near the top of the file (below the existing module docs): + +```rust +const LAB_ACTION_UNKNOWN_TOOL_HINT: &str = + "Code Mode handles upstream MCP tools only. For Lab actions, use the `tool_execute` MCP tool: \ + name= (e.g. \"radarr\"), arguments={action: \"\", params: {...}}. \ + Example: tool_execute(name=\"radarr\", arguments={action:\"movie.search\", params:{query:\"Matrix\"}})."; +``` + +- [ ] **Step 3: Use the constant in all `lab::` rejection sites** + +For each `ToolError::Sdk { sdk_kind: "unknown_tool", message: ... }` construction in `code_mode.rs` that fires on `lab::` input, refactor to include the hint. Since `ToolError::Sdk` doesn't have a `hint` field, the hint is appended to the message: + +```rust +return Err(ToolError::Sdk { + sdk_kind: "unknown_tool".to_string(), + message: format!("lab:: IDs are not supported by Code Mode. {LAB_ACTION_UNKNOWN_TOOL_HINT}"), +}); +``` + +- [ ] **Step 4: Update the hint test assertion** + +In `parse_rejects_lab_action_id` (and `schema_rejects_lab_action_id`, and `code_execute_callTool_lab_id_returns_unknown_tool`), add: + +```rust +assert!(message.contains("tool_execute")); +assert!(message.contains("\"radarr\"")); +``` + +- [ ] **Step 5: Run all rejection tests** + +Run: +```bash +cargo nextest run -p labby --all-features dispatch::gateway::code_mode::tests::parse_rejects_lab_action_id dispatch::gateway::code_mode::tests::schema_rejects_lab_action_id dispatch::gateway::code_mode::tests::code_execute_callTool_lab_id_returns_unknown_tool +``` + +Expected: ALL PASS. + +- [ ] **Step 6: Commit** + +```bash +git add crates/lab/src/dispatch/gateway/code_mode.rs +git commit -m "feat(code-mode): expand unknown_tool hint with tool_execute mechanical example" +``` + +--- + +## Task 9: Final greps + full-suite verification + +**Files:** +- Read-only verification + +- [ ] **Step 1: Confirm zero lab:: code remains** + +Run: +```bash +rg "lab::|LabAction|lab_action_id|action_input_schema|search_builtin_candidates" crates/lab/src/dispatch/gateway/code_mode.rs +``` + +Expected: zero matches (except inside the `unknown_tool` message string `"lab:: IDs are not supported"` and the hint constant — those are intentional). + +- [ ] **Step 2: Confirm zero stale references elsewhere** + +Run: +```bash +rg "LabAction|lab_action_id|action_input_schema|search_builtin_candidates|expose_builtin_services|can_execute_action" crates/lab/src/ +``` + +Expected: zero matches. + +- [ ] **Step 3: Confirm `typescript_binding` Lab-action helpers gone** + +Run: +```bash +rg "fn typescript_binding|fn typescript_type|fn object_typescript_type|fn typescript_property_name" crates/lab/src/ +``` + +Expected: zero matches (these were only used by Lab-action schema response construction). + +- [ ] **Step 4: Confirm LOC reduction** + +Run: +```bash +wc -l crates/lab/src/dispatch/gateway/code_mode.rs +``` + +Expected: ~1200-1400 lines (down from ~1844). Confirms ~400-600 LOC deletion. + +- [ ] **Step 5: Full Code Mode test suite** + +Run: +```bash +cargo nextest run -p labby --all-features code_mode +``` + +Expected: all green. + +- [ ] **Step 6: Stdio protocol integration tests** + +Run: +```bash +cargo nextest run -p labby --all-features --test code_mode_runner +``` + +Expected: ALL existing tests pass unchanged (stdio protocol is untouched). + +- [ ] **Step 7: Full workspace test** + +Run: +```bash +cargo nextest run --workspace --all-features +``` + +Expected: all green. + +- [ ] **Step 8: Clippy + fmt** + +Run: +```bash +cargo clippy --workspace --all-features -- -D warnings +cargo fmt --all -- --check +``` + +Expected: both clean. + +- [ ] **Step 9: Final commit (if any cleanup remains)** + +If any cleanup happened in Steps 1-8, commit it: + +```bash +git add -A +git commit -m "chore(code-mode): final cleanup after lab-action drop" +``` + +--- + +## Task 10: Live verification + PR + +**Files:** +- Read-only verification against a running gateway + +- [ ] **Step 1: Build release binary** + +Run: +```bash +cargo build --release --all-features --bin labby +``` + +Expected: compiles. + +- [ ] **Step 2: Live test against the local gateway** + +Assuming a running `labby serve` instance (e.g. `lab-prod` at `https://lab.tootie.tv/mcp`): + +Run: +```bash +LAB_MCP_HTTP_TOKEN=$(grep '^LAB_MCP_HTTP_TOKEN=' ~/.lab/.env | cut -d= -f2-) \ + mcporter call lab-prod.invoke name=code_search arguments:='{"query":"docker container logs","top_k":5}' 2>&1 | jq '.[].id' | head -10 +``` + +Expected: only `upstream::*` IDs. Zero `lab::*` IDs. + +- [ ] **Step 3: Live test of the unknown_tool hint** + +Run: +```bash +LAB_MCP_HTTP_TOKEN=$(...) mcporter call lab-prod.invoke name=code_schema arguments:='{"id":"lab::radarr.movie.search"}' 2>&1 +``` + +Expected: structured `unknown_tool` envelope with the expanded hint mentioning `tool_execute`. + +- [ ] **Step 4: Push and open PR** + +```bash +git push -u origin code-mode-v2-drop-lab-actions +gh pr create --title "feat(code-mode): drop Lab-action support (lab-elme3.1)" --body "$(cat <<'EOF' +## Summary +- Removes `lab::.` ID support from Code Mode entirely +- Only `upstream::::` IDs accepted +- Returns structured `unknown_tool` envelope with mechanical `tool_execute` example for any `lab::` caller +- Drops `expose_builtin_services` field from `CodeModeSurface::Mcp` and `can_execute_action` policy method +- ~400-600 LOC deletion from `crates/lab/src/dispatch/gateway/code_mode.rs` + +Bead: lab-elme3.1 (part of epic lab-elme3 — Code Mode v2 refactor). + +## Test plan +- [ ] `cargo nextest run --workspace --all-features` passes locally +- [ ] `cargo clippy --workspace --all-features -- -D warnings` clean +- [ ] Live: `mcporter call lab-prod.invoke name=code_search ...` returns only upstream IDs +- [ ] Live: `mcporter call lab-prod.invoke name=code_schema arguments:='{"id":"lab::..."}'` returns `unknown_tool` envelope with hint +- [ ] Stdio protocol tests (`crates/lab/tests/code_mode_runner.rs`) pass unchanged +EOF +)" +``` + +- [ ] **Step 5: Close the bead** + +Run: +```bash +bd update lab-elme3.1 --status=completed +bd comments add lab-elme3.1 "Closed via PR #; epic lab-elme3 chain continues at lab-elme3.2." +``` + +--- + +## Self-Review + +**Spec coverage:** Every acceptance criterion from `bd show lab-elme3.1` (Testing + Validation sections) maps to a task: +- `code_search_returns_only_upstream_candidates` → Task 1+2 +- `code_schema_rejects_lab_action_id` → Task 4 +- `code_execute_callTool_lab_id_returns_unknown_tool` → Task 7 +- `parse_rejects_lab_action_id` → Task 3 +- Hint expansion → Task 8 +- Final greps → Task 9 +- Stdio protocol unchanged → confirmed in Task 9 Step 6 +- Live tests → Task 10 + +**Placeholder scan:** all code blocks have full content. The one approximation: `completion_test_registry()` helper may be named differently in the current test module — that's an inspection moment for the implementer, not a placeholder. + +**Type consistency:** `CodeModeToolRef` becomes single-variant; matches in `schema/execute/parse` are exhaustive. `CodeModeSurface::Mcp { allow_destructive_actions: bool }` is consistent across files (`code_mode.rs`, `mcp/server.rs`, `cli/gateway.rs` all updated together in Task 5). `LAB_ACTION_UNKNOWN_TOOL_HINT` is defined once and reused in three sites (Task 8). From 398e49b709dd01793c9f82eb54361f0d35b9d4f7 Mon Sep 17 00:00:00 2001 From: Jacob Magar Date: Mon, 25 May 2026 02:46:06 -0400 Subject: [PATCH 2/5] feat(code-mode): drop Lab action support --- crates/lab/src/cli/gateway.rs | 37 +- crates/lab/src/dispatch/gateway/code_mode.rs | 806 +++---------------- crates/lab/src/main.rs | 5 +- crates/lab/src/mcp/server.rs | 113 +-- 4 files changed, 129 insertions(+), 832 deletions(-) diff --git a/crates/lab/src/cli/gateway.rs b/crates/lab/src/cli/gateway.rs index 403fab32..ade4c8ff 100644 --- a/crates/lab/src/cli/gateway.rs +++ b/crates/lab/src/cli/gateway.rs @@ -12,9 +12,7 @@ use crate::cli::helpers::{run_action_command, run_confirmable_action_command}; use crate::config::{LabConfig, ProtectedMcpRouteConfig, config_toml_path}; use crate::dispatch::clients::SharedServiceClients; use crate::dispatch::gateway::SHARED_GATEWAY_OAUTH_SUBJECT; -use crate::dispatch::gateway::code_mode::{ - CodeModeBroker, CodeModeCaller, CodeModeSurface, CodeModeToolId, CodeModeToolRef, -}; +use crate::dispatch::gateway::code_mode::{CodeModeBroker, CodeModeCaller, CodeModeSurface}; use crate::dispatch::gateway::install_gateway_manager; use crate::dispatch::gateway::manager::{GatewayManager, GatewayRuntimeHandle}; use crate::dispatch::upstream::pool::UpstreamPool; @@ -417,13 +415,7 @@ pub async fn run(args: GatewayArgs, format: OutputFormat, config: &LabConfig) -> command: GatewayMcpAuthCommand::Status(_) | GatewayMcpAuthCommand::Clear(_), }), }) - ) || matches!(&args.command, GatewayCommand::ProtectedRoute(_))) - && !matches!( - &args.command, - GatewayCommand::Code(GatewayCodeArgs { - command: GatewayCodeCommand::Schema { id }, - }) if code_mode_schema_is_builtin(id) - ); + ) || matches!(&args.command, GatewayCommand::ProtectedRoute(_))); let manager = build_manager(config, discover_upstreams).await; let cli_origin = format!("cli:{}", std::process::id()); let cli_owner = json!({ @@ -729,16 +721,6 @@ pub async fn run(args: GatewayArgs, format: OutputFormat, config: &LabConfig) -> } } -fn code_mode_schema_is_builtin(id: &str) -> bool { - matches!( - CodeModeToolId::parse(id), - Ok(CodeModeToolId { - reference: CodeModeToolRef::LabAction { .. }, - .. - }) - ) -} - async fn run_gateway_code( manager: Arc, args: GatewayCodeArgs, @@ -909,10 +891,7 @@ fn open_in_browser(url: &str) -> Result<()> { /// Format inspired by `claude mcp list` (status icon + one-line per server) /// and `codex mcp list` (column alignment). JSON mode preserves the full /// `ServerView` shape for downstream consumers. -async fn run_gateway_list( - manager: Arc, - format: OutputFormat, -) -> Result { +async fn run_gateway_list(manager: Arc, format: OutputFormat) -> Result { let servers = match manager.list().await { Ok(s) => s, Err(err) => { @@ -982,10 +961,8 @@ fn render_gateway_list_human( servers.sort_by_key(|s| { if !s.enabled { 2u8 - } else if s.connected { - 0u8 } else { - 1u8 + u8::from(!s.connected) } }); let servers = servers.as_slice(); @@ -1027,7 +1004,7 @@ fn render_gateway_list_human( let transport = theme.tertiary(&transport_padded); let status_detail = if !s.enabled { - theme.muted("disabled".to_string()) + theme.muted("disabled") } else if s.connected { let mut parts = Vec::new(); if s.exposed_tool_count > 0 { @@ -1205,7 +1182,7 @@ mod tests { "gateway", "code", "schema", - "lab::radarr.movie.search", + "upstream::github::search_issues", ]) .is_ok() ); @@ -1216,7 +1193,7 @@ mod tests { "code", "exec", "--code", - "await callTool(\"lab::gateway.gateway.servers\", {})", + "await callTool(\"upstream::github::search_issues\", {query:\"repo\"})", ]) .is_ok() ); diff --git a/crates/lab/src/dispatch/gateway/code_mode.rs b/crates/lab/src/dispatch/gateway/code_mode.rs index 91d2e453..d9117c22 100644 --- a/crates/lab/src/dispatch/gateway/code_mode.rs +++ b/crates/lab/src/dispatch/gateway/code_mode.rs @@ -1,5 +1,4 @@ use std::cell::RefCell; -use std::cmp::Ordering as CmpOrdering; use std::collections::HashMap; use std::io::{self, BufRead, BufReader, BufWriter, Write}; use std::process::ExitCode; @@ -12,7 +11,6 @@ use boa_engine::{ Context, JsArgs, JsError, JsNativeError, JsResult, JsValue, NativeFunction, Source, js_string, }; use futures::{FutureExt, StreamExt, stream::FuturesUnordered}; -use lab_apis::core::action::{ActionSpec, ParamSpec}; use rmcp::model::CallToolRequestParams; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value, json}; @@ -22,7 +20,11 @@ use tokio::process::{Child, ChildStdin, Command}; use crate::dispatch::error::ToolError; use crate::dispatch::gateway::manager::GatewayManager; -use crate::registry::{RegisteredService, ToolRegistry}; +use crate::registry::ToolRegistry; + +const LAB_ACTION_UNKNOWN_TOOL_HINT: &str = "Code Mode handles upstream MCP tools only. For Lab actions, use the `tool_execute` MCP tool: \ + name= (e.g. \"radarr\"), arguments={action: \"\", params: {...}}. \ + Example: tool_execute(name=\"radarr\", arguments={action:\"movie.search\", params:{query:\"Matrix\"}})."; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodeModeToolId { @@ -32,7 +34,6 @@ pub struct CodeModeToolId { #[derive(Debug, Clone, PartialEq, Eq)] pub enum CodeModeToolRef { - LabAction { service: String, action: String }, UpstreamTool { upstream: String, tool: String }, } @@ -43,22 +44,8 @@ impl CodeModeToolId { return Err(invalid_code_mode_id("Code Mode tool id must not be empty")); } - if let Some(rest) = raw.strip_prefix("lab::") { - let (service, action) = rest.split_once('.').ok_or_else(|| { - invalid_code_mode_id("lab Code Mode ids must use lab::.") - })?; - if service.trim().is_empty() || action.trim().is_empty() { - return Err(invalid_code_mode_id( - "lab Code Mode ids must include service and action", - )); - } - return Ok(Self { - raw: raw.to_string(), - reference: CodeModeToolRef::LabAction { - service: service.trim().to_string(), - action: action.trim().to_string(), - }, - }); + if raw.starts_with("lab::") { + return Err(lab_action_unknown_tool()); } if let Some(rest) = raw.strip_prefix("upstream::") { @@ -80,16 +67,11 @@ impl CodeModeToolId { } Err(invalid_code_mode_id( - "Code Mode ids must start with lab:: or upstream::", + "Code Mode ids must start with upstream::", )) } } -#[must_use] -pub fn lab_action_id(service: &str, action: &str) -> String { - format!("lab::{service}.{action}") -} - #[must_use] pub fn upstream_tool_id(upstream: &str, tool: &str) -> String { format!("upstream::{upstream}::{tool}") @@ -111,18 +93,6 @@ pub struct CodeModeSearchCandidate { } impl CodeModeSearchCandidate { - #[must_use] - pub fn lab_action(service: &str, action: &str, description: &str, score: f32) -> Self { - Self { - id: lab_action_id(service, action), - name: action.to_string(), - upstream: "lab".to_string(), - description: description.to_string(), - score, - schema_available: true, - } - } - #[must_use] pub fn upstream_tool( upstream: &str, @@ -181,10 +151,7 @@ pub enum CodeModeCaller { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CodeModeSurface { - Mcp { - expose_builtin_services: bool, - allow_destructive_actions: bool, - }, + Mcp { allow_destructive_actions: bool }, Cli, } @@ -208,39 +175,16 @@ impl CodeModeCaller { .any(|scope| matches!(scope.as_str(), "lab" | "lab:admin")), } } - - #[must_use] - pub fn can_execute_action(&self, entry: &RegisteredService, action: &str) -> bool { - if !builtin_action_requires_admin(entry, action) { - return self.can_execute(); - } - match self { - Self::TrustedLocal => true, - Self::Scoped { scopes, .. } => scopes.iter().any(|scope| scope == "lab:admin"), - } - } - - #[must_use] - pub fn subject(&self) -> Option<&str> { - match self { - Self::TrustedLocal => None, - Self::Scoped { subject, .. } => subject.as_deref(), - } - } } pub struct CodeModeBroker<'a> { - registry: &'a ToolRegistry, gateway_manager: Option<&'a GatewayManager>, } impl<'a> CodeModeBroker<'a> { #[must_use] - pub fn new(registry: &'a ToolRegistry, gateway_manager: Option<&'a GatewayManager>) -> Self { - Self { - registry, - gateway_manager, - } + pub fn new(_registry: &'a ToolRegistry, gateway_manager: Option<&'a GatewayManager>) -> Self { + Self { gateway_manager } } pub async fn search( @@ -248,7 +192,7 @@ impl<'a> CodeModeBroker<'a> { query: &str, top_k: usize, caller: CodeModeCaller, - surface: CodeModeSurface, + _surface: CodeModeSurface, ) -> Result, ToolError> { if !caller.can_read() { return Err(ToolError::Sdk { @@ -257,42 +201,34 @@ impl<'a> CodeModeBroker<'a> { }); } - let score_floor_fraction = match self.gateway_manager { - Some(manager) => manager.tool_search_config().await.score_floor_fraction, - None => 0.0, + let Some(manager) = self.gateway_manager else { + return Ok(Vec::new()); }; - let mut candidates = self - .search_builtin_candidates(query, top_k, score_floor_fraction, surface) - .await; - - if let Some(manager) = self.gateway_manager { - match manager.search_tools(query, top_k, true).await { - Ok(upstream_results) => { - candidates.extend(upstream_results.into_iter().map(|result| { - CodeModeSearchCandidate::upstream_tool( - &result.upstream, - &result.name, - &result.description, - result.score, - result.input_schema, - ) - })); - } - Err(err) if err.kind() == "index_warming" && !candidates.is_empty() => {} - Err(err) => return Err(err), - } - } - candidates.sort_by(compare_code_mode_search_candidates); - candidates.truncate(top_k.max(1).min(50)); - Ok(candidates) + let top_k = top_k.max(1).min(50); + match manager.search_tools(query, top_k, true).await { + Ok(upstream_results) => Ok(upstream_results + .into_iter() + .map(|result| { + CodeModeSearchCandidate::upstream_tool( + &result.upstream, + &result.name, + &result.description, + result.score, + result.input_schema, + ) + }) + .collect()), + Err(err) if err.kind() == "index_warming" => Ok(Vec::new()), + Err(err) => Err(err), + } } pub async fn schema( &self, id: &str, caller: CodeModeCaller, - surface: CodeModeSurface, + _surface: CodeModeSurface, ) -> Result { if !caller.can_execute() { return Err(ToolError::Sdk { @@ -301,13 +237,15 @@ impl<'a> CodeModeBroker<'a> { }); } let parsed = CodeModeToolId::parse(id)?; + let Some(manager) = self.gateway_manager else { + return Err(ToolError::Sdk { + sdk_kind: "unknown_tool".to_string(), + message: "no gateway manager configured".to_string(), + }); + }; match parsed.reference { - CodeModeToolRef::LabAction { service, action } => { - self.schema_for_lab_action(&parsed.raw, &service, &action, caller, surface) - .await - } CodeModeToolRef::UpstreamTool { upstream, tool } => { - self.schema_for_upstream_tool(&parsed.raw, &upstream, &tool) + self.schema_for_upstream_tool(manager, &parsed.raw, &upstream, &tool) .await } } @@ -345,125 +283,13 @@ impl<'a> CodeModeBroker<'a> { .await } - async fn search_builtin_candidates( - &self, - query: &str, - top_k: usize, - score_floor_fraction: f32, - surface: CodeModeSurface, - ) -> Vec { - let needle = query.trim().to_ascii_lowercase(); - if needle.is_empty() || needle.len() > 500 { - return Vec::new(); - } - - let mut candidates = Vec::new(); - for service in self.registry.services() { - if !self.service_visible(service.name, surface).await { - continue; - } - for action in self.searchable_builtin_actions(service, surface).await { - let haystack = format!( - "{}\n{}\n{}\n{}", - service.name, service.description, action.name, action.description - ) - .to_ascii_lowercase(); - let score = crate::dispatch::gateway::score_name_haystack( - &needle, - &action.name.to_ascii_lowercase(), - &haystack, - ); - if score > 0.0 { - candidates.push(CodeModeSearchCandidate::lab_action( - service.name, - action.name, - action.description, - score, - )); - } - } - } - - candidates.sort_by(compare_code_mode_search_candidates); - if score_floor_fraction > 0.0 - && let Some(top) = candidates.first() - { - let floor = top.score * score_floor_fraction; - candidates.retain(|candidate| candidate.score >= floor); - } - candidates.truncate(top_k.max(1).min(50)); - candidates - } - - async fn schema_for_lab_action( - &self, - id: &str, - service_name: &str, - action_name: &str, - caller: CodeModeCaller, - surface: CodeModeSurface, - ) -> Result { - let Some(entry) = self - .registry - .services() - .iter() - .find(|entry| entry.name == service_name) - else { - return Err(ToolError::Sdk { - sdk_kind: "not_found".to_string(), - message: format!("Lab service `{service_name}` was not found"), - }); - }; - if !self.service_visible(entry.name, surface).await - || !self.action_allowed(entry.name, action_name, surface).await - { - return Err(ToolError::Sdk { - sdk_kind: "not_found".to_string(), - message: format!( - "Lab action `{service_name}.{action_name}` is not exposed on this surface" - ), - }); - } - let action = entry - .actions - .iter() - .find(|action| action.name == action_name) - .ok_or_else(|| ToolError::Sdk { - sdk_kind: "not_found".to_string(), - message: format!("Lab action `{service_name}.{action_name}` was not found"), - })?; - if !caller.can_execute_action(entry, action_name) { - return Err(ToolError::Sdk { - sdk_kind: "forbidden".to_string(), - message: format!( - "action `{action_name}` for service `{}` requires `lab:admin` scope", - entry.name - ), - }); - } - let input_schema = action_input_schema(action); - crate::dispatch::helpers::action_schema(entry.actions, action_name).map(|schema| { - CodeModeSchemaResponse::lab_action_with_input_schema( - id, - action_name, - schema, - input_schema, - ) - }) - } - async fn schema_for_upstream_tool( &self, + manager: &GatewayManager, id: &str, upstream: &str, tool: &str, ) -> Result { - let Some(manager) = self.gateway_manager else { - return Err(ToolError::Sdk { - sdk_kind: "upstream_error".to_string(), - message: "gateway manager is unavailable".to_string(), - }); - }; let candidate = manager .resolve_code_mode_upstream_tool(upstream, tool) .await?; @@ -698,101 +524,31 @@ impl<'a> CodeModeBroker<'a> { &self, id: &str, params: Value, - caller: CodeModeCaller, - surface: CodeModeSurface, + _caller: CodeModeCaller, + _surface: CodeModeSurface, ) -> Result { let parsed = CodeModeToolId::parse(id)?; + let Some(manager) = self.gateway_manager else { + return Err(ToolError::Sdk { + sdk_kind: "unknown_tool".to_string(), + message: "no gateway manager configured".to_string(), + }); + }; match parsed.reference { - CodeModeToolRef::LabAction { service, action } => { - self.call_lab_action(&service, &action, params, caller, surface) - .await - } CodeModeToolRef::UpstreamTool { upstream, tool } => { - self.call_upstream_tool(&upstream, &tool, params).await + self.call_upstream_tool(manager, &upstream, &tool, params) + .await } } } - async fn call_lab_action( - &self, - service_name: &str, - action_name: &str, - params: Value, - caller: CodeModeCaller, - surface: CodeModeSurface, - ) -> Result { - let Some(entry) = self - .registry - .services() - .iter() - .find(|entry| entry.name == service_name) - else { - return Err(ToolError::Sdk { - sdk_kind: "not_found".to_string(), - message: format!("Lab service `{service_name}` was not found"), - }); - }; - if !self.service_visible(entry.name, surface).await - || !self.action_allowed(entry.name, action_name, surface).await - { - return Err(ToolError::Sdk { - sdk_kind: "not_found".to_string(), - message: format!( - "Lab action `{service_name}.{action_name}` is not exposed on this surface" - ), - }); - } - if !caller.can_execute_action(entry, action_name) { - return Err(ToolError::Sdk { - sdk_kind: "forbidden".to_string(), - message: format!( - "action `{action_name}` for service `{}` requires `lab:admin` scope", - entry.name - ), - }); - } - let is_destructive = entry - .actions - .iter() - .any(|action| action.name == action_name && action.destructive); - let confirmed = params.get("confirm").and_then(Value::as_bool) == Some(true); - if is_destructive && !confirmed { - return Err(ToolError::Sdk { - sdk_kind: "confirmation_required".to_string(), - message: format!( - "action `{action_name}` is destructive - pass {{\"confirm\":true}} in params" - ), - }); - } - if is_destructive && !surface.allows_destructive_actions() { - return Err(ToolError::Sdk { - sdk_kind: "confirmation_required".to_string(), - message: format!( - "action `{action_name}` is destructive - pass {{\"confirm\":true}} to code_execute and to the tool params" - ), - }); - } - let params = strip_code_mode_control_params(params); - let params = if entry.name == "gateway" { - inject_gateway_origin_param(params, caller.subject(), surface) - } else { - params - }; - (entry.dispatch)(action_name.to_string(), params).await - } - async fn call_upstream_tool( &self, + manager: &GatewayManager, upstream: &str, tool: &str, params: Value, ) -> Result { - let Some(manager) = self.gateway_manager else { - return Err(ToolError::Sdk { - sdk_kind: "upstream_error".to_string(), - message: "gateway manager is unavailable".to_string(), - }); - }; manager .resolve_code_mode_upstream_tool(upstream, tool) .await?; @@ -850,75 +606,6 @@ impl<'a> CodeModeBroker<'a> { } } } - - async fn searchable_builtin_actions<'b>( - &self, - service: &'b RegisteredService, - surface: CodeModeSurface, - ) -> Vec<&'b ActionSpec> { - let mut actions = service.actions.iter().collect::>(); - if let Some(allowed_actions) = self.allowed_actions(service.name, surface).await - && !allowed_actions.is_empty() - { - actions.retain(|action| allowed_actions.iter().any(|allowed| allowed == action.name)); - } - actions - } - - async fn service_visible(&self, service: &str, surface: CodeModeSurface) -> bool { - match (surface, self.gateway_manager) { - ( - CodeModeSurface::Mcp { - expose_builtin_services: false, - .. - }, - _, - ) => false, - (CodeModeSurface::Mcp { .. }, Some(manager)) => { - manager.surface_enabled_for_service(service, "mcp").await - } - (CodeModeSurface::Cli, Some(manager)) => { - manager.surface_enabled_for_service(service, "cli").await - } - _ => true, - } - } - - async fn action_allowed(&self, service: &str, action: &str, surface: CodeModeSurface) -> bool { - match (surface, self.gateway_manager) { - (CodeModeSurface::Mcp { .. }, Some(manager)) => { - manager - .mcp_action_allowed_for_service(service, action) - .await - } - _ => true, - } - } - - async fn allowed_actions( - &self, - service: &str, - surface: CodeModeSurface, - ) -> Option> { - match (surface, self.gateway_manager) { - (CodeModeSurface::Mcp { .. }, Some(manager)) => { - manager.allowed_mcp_actions_for_service(service).await - } - _ => None, - } - } -} - -impl CodeModeSurface { - fn allows_destructive_actions(self) -> bool { - match self { - Self::Cli => true, - Self::Mcp { - allow_destructive_actions, - .. - } => allow_destructive_actions, - } - } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -962,33 +649,6 @@ thread_local! { } impl CodeModeSchemaResponse { - #[cfg(test)] - #[must_use] - pub fn lab_action(id: &str, action: &str, schema: Value) -> Self { - Self::lab_action_with_input_schema(id, action, schema.clone(), schema) - } - - #[must_use] - pub fn lab_action_with_input_schema( - id: &str, - action: &str, - schema: Value, - input_schema: Value, - ) -> Self { - Self { - id: id.to_string(), - kind: "lab_action", - name: action.to_string(), - upstream: "lab".to_string(), - schema, - schema_format: "lab_action_spec", - bindings: CodeModeBindings { - typescript: typescript_binding(id, "ToolArgs", &input_schema), - }, - input_schema, - } - } - #[must_use] pub fn upstream_tool(id: &str, upstream: &str, tool: &str, schema: Value) -> Self { Self { @@ -1013,14 +673,13 @@ pub fn invalid_code_mode_id(message: impl Into) -> ToolError { } } -fn compare_code_mode_search_candidates( - a: &CodeModeSearchCandidate, - b: &CodeModeSearchCandidate, -) -> CmpOrdering { - b.score - .partial_cmp(&a.score) - .unwrap_or(CmpOrdering::Equal) - .then_with(|| a.id.cmp(&b.id)) +fn lab_action_unknown_tool() -> ToolError { + ToolError::Sdk { + sdk_kind: "unknown_tool".to_string(), + message: format!( + "lab:: IDs are not supported by Code Mode. {LAB_ACTION_UNKNOWN_TOOL_HINT}" + ), + } } async fn write_runner_input( @@ -1047,54 +706,6 @@ async fn terminate_code_mode_runner(child: &mut Child) { drop(child.wait().await); } -fn strip_code_mode_control_params(mut params: Value) -> Value { - if let Value::Object(map) = &mut params { - map.remove("confirm"); - } - params -} - -fn inject_gateway_origin_param( - params: Value, - subject: Option<&str>, - surface: CodeModeSurface, -) -> Value { - let surface_label = match surface { - CodeModeSurface::Mcp { .. } => "mcp", - CodeModeSurface::Cli => "cli", - }; - let raw = subject - .map(|value| format!("{surface_label}:{value}")) - .unwrap_or_else(|| format!("{surface_label}:anonymous")); - let Some(mut object) = params.as_object().cloned() else { - return params; - }; - object.insert( - "owner".to_string(), - json!({ - "surface": surface_label, - "subject": subject, - "raw": raw, - }), - ); - object.insert("origin".to_string(), Value::String(raw)); - Value::Object(object) -} - -fn builtin_action_requires_admin(entry: &RegisteredService, action: &str) -> bool { - if entry.name == "gateway" { - return !matches!( - action, - "help" | "schema" | "gateway.help" | "gateway.schema" - ); - } - entry.name == "setup" - && entry - .actions - .iter() - .any(|spec| spec.name == action && spec.destructive) -} - fn code_mode_canonical_error_kind(s: &str) -> &'static str { match s { "unknown_action" => "unknown_action", @@ -1357,80 +968,6 @@ fn js_value_message(value: &JsValue, context: &mut Context) -> String { .unwrap_or_else(|_| "promise rejected".to_string()) } -#[must_use] -pub fn action_input_schema(action: &ActionSpec) -> Value { - let mut properties = Map::new(); - let mut required = Vec::new(); - - for param in action.params { - let mut schema = param_json_schema(param); - if let Value::Object(map) = &mut schema - && !param.description.is_empty() - { - map.insert( - "description".to_string(), - Value::String(param.description.to_string()), - ); - } - properties.insert(param.name.to_string(), schema); - if param.required { - required.push(Value::String(param.name.to_string())); - } - } - - let mut schema = Map::from_iter([ - ("type".to_string(), Value::String("object".to_string())), - ("properties".to_string(), Value::Object(properties)), - ("additionalProperties".to_string(), Value::Bool(false)), - ]); - if !required.is_empty() { - schema.insert("required".to_string(), Value::Array(required)); - } - Value::Object(schema) -} - -fn param_json_schema(param: &ParamSpec) -> Value { - let ty = param.ty.trim(); - if let Some(item) = ty.strip_suffix("[]") { - return json!({ - "type": "array", - "items": type_label_json_schema(item) - }); - } - if ty.contains('|') - && ty.split('|').all(|part| { - !matches!( - part.trim(), - "string" | "number" | "integer" | "boolean" | "object" | "array" | "null" - ) - }) - { - return json!({ - "type": "string", - "enum": ty.split('|').map(str::trim).collect::>() - }); - } - if ty.contains('|') { - return json!({ - "anyOf": ty.split('|').map(|part| type_label_json_schema(part.trim())).collect::>() - }); - } - type_label_json_schema(ty) -} - -fn type_label_json_schema(ty: &str) -> Value { - match ty { - "string" => json!({ "type": "string" }), - "integer" | "int" | "i64" | "u64" | "usize" => json!({ "type": "integer" }), - "number" | "float" | "f64" => json!({ "type": "number" }), - "boolean" | "bool" => json!({ "type": "boolean" }), - "object" | "json" | "value" => json!({ "type": "object" }), - "array" | "list" => json!({ "type": "array" }), - "null" => json!({ "type": "null" }), - _ => json!({ "description": format!("Lab type hint: {ty}") }), - } -} - #[must_use] pub fn typescript_binding(id: &str, type_name: &str, schema: &Value) -> String { let args_type = typescript_type(schema, 0); @@ -1527,31 +1064,26 @@ fn typescript_property_name(name: &str) -> String { mod tests { use boa_engine::{Context, Source}; use serde_json::json; - use std::future::Future; - use std::pin::Pin; use super::{ CodeModeSchemaResponse, CodeModeSearchCandidate, CodeModeToolId, CodeModeToolRef, - action_input_schema, code_mode_upstream_error_info, configure_code_mode_runtime_limits, + code_mode_upstream_error_info, configure_code_mode_runtime_limits, sanitize_code_mode_schema, }; - use crate::dispatch::error::ToolError; - use crate::registry::{RegisteredService, RegisteredServiceKind, ToolRegistry}; - use lab_apis::core::action::{ActionSpec, ParamSpec}; #[test] - fn parses_lab_action_id() { - let parsed = CodeModeToolId::parse("lab::gateway.gateway.schema").unwrap(); - assert_eq!( - parsed, - CodeModeToolId { - raw: "lab::gateway.gateway.schema".to_string(), - reference: CodeModeToolRef::LabAction { - service: "gateway".to_string(), - action: "gateway.schema".to_string(), - }, + fn parse_rejects_lab_id() { + let err = + CodeModeToolId::parse("lab::radarr.movie.search").expect_err("lab:: ids are rejected"); + match err { + super::ToolError::Sdk { sdk_kind, message } => { + assert_eq!(sdk_kind, "unknown_tool"); + assert!(message.contains("lab::")); + assert!(message.contains("tool_execute")); + assert!(message.contains("\"radarr\"")); } - ); + other => panic!("expected unknown_tool, got {other:?}"), + } } #[test] @@ -1600,121 +1132,77 @@ mod tests { assert!(!counts_as_failure); } - const DESTRUCTIVE_ACTIONS: &[ActionSpec] = &[ActionSpec { - name: "danger", - description: "Dangerous test action", - destructive: true, - params: &[], - returns: "object", - }]; - - fn echo_dispatch( - _action: String, - params: serde_json::Value, - ) -> Pin> + Send>> { - Box::pin(async move { Ok(params) }) - } - - fn destructive_test_registry() -> ToolRegistry { - let mut registry = ToolRegistry::new(); - registry.register(RegisteredService { - name: "gateway", - description: "Gateway", - category: "bootstrap", - kind: RegisteredServiceKind::BootstrapOperator, - status: "available", - actions: DESTRUCTIVE_ACTIONS, - dispatch: echo_dispatch, - }); - registry - } - #[tokio::test] - async fn mcp_code_mode_requires_top_level_confirmation_for_destructive_actions() { - let registry = destructive_test_registry(); + async fn schema_rejects_lab_id() { + let registry = super::ToolRegistry::new(); let broker = super::CodeModeBroker::new(®istry, None); let err = broker - .call_tool_id( - "lab::gateway.danger", - json!({"confirm": true}), + .schema( + "lab::radarr.movie.search", super::CodeModeCaller::TrustedLocal, - super::CodeModeSurface::Mcp { - expose_builtin_services: true, - allow_destructive_actions: false, - }, + super::CodeModeSurface::Cli, ) .await - .expect_err("mcp destructive action should require top-level code_execute confirm"); + .expect_err("schema should reject lab:: id"); - assert_eq!(err.kind(), "confirmation_required"); + match err { + super::ToolError::Sdk { sdk_kind, message } => { + assert_eq!(sdk_kind, "unknown_tool"); + assert!(message.contains("tool_execute")); + assert!(message.contains("\"radarr\"")); + } + other => panic!("expected unknown_tool, got {other:?}"), + } } #[tokio::test] - async fn code_mode_schema_requires_admin_for_admin_only_actions() { - let registry = destructive_test_registry(); + async fn code_search_returns_only_upstream_candidates() { + let registry = super::ToolRegistry::new(); let broker = super::CodeModeBroker::new(®istry, None); - let err = broker - .schema( - "lab::gateway.danger", - super::CodeModeCaller::Scoped { - scopes: vec!["lab".to_string()], - subject: Some("subject-1".to_string()), - }, - super::CodeModeSurface::Mcp { - expose_builtin_services: true, - allow_destructive_actions: true, - }, + let results = broker + .search( + "movie.search", + 10, + super::CodeModeCaller::TrustedLocal, + super::CodeModeSurface::Cli, ) .await - .expect_err("schema for admin-only action must require admin scope"); + .expect("search ok"); - assert_eq!(err.kind(), "forbidden"); + for candidate in &results { + assert!( + !candidate.id.starts_with("lab::"), + "found lab:: candidate after drop: {}", + candidate.id + ); + } } #[tokio::test] - async fn code_mode_overwrites_gateway_provenance_fields() { - let registry = destructive_test_registry(); + async fn code_execute_call_tool_lab_id_returns_unknown_tool() { + let registry = super::ToolRegistry::new(); let broker = super::CodeModeBroker::new(®istry, None); - let result = broker + let err = broker .call_tool_id( - "lab::gateway.danger", - json!({ - "confirm": true, - "origin": "spoofed", - "owner": {"raw": "spoofed"} - }), - super::CodeModeCaller::Scoped { - scopes: vec!["lab:admin".to_string()], - subject: Some("subject-1".to_string()), - }, - super::CodeModeSurface::Mcp { - expose_builtin_services: true, - allow_destructive_actions: true, - }, + "lab::radarr.movie.search", + json!({"query": "Matrix"}), + super::CodeModeCaller::TrustedLocal, + super::CodeModeSurface::Cli, ) .await - .unwrap(); + .expect_err("lab:: callTool id should return unknown_tool"); - assert_eq!(result.pointer("/origin"), Some(&json!("mcp:subject-1"))); - assert_eq!(result.pointer("/owner/raw"), Some(&json!("mcp:subject-1"))); - assert_eq!(result.pointer("/owner/surface"), Some(&json!("mcp"))); - } - - #[test] - fn builds_search_candidate_for_lab_action() { - let candidate = CodeModeSearchCandidate::lab_action( - "gateway", - "gateway.schema", - "Return gateway schema", - 10.0, - ); - assert_eq!(candidate.id, "lab::gateway.gateway.schema"); - assert_eq!(candidate.upstream, "lab"); - assert_eq!(candidate.name, "gateway.schema"); - assert!(candidate.schema_available); + match err { + super::ToolError::Sdk { sdk_kind, message } => { + assert_eq!(sdk_kind, "unknown_tool"); + assert!(message.contains("tool_execute")); + assert!(message.contains("\"radarr\"")); + } + other => panic!("expected unknown_tool, got {other:?}"), + } } #[test] @@ -1732,17 +1220,6 @@ mod tests { assert!(candidate.schema_available); } - #[test] - fn builds_lab_schema_response() { - let response = CodeModeSchemaResponse::lab_action( - "lab::gateway.gateway.schema", - "gateway.schema", - json!({"action": "gateway.schema"}), - ); - assert_eq!(response.kind, "lab_action"); - assert_eq!(response.schema_format, "lab_action_spec"); - } - #[test] fn builds_upstream_schema_response() { let response = CodeModeSchemaResponse::upstream_tool( @@ -1778,57 +1255,6 @@ mod tests { assert!(description.contains("")); } - #[test] - fn builds_action_input_schema_and_typescript_binding() { - const PARAMS: &[ParamSpec] = &[ - ParamSpec { - name: "query", - ty: "string", - required: true, - description: "Search query", - }, - ParamSpec { - name: "limit", - ty: "integer", - required: false, - description: "Maximum result count", - }, - ]; - let action = ActionSpec { - name: "issue.search", - description: "Search issues", - destructive: false, - params: PARAMS, - returns: "Issue[]", - }; - - let schema = action_input_schema(&action); - assert_eq!( - schema.pointer("/properties/query/type"), - Some(&json!("string")) - ); - assert_eq!( - schema.pointer("/properties/limit/type"), - Some(&json!("integer")) - ); - assert_eq!(schema.pointer("/required/0"), Some(&json!("query"))); - - let response = CodeModeSchemaResponse::lab_action_with_input_schema( - "lab::github.issue.search", - "issue.search", - json!({"action": "issue.search"}), - schema, - ); - assert!(response.bindings.typescript.contains("query: string;")); - assert!(response.bindings.typescript.contains("limit?: number;")); - assert!( - response - .bindings - .typescript - .contains("caller.callTool(\"lab::github.issue.search\", args)") - ); - } - #[test] fn configured_runtime_limits_reject_unbounded_loops() { let mut context = Context::default(); diff --git a/crates/lab/src/main.rs b/crates/lab/src/main.rs index aaaac869..3d6bc715 100644 --- a/crates/lab/src/main.rs +++ b/crates/lab/src/main.rs @@ -177,10 +177,7 @@ async fn main() -> ExitCode { // Silence upstream connect/discovery warnings — failures are surfaced // inline in command output (e.g. `gateway list`); raw events just leak // above the human-readable result. Set LAB_LOG=labby=warn to see them. - Some( - "labby=warn,labby::dispatch::upstream=error,lab_apis=warn,rmcp=warn" - .to_string(), - ) + Some("labby=warn,labby::dispatch::upstream=error,lab_apis=warn,rmcp=warn".to_string()) } _ => None, }; diff --git a/crates/lab/src/mcp/server.rs b/crates/lab/src/mcp/server.rs index 1d7e9e56..92d49b0f 100644 --- a/crates/lab/src/mcp/server.rs +++ b/crates/lab/src/mcp/server.rs @@ -1129,7 +1129,7 @@ impl ServerHandler for LabMcpServer { }; tools.push(Tool::new( CODE_SEARCH_TOOL_NAME, - "Schema-first Code Mode discovery for Lab and proxied upstream tools. \ + "Schema-first Code Mode discovery for proxied upstream MCP tools. \ Returns stable tool ids, short descriptions, scores, and whether an \ exact schema is available. Use code_schema with the returned id before \ generating tool-call code.", @@ -1141,7 +1141,7 @@ impl ServerHandler for LabMcpServer { "properties": { "id": { "type": "string", - "description": "Stable id returned by code_search, e.g. lab::gateway.gateway.schema or upstream::github::search_issues" + "description": "Stable id returned by code_search, e.g. upstream::github::search_issues" } }, "required": ["id"] @@ -1152,8 +1152,8 @@ impl ServerHandler for LabMcpServer { tools.push(Tool::new( CODE_SCHEMA_TOOL_NAME, "Return the exact input contract for one Code Mode tool id. \ - Lab ids return the ActionSpec-derived action contract; upstream ids \ - return the upstream JSON Schema exposed by the gateway.", + Returns the upstream JSON Schema exposed by the gateway for a given \ + upstream:: tool id.", code_schema_schema, )); gateway_tool_count += 1; @@ -1172,7 +1172,7 @@ impl ServerHandler for LabMcpServer { }, "confirm": { "type": "boolean", - "description": "Required at the top level, in addition to per-call params.confirm, before Code Mode may execute destructive Lab actions." + "description": "Reserved for compatibility; Code Mode executes proxied upstream MCP tools only." } }, "required": ["code"] @@ -2751,7 +2751,6 @@ fn redact_subject_for_logging(subject: &str) -> String { impl LabMcpServer { fn code_mode_surface(&self, allow_destructive_actions: bool) -> CodeModeSurface { CodeModeSurface::Mcp { - expose_builtin_services: !matches!(self.node_role, Some(NodeRole::NonMaster)), allow_destructive_actions, } } @@ -3322,7 +3321,6 @@ pub fn extract_error_info(e: &anyhow::Error) -> (&'static str, String, Option Pin> + Send>> { - Box::pin(async { - tokio::time::sleep(Duration::from_secs(5)).await; - Ok(serde_json::json!({"ok": true})) - }) - } - - #[tokio::test] - async fn code_mode_timeout_covers_brokered_lab_calls() { - let mut registry = ToolRegistry::new(); - registry.register(RegisteredService { - name: "slow", - description: "Slow test service", - category: "bootstrap", - kind: crate::registry::RegisteredServiceKind::BootstrapOperator, - status: "available", - actions: SLOW_ACTIONS, - dispatch: slow_dispatch, - }); - let broker = CodeModeBroker::new(®istry, None); - - let started = std::time::Instant::now(); - let err = broker - .call_tool_id_before_deadline( - "lab::slow.wait", - serde_json::json!({}), - tokio::time::Instant::now() + Duration::from_millis(50), - CodeModeCaller::TrustedLocal, - CodeModeSurface::Cli, - ) - .await - .expect_err("brokered tool call should be bounded by Code Mode timeout"); - - assert!( - started.elapsed() < Duration::from_secs(2), - "timeout should not wait for the slow dispatch to finish" - ); - match err { - ToolError::Sdk { sdk_kind, .. } => assert_eq!(sdk_kind, "timeout"), - other => panic!("expected timeout sdk error, got {other:?}"), - } - } - #[tokio::test] async fn snapshot_catalog_hides_builtin_tools_when_tool_search_is_enabled() { let runtime = crate::dispatch::gateway::manager::GatewayRuntimeHandle::default(); From f7c8a4c3ab216fc000f29f39a69c8d99f0db9486 Mon Sep 17 00:00:00 2001 From: Jacob Magar Date: Mon, 25 May 2026 03:59:50 -0400 Subject: [PATCH 3/5] feat(code-mode): finish v2 epic --- Cargo.lock | 730 ++++++++++++++- crates/lab/Cargo.toml | 10 +- crates/lab/src/api/error.rs | 4 +- crates/lab/src/cli/gateway.rs | 86 +- crates/lab/src/config.rs | 68 +- .../gateway/code_execute_description.md | 58 ++ crates/lab/src/dispatch/gateway/code_mode.rs | 874 +++++++++++++----- crates/lab/src/mcp/CLAUDE.md | 4 +- crates/lab/src/mcp/catalog.rs | 2 - crates/lab/src/mcp/server.rs | 192 ++-- crates/lab/tests/code_mode_runner.rs | 11 +- docs/dev/CODE_MODE.md | 54 ++ docs/dev/ERRORS.md | 7 +- scripts/refresh-javy-plugin.sh | 21 + 14 files changed, 1677 insertions(+), 444 deletions(-) create mode 100644 crates/lab/src/dispatch/gateway/code_execute_description.md create mode 100644 docs/dev/CODE_MODE.md create mode 100755 scripts/refresh-javy-plugin.sh diff --git a/Cargo.lock b/Cargo.lock index 1179e526..27fe7933 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -216,6 +225,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arc-swap" version = "1.9.1" @@ -413,6 +428,26 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -536,7 +571,7 @@ dependencies = [ "icu_normalizer", "indexmap 2.14.0", "intrusive-collections", - "itertools", + "itertools 0.14.0", "num-bigint", "num-integer", "num-traits", @@ -648,6 +683,9 @@ name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytemuck" @@ -711,6 +749,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -794,6 +841,17 @@ dependencies = [ "inout 0.2.2", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.6.1" @@ -858,6 +916,15 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -970,6 +1037,144 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a04121a197fde2fe896f8e7cac9812fc41ed6ee9c63e1906090f9f497845f6" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a09e699a94f477303820fb2167024f091543d6240783a2d3b01a3f21c42bc744" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f07732c662a9755529e332d86f8c5842171f6e98ba4d5976a178043dad838654" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18391da761cf362a06def7a7cf11474d79e55801dd34c2e9ba105b33dc0aef88" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3a09b3042c69810d255aef59ddc3b3e4c0644d1d90ecfd6e3837798cc88a3c" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75817926ec812241889208d1b190cadb7fedded4592a4bb01b8524babb9e4849" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859158f87a59476476eda3884d883c32e08a143cf3d315095533b362a3250a63" + +[[package]] +name = "cranelift-control" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b65a9aec442d715cbf54d14548b8f395476c09cef7abe03e104a378291ab88" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334c99a7e86060c24028732efd23bac84585770dcb752329c69f135d64f2fc1" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ac6c095aa5b3e845d7ca3461e67e2b65249eb5401477a5ff9100369b745111" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d3d992870ed4f0f2e82e2175275cb3a123a46e9660c6558c46417b822c91fa" + +[[package]] +name = "cranelift-native" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee32e36beaf80f309edb535274cfe0349e1c5cf5799ba2d9f42e828285c6b52e" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9" + [[package]] name = "crc32fast" version = "1.5.0" @@ -1508,6 +1713,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -1526,6 +1743,26 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "env_filter", + "log", +] + [[package]] name = "equator" version = "0.4.2" @@ -1890,6 +2127,23 @@ dependencies = [ "polyval 0.7.1", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap 2.14.0", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "globset" version = "0.4.18" @@ -1952,6 +2206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash 0.1.5", + "serde", ] [[package]] @@ -2483,6 +2738,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2498,6 +2762,20 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "javy" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2ed172feeacbcb567c8c0d927254b73150163cf7bbcfdab9c40fb25cd46d8" +dependencies = [ + "anyhow", + "bitflags", + "fastrand", + "quickcheck", + "rquickjs", + "serde", +] + [[package]] name = "jiff" version = "0.2.24" @@ -2746,6 +3024,7 @@ dependencies = [ "indexmap 2.14.0", "indicatif", "is-terminal", + "javy", "jiff", "jsonwebtoken", "lab-apis", @@ -2791,6 +3070,7 @@ dependencies = [ "utoipa-scalar", "uuid", "walkdir", + "wasmtime", "wiremock", "xxhash-rust", ] @@ -2816,6 +3096,16 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.16" @@ -2878,6 +3168,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.2.0" @@ -2905,6 +3204,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2930,6 +3238,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2987,6 +3301,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -3148,6 +3472,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3563,6 +3899,18 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -3680,6 +4028,29 @@ dependencies = [ "unarray", ] +[[package]] +name = "pulley-interpreter" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9812652c1feb63cf39f8780cecac154a32b22b3665806c733cd4072547233a4" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56000349b6896e3d44286eb9c330891237f40b27fd43c1ccc84547d0b463cb40" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -3696,6 +4067,17 @@ dependencies = [ "serde", ] +[[package]] +name = "quickcheck" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c589f335db0f6aaa168a7cd27b1fc6920f5e1470c804f814d9cd6e62a0f70b" +dependencies = [ + "env_logger", + "log", + "rand 0.10.1", +] + [[package]] name = "quinn" version = "0.11.9" @@ -3929,6 +4311,20 @@ dependencies = [ "syn", ] +[[package]] +name = "regalloc2" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + [[package]] name = "regex" version = "1.12.3" @@ -3968,6 +4364,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "relative-path" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0" +dependencies = [ + "serde", +] + [[package]] name = "reqwest" version = "0.13.3" @@ -4092,6 +4497,54 @@ dependencies = [ "syn", ] +[[package]] +name = "rquickjs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50dc6d6c587c339edb4769cf705867497a2baf0eca8b4645fa6ecd22f02c77a" +dependencies = [ + "rquickjs-core", + "rquickjs-macro", +] + +[[package]] +name = "rquickjs-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bf7840285c321c3ab20e752a9afb95548c75cd7f4632a0627cea3507e310c1" +dependencies = [ + "hashbrown 0.16.1", + "relative-path", + "rquickjs-sys", +] + +[[package]] +name = "rquickjs-macro" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7106215ff41a5677b104906a13e1a440b880f4b6362b5dc4f3978c267fad2b80" +dependencies = [ + "convert_case", + "fnv", + "ident_case", + "indexmap 2.14.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "rquickjs-core", + "syn", +] + +[[package]] +name = "rquickjs-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27344601ef27460e82d6a4e1ecb9e7e99f518122095f3c51296da8e9be2b9d83" +dependencies = [ + "bindgen", + "cc", +] + [[package]] name = "rsa" version = "0.9.10" @@ -4949,6 +5402,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -5157,6 +5613,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -5170,6 +5632,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thin-vec" version = "0.2.18" @@ -5921,6 +6392,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" +dependencies = [ + "leb128fmt", + "wasmparser 0.243.0", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -5928,7 +6409,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.250.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2271adb766023046af314460f1fae02cc34ea16d736d93404d3b65be44270923" +dependencies = [ + "leb128fmt", + "wasmparser 0.250.0", ] [[package]] @@ -5939,8 +6430,8 @@ checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", ] [[package]] @@ -5956,6 +6447,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver 1.0.28", + "serde", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -5968,6 +6472,218 @@ dependencies = [ "semver 1.0.28", ] +[[package]] +name = "wasmparser" +version = "0.250.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071d99cdfb8111603ed05500506c3298a940b58d609dd0259d3981785dd33556" +dependencies = [ + "bitflags", + "indexmap 2.14.0", + "semver 1.0.28", +] + +[[package]] +name = "wasmprinter" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.243.0", +] + +[[package]] +name = "wasmtime" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2a83182bf04af87571b4c642300479501684f26bab5597f68f68cded5b098fd" +dependencies = [ + "addr2line", + "anyhow", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb201c41aa23a3642365cfb2e4a183573d85127a3c9d528f56b9997c984541ab" +dependencies = [ + "anyhow", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap 2.14.0", + "log", + "object", + "postcard", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.243.0", + "wasmparser 0.243.0", + "wasmprinter", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633e889cdae76829738db0114ab3b02fce51ea4a1cd9675a67a65fce92e8b418" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools 0.14.0", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-math", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb126adc5d0c72695cfb77260b357f1b81705a0f8fa30b3944e7c2219c17341" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e66ff7f90a8002187691ff6237ffd09f954a0ebb9de8b2ff7f5c62632134120" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b96df23179ae16d54fb3a420f84ffe4383ec9dd06fad3e5bc782f85f66e8e08" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-math" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d1380926682b44c383e9a67f47e7a95e60c6d3fa8c072294dab2c7de6168a0" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-internal-slab" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b63cbea1c0192c7feb7c0dfb35f47166988a3742f29f46b585ef57246c65764" + +[[package]] +name = "wasmtime-internal-unwinder" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c392c7e5fb891a7416e3c34cfbd148849271e8c58744fda875dde4bec4d6a" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f8b9796a3f0451a7b702508b303d654de640271ac80287176de222f187a237" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wast" +version = "250.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e9294a1f0204aeb5c47e95165517f43ef3cc895918c4f3e939380d4c290f4a" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.250.0", +] + +[[package]] +name = "wat" +version = "1.250.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a549ed329a70e444e0f7796391ab2a87d0aef30ddde9f60e16e429224fafd02" +dependencies = [ + "wast", +] + [[package]] name = "web-sys" version = "0.3.97" @@ -6509,9 +7225,9 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", + "wasm-encoder 0.244.0", "wasm-metadata", - "wasmparser", + "wasmparser 0.244.0", "wit-parser", ] @@ -6530,7 +7246,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.244.0", ] [[package]] diff --git a/crates/lab/Cargo.toml b/crates/lab/Cargo.toml index c9e85222..2c2a0c3e 100644 --- a/crates/lab/Cargo.toml +++ b/crates/lab/Cargo.toml @@ -43,6 +43,13 @@ anyhow.workspace = true thiserror.workspace = true jiff.workspace = true boa_engine = "0.21.1" +javy = { version = "7.0.0", optional = true } +wasmtime = { version = "41.0.4", optional = true, default-features = false, features = [ + "cranelift", + "pooling-allocator", + "runtime", + "wat", +] } tracing.workspace = true tracing-subscriber.workspace = true @@ -128,7 +135,8 @@ path = "src/lib.rs" [features] acp_registry = ["lab-apis/acp_registry"] node-runtime = [] -all = ["lab-apis/all", "lab-admin", "acp_registry", "deploy", "extract", "mcpregistry", "gateway", "marketplace"] +all = ["lab-apis/all", "lab-admin", "acp_registry", "deploy", "extract", "mcpregistry", "gateway", "marketplace", "code_mode_wasm"] +code_mode_wasm = ["dep:javy", "dep:wasmtime"] gateway = [] marketplace = ["mcpregistry"] services-all = [ diff --git a/crates/lab/src/api/error.rs b/crates/lab/src/api/error.rs index 18ca6ae0..8726094c 100644 --- a/crates/lab/src/api/error.rs +++ b/crates/lab/src/api/error.rs @@ -32,7 +32,9 @@ impl IntoResponse for ToolError { | "path_traversal_rejected" | "invalid_encoding" => StatusCode::UNPROCESSABLE_ENTITY, "content_too_large" => StatusCode::PAYLOAD_TOO_LARGE, - "install_timeout" | "timeout" => StatusCode::GATEWAY_TIMEOUT, + "install_timeout" | "timeout" | "code_mode_timeout" | "code_mode_fuel_exhausted" => { + StatusCode::GATEWAY_TIMEOUT + } "oauth_needs_reauth" => StatusCode::UNAUTHORIZED, "oauth_state_invalid" => StatusCode::BAD_REQUEST, "forbidden" | "dev_preview_read_only" => StatusCode::FORBIDDEN, diff --git a/crates/lab/src/cli/gateway.rs b/crates/lab/src/cli/gateway.rs index ade4c8ff..ba7cc1f2 100644 --- a/crates/lab/src/cli/gateway.rs +++ b/crates/lab/src/cli/gateway.rs @@ -58,14 +58,13 @@ pub struct GatewayCodeArgs { #[derive(Debug, Subcommand)] pub enum GatewayCodeCommand { - /// Search Code Mode tool IDs by natural-language query + /// Filter the inlined Code Mode tool catalog with JavaScript Search { - query: String, - #[arg(long, default_value_t = 10)] - top_k: usize, + #[arg(long, conflicts_with = "file")] + code: Option, + #[arg(long)] + file: Option, }, - /// Show the schema and generated bindings for one Code Mode tool ID - Schema { id: String }, /// Execute a sandboxed JavaScript snippet that calls callTool(id, params) Exec { #[arg(long, conflicts_with = "file")] @@ -734,29 +733,13 @@ async fn run_gateway_code( let surface = CodeModeSurface::Cli; match args.command { - GatewayCodeCommand::Search { query, top_k } => { - let candidates = broker.search(&query, top_k, caller, surface).await?; - crate::output::print(&candidates, format)?; - } - GatewayCodeCommand::Schema { id } => { - let schema = broker.schema(&id, caller, surface).await?; - crate::output::print(&schema, format)?; + GatewayCodeCommand::Search { code, file } => { + let code = read_code_mode_source(code, file, CODE_MODE_CLI_MAX_SOURCE_BYTES)?; + let response = broker.search(&code, caller, surface).await?; + crate::output::print(&response, format)?; } GatewayCodeCommand::Exec { code, file } => { - let code = match (code, file) { - (Some(code), None) => code, - (None, Some(path)) => { - let metadata = std::fs::metadata(&path)?; - if metadata.len() > CODE_MODE_CLI_MAX_SOURCE_BYTES { - anyhow::bail!("Code Mode source file exceeds 20480 bytes"); - } - std::fs::read_to_string(path)? - } - _ => anyhow::bail!("provide exactly one of --code or --file"), - }; - if code.len() as u64 > CODE_MODE_CLI_MAX_SOURCE_BYTES { - anyhow::bail!("Code Mode source exceeds 20480 bytes"); - } + let code = read_code_mode_source(code, file, CODE_MODE_CLI_MAX_SOURCE_BYTES)?; let config = manager.code_mode_config().await; let max_tool_calls = config.max_tool_calls; let response = broker @@ -769,6 +752,28 @@ async fn run_gateway_code( Ok(ExitCode::SUCCESS) } +fn read_code_mode_source( + code: Option, + file: Option, + max_source_bytes: u64, +) -> Result { + let code = match (code, file) { + (Some(code), None) => code, + (None, Some(path)) => { + let metadata = std::fs::metadata(&path)?; + if metadata.len() > max_source_bytes { + anyhow::bail!("Code Mode source file exceeds {max_source_bytes} bytes"); + } + std::fs::read_to_string(path)? + } + _ => anyhow::bail!("provide exactly one of --code or --file"), + }; + if code.len() as u64 > max_source_bytes { + anyhow::bail!("Code Mode source exceeds {max_source_bytes} bytes"); + } + Ok(code) +} + async fn run_gateway_oauth_start( manager: Arc, args: GatewayOauthUpstreamArgs, @@ -1175,17 +1180,38 @@ mod tests { ]) .is_ok() ); - assert!(Cli::try_parse_from(["lab", "gateway", "code", "search", "movie.search"]).is_ok()); assert!( Cli::try_parse_from([ "lab", "gateway", "code", - "schema", - "upstream::github::search_issues", + "search", + "--code", + "async () => tools.slice(0, 3)", ]) .is_ok() ); + assert!( + Cli::try_parse_from([ + "lab", + "gateway", + "code", + "search", + "--code", + "async () => tools.filter(t => /github/i.test(t.id)).slice(0, 3)", + ]) + .is_ok() + ); + assert!( + Cli::try_parse_from([ + "lab", + "gateway", + "code", + "schema", + "upstream::github::search_issues" + ]) + .is_err() + ); assert!( Cli::try_parse_from([ "lab", diff --git a/crates/lab/src/config.rs b/crates/lab/src/config.rs index 68b74016..b085c8e4 100644 --- a/crates/lab/src/config.rs +++ b/crates/lab/src/config.rs @@ -496,10 +496,18 @@ fn default_code_mode_max_tool_calls() -> usize { 8 } +fn default_code_mode_max_response_bytes() -> usize { + 24 * 1024 +} + +fn default_code_mode_max_response_tokens() -> usize { + 6_000 +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CodeModeConfig { - /// Enable the constrained Code Mode executor. Discovery and schema lookup - /// can be enabled through `[tool_search]` without enabling execution. + /// Enable the constrained Code Mode executor. Catalog search can be enabled + /// through `[tool_search]` without enabling execution. #[serde(default)] pub enabled: bool, /// Maximum wall-clock time for one Code Mode execution. @@ -508,6 +516,12 @@ pub struct CodeModeConfig { /// Maximum host-brokered tool calls allowed in one Code Mode execution. #[serde(default = "default_code_mode_max_tool_calls")] pub max_tool_calls: usize, + /// Maximum serialized response envelope size returned by code_execute. + #[serde(default = "default_code_mode_max_response_bytes")] + pub max_response_bytes: usize, + /// Approximate maximum response tokens returned by code_execute. + #[serde(default = "default_code_mode_max_response_tokens")] + pub max_response_tokens: usize, } impl Default for CodeModeConfig { @@ -516,6 +530,8 @@ impl Default for CodeModeConfig { enabled: false, timeout_ms: default_code_mode_timeout_ms(), max_tool_calls: default_code_mode_max_tool_calls(), + max_response_bytes: default_code_mode_max_response_bytes(), + max_response_tokens: default_code_mode_max_response_tokens(), } } } @@ -532,6 +548,16 @@ impl CodeModeConfig { value: self.max_tool_calls, }); } + if !(1024..=1024 * 1024).contains(&self.max_response_bytes) { + return Err(ConfigError::InvalidCodeModeMaxResponseBytes { + value: self.max_response_bytes, + }); + } + if !(256..=256_000).contains(&self.max_response_tokens) { + return Err(ConfigError::InvalidCodeModeMaxResponseTokens { + value: self.max_response_tokens, + }); + } Ok(()) } } @@ -986,6 +1012,10 @@ pub enum ConfigError { InvalidCodeModeTimeout { value: u64 }, #[error("gateway code_mode.max_tool_calls={value} is invalid — expected 1..=50")] InvalidCodeModeMaxToolCalls { value: usize }, + #[error("gateway code_mode.max_response_bytes={value} is invalid — expected 1024..=1048576")] + InvalidCodeModeMaxResponseBytes { value: usize }, + #[error("gateway code_mode.max_response_tokens={value} is invalid — expected 256..=256000")] + InvalidCodeModeMaxResponseTokens { value: usize }, #[error("protected MCP route '{name}' has invalid {field}: {value}")] InvalidProtectedRoute { name: String, @@ -2842,6 +2872,8 @@ url = "https://acme.example.com/mcp" assert!(!default_cfg.code_mode.enabled); assert_eq!(default_cfg.code_mode.timeout_ms, 5000); assert_eq!(default_cfg.code_mode.max_tool_calls, 8); + assert_eq!(default_cfg.code_mode.max_response_bytes, 24 * 1024); + assert_eq!(default_cfg.code_mode.max_response_tokens, 6000); let cfg = toml::from_str::( r#" @@ -2849,6 +2881,8 @@ url = "https://acme.example.com/mcp" enabled = true timeout_ms = 2500 max_tool_calls = 3 +max_response_bytes = 12000 +max_response_tokens = 3000 "#, ) .expect("root code_mode parses"); @@ -2856,6 +2890,8 @@ max_tool_calls = 3 assert!(cfg.code_mode.enabled); assert_eq!(cfg.code_mode.timeout_ms, 2500); assert_eq!(cfg.code_mode.max_tool_calls, 3); + assert_eq!(cfg.code_mode.max_response_bytes, 12000); + assert_eq!(cfg.code_mode.max_response_tokens, 3000); } #[test] @@ -2885,6 +2921,34 @@ max_tool_calls = 0 cfg.validate(), Err(ConfigError::InvalidCodeModeMaxToolCalls { value: 0 }) )); + + let cfg = toml::from_str::( + r#" +[code_mode] +timeout_ms = 5000 +max_tool_calls = 8 +max_response_bytes = 100 +"#, + ) + .expect("code_mode parses"); + assert!(matches!( + cfg.validate(), + Err(ConfigError::InvalidCodeModeMaxResponseBytes { value: 100 }) + )); + + let cfg = toml::from_str::( + r#" +[code_mode] +timeout_ms = 5000 +max_tool_calls = 8 +max_response_tokens = 100 +"#, + ) + .expect("code_mode parses"); + assert!(matches!( + cfg.validate(), + Err(ConfigError::InvalidCodeModeMaxResponseTokens { value: 100 }) + )); } #[test] diff --git a/crates/lab/src/dispatch/gateway/code_execute_description.md b/crates/lab/src/dispatch/gateway/code_execute_description.md new file mode 100644 index 00000000..52b78a11 --- /dev/null +++ b/crates/lab/src/dispatch/gateway/code_execute_description.md @@ -0,0 +1,58 @@ +Execute JavaScript in the Code Mode sandbox against proxied upstream MCP tools. + +Use IDs returned by `code_search`. `Promise.all([...])` dispatches `callTool` +requests in parallel, so batch independent read-only calls instead of awaiting +them serially. + +```ts +type CodeModeToolId = `upstream::${string}::${string}`; + +type CodeModeError = { + kind: + | "unknown_tool" + | "unknown_action" + | "missing_param" + | "invalid_param" + | "validation_failed" + | "confirmation_required" + | "auth_failed" + | "rate_limited" + | "network_error" + | "server_error" + | "decode_error" + | "internal_error" + | "timeout" + | "tool_call_limit_exceeded" + | "code_mode_timeout" + | "code_mode_fuel_exhausted"; + message: string; + valid?: string[]; + hint?: string; + retry_after_ms?: number; +}; + +declare function callTool( + id: CodeModeToolId, + params: Record +): Promise; + +// Successful return: the upstream tool's structuredContent if present, +// else the parsed text of the first content[0] block. Never the raw MCP envelope. +// To recover: const env: CodeModeError = JSON.parse(String(e.message)); +// switch (env.kind) { ... } +// Retry-safe: rate_limited (honor retry_after_ms), timeout, network_error, code_mode_timeout +// Fix-and-retry: missing_param, invalid_param, validation_failed, confirmation_required, code_mode_fuel_exhausted +// Terminal: unknown_tool, unknown_action, auth_failed, server_error, internal_error, decode_error +``` + +Results are capped to the configured Code Mode envelope budget, defaulting to +24KB and roughly 6000 tokens. Oversized per-call results are replaced with a +truncation marker containing `truncated`, `original_size`, `original_tokens`, +`preview`, and `next_action`. + +Fuel budget guidance: +- Base overhead: about 100K fuel for the JS module and promise scheduler. +- Per `callTool` boundary: about 2K fuel for promise plumbing and host dispatch. +- Default 50M fuel is intended for heavy fan-out plus moderate result processing. +- Hitting `code_mode_fuel_exhausted` means split the work across calls or reduce + local processing over large result arrays. diff --git a/crates/lab/src/dispatch/gateway/code_mode.rs b/crates/lab/src/dispatch/gateway/code_mode.rs index d9117c22..29857f5f 100644 --- a/crates/lab/src/dispatch/gateway/code_mode.rs +++ b/crates/lab/src/dispatch/gateway/code_mode.rs @@ -1,15 +1,18 @@ use std::cell::RefCell; +#[cfg(not(feature = "code_mode_wasm"))] use std::collections::HashMap; use std::io::{self, BufRead, BufReader, BufWriter, Write}; use std::process::ExitCode; use std::process::Stdio; use std::time::Duration; -use boa_engine::builtins::promise::{PromiseState, ResolvingFunctions}; +use boa_engine::builtins::promise::PromiseState; +#[cfg(not(feature = "code_mode_wasm"))] +use boa_engine::builtins::promise::ResolvingFunctions; use boa_engine::object::builtins::JsPromise; -use boa_engine::{ - Context, JsArgs, JsError, JsNativeError, JsResult, JsValue, NativeFunction, Source, js_string, -}; +use boa_engine::{Context, JsValue, Source}; +#[cfg(not(feature = "code_mode_wasm"))] +use boa_engine::{JsArgs, JsError, JsNativeError, JsResult, JsValue, NativeFunction, js_string}; use futures::{FutureExt, StreamExt, stream::FuturesUnordered}; use rmcp::model::CallToolRequestParams; use serde::{Deserialize, Serialize}; @@ -25,6 +28,8 @@ use crate::registry::ToolRegistry; const LAB_ACTION_UNKNOWN_TOOL_HINT: &str = "Code Mode handles upstream MCP tools only. For Lab actions, use the `tool_execute` MCP tool: \ name= (e.g. \"radarr\"), arguments={action: \"\", params: {...}}. \ Example: tool_execute(name=\"radarr\", arguments={action:\"movie.search\", params:{query:\"Matrix\"}})."; +const CODE_SEARCH_CATALOG_SOFT_CAP_BYTES: usize = 256 * 1024; +const CODE_SEARCH_CATALOG_HARD_CAP_BYTES: usize = 512 * 1024; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CodeModeToolId { @@ -83,22 +88,25 @@ pub fn sanitize_code_mode_schema(schema: Option) -> Option { } #[derive(Debug, Clone, PartialEq, Serialize)] -pub struct CodeModeSearchCandidate { +pub struct CodeModeCatalogEntry { pub id: String, pub name: String, pub upstream: String, pub description: String, - pub score: f32, - pub schema_available: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub schema: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub note: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dropped_count: Option, } -impl CodeModeSearchCandidate { +impl CodeModeCatalogEntry { #[must_use] pub fn upstream_tool( upstream: &str, tool: &str, description: &str, - score: f32, schema: Option, ) -> Self { Self { @@ -106,27 +114,27 @@ impl CodeModeSearchCandidate { name: tool.to_string(), upstream: upstream.to_string(), description: description.to_string(), - score, - schema_available: schema.is_some(), + schema, + note: None, + dropped_count: None, } } -} -#[derive(Debug, Clone, PartialEq, Serialize)] -pub struct CodeModeSchemaResponse { - pub id: String, - pub kind: &'static str, - pub name: String, - pub upstream: String, - pub schema: Value, - pub schema_format: &'static str, - pub input_schema: Value, - pub bindings: CodeModeBindings, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct CodeModeBindings { - pub typescript: String, + #[must_use] + pub fn truncation_sentinel(dropped_count: usize) -> Self { + Self { + id: "__truncated__".to_string(), + name: "__truncated__".to_string(), + upstream: "__catalog__".to_string(), + description: "Catalog entries were dropped to fit the Code Mode inline catalog budget" + .to_string(), + schema: None, + note: Some( + "Some entries were dropped to fit the 256KB inline catalog cap. Use scout for full RRF discovery.".to_string(), + ), + dropped_count: Some(dropped_count), + } + } } #[derive(Debug, Clone, PartialEq, Serialize)] @@ -189,11 +197,10 @@ impl<'a> CodeModeBroker<'a> { pub async fn search( &self, - query: &str, - top_k: usize, + code: &str, caller: CodeModeCaller, _surface: CodeModeSurface, - ) -> Result, ToolError> { + ) -> Result { if !caller.can_read() { return Err(ToolError::Sdk { sdk_kind: "forbidden".to_string(), @@ -202,53 +209,20 @@ impl<'a> CodeModeBroker<'a> { } let Some(manager) = self.gateway_manager else { - return Ok(Vec::new()); + return Ok(Value::Array(Vec::new())); }; - let top_k = top_k.max(1).min(50); - match manager.search_tools(query, top_k, true).await { - Ok(upstream_results) => Ok(upstream_results - .into_iter() - .map(|result| { - CodeModeSearchCandidate::upstream_tool( - &result.upstream, - &result.name, - &result.description, - result.score, - result.input_schema, - ) - }) - .collect()), - Err(err) if err.kind() == "index_warming" => Ok(Vec::new()), - Err(err) => Err(err), - } - } - - pub async fn schema( - &self, - id: &str, - caller: CodeModeCaller, - _surface: CodeModeSurface, - ) -> Result { - if !caller.can_execute() { - return Err(ToolError::Sdk { - sdk_kind: "forbidden".to_string(), - message: "code_schema requires one of scopes: lab, lab:admin".to_string(), - }); - } - let parsed = CodeModeToolId::parse(id)?; - let Some(manager) = self.gateway_manager else { - return Err(ToolError::Sdk { - sdk_kind: "unknown_tool".to_string(), - message: "no gateway manager configured".to_string(), - }); - }; - match parsed.reference { - CodeModeToolRef::UpstreamTool { upstream, tool } => { - self.schema_for_upstream_tool(manager, &parsed.raw, &upstream, &tool) - .await - } - } + let (catalog, serialized_size, truncated) = self.code_search_catalog(manager).await?; + tracing::info!( + surface = "dispatch", + service = "code_search", + action = "catalog.build", + catalog_size_bytes = serialized_size, + entry_count = catalog.len(), + truncated, + "Code Mode search catalog ready" + ); + evaluate_code_search(code, &catalog) } pub async fn execute( @@ -273,37 +247,92 @@ impl<'a> CodeModeBroker<'a> { message: "code_execute requires one of scopes: lab, lab:admin".to_string(), }); } - self.execute_sandboxed( - code, - max_tool_calls.max(1).min(config.max_tool_calls.max(1)), - Duration::from_millis(config.timeout_ms.max(1)), - caller, - surface, - ) - .await + let response = self + .execute_sandboxed( + code, + max_tool_calls.max(1).min(config.max_tool_calls.max(1)), + Duration::from_millis(config.timeout_ms.max(1)), + caller, + surface, + ) + .await?; + Ok(truncate_execution_response( + response, + config.max_response_bytes, + config.max_response_tokens, + )) } - async fn schema_for_upstream_tool( + async fn code_search_catalog( &self, manager: &GatewayManager, - id: &str, - upstream: &str, - tool: &str, - ) -> Result { - let candidate = manager - .resolve_code_mode_upstream_tool(upstream, tool) - .await?; - let Some(schema) = sanitize_code_mode_schema(candidate.input_schema) else { + ) -> Result<(Vec, usize, bool), ToolError> { + let Some(pool) = manager.current_pool().await else { + return Ok((Vec::new(), 2, false)); + }; + + let mut entries = pool + .healthy_tools() + .await + .into_iter() + .map(|tool| { + let upstream = tool.upstream_name.to_string(); + let name = tool.tool.name.to_string(); + let description = tool + .tool + .description + .as_ref() + .map(|description| description.to_string()) + .unwrap_or_default(); + CodeModeCatalogEntry::upstream_tool( + &upstream, + &name, + &super::projection::sanitize_tool_text(&description, 2048), + sanitize_code_mode_schema(tool.input_schema), + ) + }) + .collect::>(); + + entries.sort_by(|a, b| { + a.upstream + .cmp(&b.upstream) + .then_with(|| a.name.cmp(&b.name)) + }); + + let mut serialized_size = serialized_catalog_size(&entries)?; + if serialized_size > CODE_SEARCH_CATALOG_HARD_CAP_BYTES { return Err(ToolError::Sdk { - sdk_kind: "schema_unavailable".to_string(), + sdk_kind: "invalid_param".to_string(), message: format!( - "upstream tool `{upstream}::{tool}` schema is unavailable or exceeds the safe return size" + "Code Mode inline catalog is {serialized_size} bytes, above the 512KB hard cap; use scout for full RRF discovery" ), }); - }; - Ok(CodeModeSchemaResponse::upstream_tool( - id, upstream, tool, schema, - )) + } + + let mut truncated = false; + if serialized_size > CODE_SEARCH_CATALOG_SOFT_CAP_BYTES { + truncated = true; + entries.sort_by(|a, b| { + (a.description.len() + a.name.len()) + .cmp(&(b.description.len() + b.name.len())) + .then_with(|| a.upstream.cmp(&b.upstream)) + .then_with(|| a.name.cmp(&b.name)) + }); + let original_len = entries.len(); + while !entries.is_empty() + && serialized_catalog_size_with_sentinel(&entries, original_len - entries.len())? + > CODE_SEARCH_CATALOG_SOFT_CAP_BYTES + { + entries.pop(); + } + let dropped = original_len - entries.len(); + if dropped > 0 { + entries.push(CodeModeCatalogEntry::truncation_sentinel(dropped)); + } + serialized_size = serialized_catalog_size(&entries)?; + } + + Ok((entries, serialized_size, truncated)) } async fn execute_sandboxed( @@ -637,6 +666,7 @@ struct CodeModeRunnerState { reader: BufReader, writer: BufWriter, next_seq: u64, + #[cfg(not(feature = "code_mode_wasm"))] pending_calls: HashMap, } @@ -648,20 +678,49 @@ thread_local! { static RUNNER_STATE: RefCell> = const { RefCell::new(None) }; } -impl CodeModeSchemaResponse { - #[must_use] - pub fn upstream_tool(id: &str, upstream: &str, tool: &str, schema: Value) -> Self { - Self { - id: id.to_string(), - kind: "upstream_tool", - name: tool.to_string(), - upstream: upstream.to_string(), - bindings: CodeModeBindings { - typescript: typescript_binding(id, "ToolArgs", &schema), - }, - input_schema: schema.clone(), - schema, - schema_format: "json_schema", +#[cfg(feature = "code_mode_wasm")] +#[allow(dead_code)] +mod wasm_runner { + use wasmtime::{Config, Engine, Instance, Module, Store, Trap}; + + pub const DEFAULT_SEARCH_FUEL: u64 = 10_000_000; + pub const DEFAULT_EXECUTE_FUEL: u64 = 50_000_000; + + pub fn engine() -> Result { + let mut config = Config::new(); + config.consume_fuel(true); + config.epoch_interruption(true); + Engine::new(&config) + } + + pub fn run_wasm_i32_export_for_smoke( + wat: &str, + export_name: &str, + fuel: u64, + ) -> Result { + let engine = engine()?; + let module = Module::new(&engine, wat)?; + let mut store = Store::new(&engine, ()); + store.set_fuel(fuel)?; + store.set_epoch_deadline(u64::MAX); + let instance = Instance::new(&mut store, &module, &[])?; + let func = instance.get_typed_func::<(), i32>(&mut store, export_name)?; + func.call(&mut store, ()) + } + + pub fn trap_kind(error: &wasmtime::Error) -> Option<&'static str> { + let message = error.to_string(); + if message.contains("fuel") { + return Some("code_mode_fuel_exhausted"); + } + if message.contains("epoch") || message.contains("interrupt") { + return Some("code_mode_timeout"); + } + let trap = error.downcast_ref::()?; + match trap { + Trap::OutOfFuel => Some("code_mode_fuel_exhausted"), + Trap::Interrupt => Some("code_mode_timeout"), + _ => Some("code_execution_failed"), } } } @@ -682,6 +741,143 @@ fn lab_action_unknown_tool() -> ToolError { } } +fn serialized_catalog_size(entries: &[CodeModeCatalogEntry]) -> Result { + serde_json::to_vec(entries) + .map(|bytes| bytes.len()) + .map_err(|err| ToolError::Sdk { + sdk_kind: "internal_error".to_string(), + message: format!("failed to serialize Code Mode catalog: {err}"), + }) +} + +fn serialized_catalog_size_with_sentinel( + entries: &[CodeModeCatalogEntry], + dropped_count: usize, +) -> Result { + let mut candidate = entries.to_vec(); + if dropped_count > 0 { + candidate.push(CodeModeCatalogEntry::truncation_sentinel(dropped_count)); + } + serialized_catalog_size(&candidate) +} + +fn evaluate_code_search(code: &str, catalog: &[CodeModeCatalogEntry]) -> Result { + let catalog_json = serde_json::to_string(catalog).map_err(|err| ToolError::Sdk { + sdk_kind: "internal_error".to_string(), + message: format!("failed to encode Code Mode catalog: {err}"), + })?; + let wrapped = format!( + "const tools = {catalog_json};\n\ + (async () => {{\n\ + const __codeModeSearch = ({code});\n\ + if (typeof __codeModeSearch !== 'function') {{\n\ + throw new TypeError('code_search code must evaluate to a function');\n\ + }}\n\ + return await __codeModeSearch();\n\ + }})()" + ); + + let mut context = Context::default(); + configure_code_mode_runtime_limits(&mut context); + let value = context + .eval(Source::from_bytes(wrapped.as_bytes())) + .map_err(|err| ToolError::Sdk { + sdk_kind: "invalid_param".to_string(), + message: format!("Code Mode search JavaScript failed to evaluate: {err}"), + })?; + let object = value.as_object().ok_or_else(|| ToolError::Sdk { + sdk_kind: "invalid_param".to_string(), + message: "Code Mode search script did not return a promise".to_string(), + })?; + let promise = JsPromise::from_object(object.clone()).map_err(|err| ToolError::Sdk { + sdk_kind: "invalid_param".to_string(), + message: format!("Code Mode search script did not return a promise: {err}"), + })?; + + for _ in 0..CODE_MODE_LOOP_ITERATION_LIMIT { + context.run_jobs().map_err(|err| ToolError::Sdk { + sdk_kind: "code_execution_failed".to_string(), + message: err.to_string(), + })?; + match promise.state() { + PromiseState::Fulfilled(value) => { + return value + .to_json(&mut context) + .map_err(|err| ToolError::Sdk { + sdk_kind: "code_execution_failed".to_string(), + message: format!("failed to serialize Code Mode search result: {err}"), + })? + .ok_or_else(|| ToolError::Sdk { + sdk_kind: "code_execution_failed".to_string(), + message: "Code Mode search result is not JSON-serializable".to_string(), + }); + } + PromiseState::Rejected(reason) => { + return Err(ToolError::Sdk { + sdk_kind: "code_execution_failed".to_string(), + message: js_value_message(&reason, &mut context), + }); + } + PromiseState::Pending => {} + } + } + + Err(ToolError::Sdk { + sdk_kind: "code_execution_failed".to_string(), + message: "Code Mode search script did not settle before the iteration limit".to_string(), + }) +} + +fn truncate_execution_response( + mut response: CodeModeExecutionResponse, + max_response_bytes: usize, + max_response_tokens: usize, +) -> CodeModeExecutionResponse { + if response_within_budget(&response, max_response_bytes, max_response_tokens) { + return response; + } + + for idx in (0..response.calls.len()).rev() { + if response_within_budget(&response, max_response_bytes, max_response_tokens) { + break; + } + let marker = truncation_marker(&response.calls[idx].result); + response.calls[idx].result = marker; + } + + response +} + +fn response_within_budget( + response: &CodeModeExecutionResponse, + max_response_bytes: usize, + max_response_tokens: usize, +) -> bool { + match serde_json::to_vec(response) { + Ok(bytes) => { + bytes.len() <= max_response_bytes + && estimated_tokens(bytes.len()) <= max_response_tokens.max(1) + } + Err(_) => false, + } +} + +fn estimated_tokens(byte_len: usize) -> usize { + byte_len.div_ceil(4).max(1) +} + +fn truncation_marker(value: &Value) -> Value { + let serialized = serde_json::to_string(value).unwrap_or_else(|_| "null".to_string()); + let preview = serialized.chars().take(1024).collect::(); + json!({ + "truncated": true, + "original_size": serialized.len(), + "original_tokens": estimated_tokens(serialized.len()), + "preview": preview, + "next_action": "Use a narrower query, request fewer fields, or split the work across multiple code_execute calls." + }) +} + async fn write_runner_input( stdin: &mut ChildStdin, input: &CodeModeRunnerInput, @@ -724,6 +920,8 @@ fn code_mode_canonical_error_kind(s: &str) -> &'static str { "decode_error" => "decode_error", "internal_error" => "internal_error", "upstream_error" => "upstream_error", + "code_mode_timeout" => "code_mode_timeout", + "code_mode_fuel_exhausted" => "code_mode_fuel_exhausted", _ => "internal_error", } } @@ -773,6 +971,7 @@ pub fn run_code_mode_runner_stdio() -> ExitCode { reader: BufReader::new(io::stdin()), writer: BufWriter::new(io::stdout()), next_seq: 0, + #[cfg(not(feature = "code_mode_wasm"))] pending_calls: HashMap::new(), }); }); @@ -788,6 +987,97 @@ pub fn run_code_mode_runner_stdio() -> ExitCode { ExitCode::SUCCESS } +#[cfg(feature = "code_mode_wasm")] +fn run_code_mode_runner() -> Result<(), String> { + let CodeModeRunnerInput::Start { code } = runner_read_input()? else { + return Err("runner expected start message".to_string()); + }; + + let mut config = javy::Config::default(); + config + .redirect_stdout_to_stderr(true) + .memory_limit(64 * 1024 * 1024) + .max_stack_size(CODE_MODE_STACK_SIZE_LIMIT); + let runtime = javy::Runtime::new(config).map_err(|err| err.to_string())?; + + runtime + .context() + .with(|cx| -> javy::quickjs::Result<()> { + let globals = cx.globals(); + globals.set( + "__labEmitToolCall", + javy::quickjs::Function::new( + cx.clone(), + javy::quickjs::prelude::MutFn::new(|cx, args| { + javy_emit_tool_call(javy::Args::hold(cx, args)) + }), + )?, + )?; + Ok(()) + }) + .map_err(javy_error_message)?; + + let wrapped = format!( + r#" +globalThis.__labPendingToolCalls = new Map(); +globalThis.callTool = (id, params = {{}}) => {{ + if (typeof id !== "string" || id.trim() === "") {{ + throw new TypeError("callTool id must be a non-empty string"); + }} + if (params === null || typeof params !== "object" || Array.isArray(params)) {{ + throw new TypeError("callTool params must be a JSON object"); + }} + return new Promise((resolve, reject) => {{ + const seq = globalThis.__labEmitToolCall(id, params); + globalThis.__labPendingToolCalls.set(seq, {{ resolve, reject }}); + }}); +}}; +globalThis.__labSettleToolCall = (message) => {{ + const input = JSON.parse(message); + const pending = globalThis.__labPendingToolCalls.get(input.seq); + if (!pending) {{ + throw new Error("runner received a response for an unknown tool call"); + }} + globalThis.__labPendingToolCalls.delete(input.seq); + if (input.type === "tool_result") {{ + pending.resolve(input.result); + return; + }} + if (input.type === "tool_error") {{ + pending.reject(new Error(`${{input.kind}}: ${{input.message}}`)); + return; + }} + throw new Error("runner received unexpected protocol message"); +}}; +globalThis.__labMainPromise = (async () => {{ +{code} +}})(); +"# + ); + + runtime + .context() + .with(|cx| cx.eval::<(), _>(wrapped)) + .map_err(javy_error_message)?; + + loop { + runtime + .resolve_pending_jobs() + .map_err(|err| err.to_string())?; + match javy_main_promise_state(&runtime)? { + JavyMainPromiseState::Resolved => break, + JavyMainPromiseState::Rejected(message) => return Err(message), + JavyMainPromiseState::Pending => { + let input = runner_read_input()?; + javy_settle_tool_promise(&runtime, &input)?; + } + } + } + + runner_emit(CodeModeRunnerOutput::Done) +} + +#[cfg(not(feature = "code_mode_wasm"))] fn run_code_mode_runner() -> Result<(), String> { let CodeModeRunnerInput::Start { code } = runner_read_input()? else { return Err("runner expected start message".to_string()); @@ -828,6 +1118,118 @@ fn run_code_mode_runner() -> Result<(), String> { runner_emit(CodeModeRunnerOutput::Done) } +#[cfg(feature = "code_mode_wasm")] +enum JavyMainPromiseState { + Pending, + Resolved, + Rejected(String), +} + +#[cfg(feature = "code_mode_wasm")] +fn javy_emit_tool_call(args: javy::Args<'_>) -> javy::quickjs::Result { + let (cx, args) = args.release(); + let id_value = args + .0 + .first() + .ok_or_else(|| javy_type_error(cx.clone(), "callTool id must be a non-empty string"))?; + let id = javy::val_to_string(&cx, id_value.clone()) + .map_err(|err| javy::to_js_error(cx.clone(), err))?; + if id.trim().is_empty() { + return Err(javy_type_error( + cx.clone(), + "callTool id must be a non-empty string", + )); + } + + let params_json = args + .0 + .get(1) + .map(|params| cx.json_stringify(params.clone())) + .transpose()? + .flatten() + .map(|params| params.to_string()) + .transpose()? + .unwrap_or_else(|| "{}".to_string()); + let params: Value = serde_json::from_str(¶ms_json).map_err(|err| { + javy_type_error( + cx.clone(), + format!("callTool params must be JSON-serializable: {err}"), + ) + })?; + if !params.is_object() { + return Err(javy_type_error( + cx.clone(), + "callTool params must be a JSON object", + )); + } + + let seq = RUNNER_STATE + .with(|state| { + let mut state = state.borrow_mut(); + let state = state + .as_mut() + .ok_or_else(|| "runner state is not initialized".to_string())?; + let seq = state.next_seq; + state.next_seq += 1; + Ok::<_, String>(seq) + }) + .map_err(|err| javy_type_error(cx.clone(), err))?; + + runner_emit(CodeModeRunnerOutput::ToolCall { seq, id, params }) + .map_err(|err| javy_type_error(cx, err))?; + Ok(seq) +} + +#[cfg(feature = "code_mode_wasm")] +fn javy_settle_tool_promise( + runtime: &javy::Runtime, + input: &CodeModeRunnerInput, +) -> Result<(), String> { + let message = serde_json::to_string(input).map_err(|err| err.to_string())?; + runtime + .context() + .with(|cx| -> javy::quickjs::Result<()> { + let settle: javy::quickjs::Function<'_> = cx.globals().get("__labSettleToolCall")?; + settle.call::<_, ()>((message,))?; + Ok(()) + }) + .map_err(javy_error_message)?; + runtime + .resolve_pending_jobs() + .map_err(|err| err.to_string()) +} + +#[cfg(feature = "code_mode_wasm")] +fn javy_main_promise_state(runtime: &javy::Runtime) -> Result { + runtime + .context() + .with(|cx| -> javy::quickjs::Result { + let promise: javy::quickjs::Promise<'_> = cx.globals().get("__labMainPromise")?; + match promise.result::>() { + None => Ok(JavyMainPromiseState::Pending), + Some(Ok(_)) => Ok(JavyMainPromiseState::Resolved), + Some(Err(err)) => { + let message = javy::from_js_error(cx.clone(), err).to_string(); + Ok(JavyMainPromiseState::Rejected(message)) + } + } + }) + .map_err(javy_error_message) +} + +#[cfg(feature = "code_mode_wasm")] +fn javy_type_error( + message_context: javy::quickjs::Ctx<'_>, + message: impl Into, +) -> javy::quickjs::Error { + javy::to_js_error(message_context, anyhow::anyhow!(message.into())) +} + +#[cfg(feature = "code_mode_wasm")] +fn javy_error_message(error: javy::quickjs::Error) -> String { + error.to_string() +} + fn configure_code_mode_runtime_limits(context: &mut Context) { let limits = context.runtime_limits_mut(); limits.set_loop_iteration_limit(CODE_MODE_LOOP_ITERATION_LIMIT); @@ -835,6 +1237,7 @@ fn configure_code_mode_runtime_limits(context: &mut Context) { limits.set_recursion_limit(CODE_MODE_RECURSION_LIMIT); } +#[cfg(not(feature = "code_mode_wasm"))] fn code_mode_call_tool_native( _this: &JsValue, args: &[JsValue], @@ -876,6 +1279,7 @@ fn code_mode_call_tool_native( Ok(promise.into()) } +#[cfg(not(feature = "code_mode_wasm"))] fn settle_code_mode_tool_promise( input: CodeModeRunnerInput, context: &mut Context, @@ -953,10 +1357,12 @@ fn runner_read_input() -> Result { }) } +#[cfg(not(feature = "code_mode_wasm"))] fn js_type_error(message: impl Into) -> JsError { JsNativeError::typ().with_message(message.into()).into() } +#[cfg(not(feature = "code_mode_wasm"))] fn js_error_message(error: JsError) -> String { error.to_string() } @@ -968,107 +1374,15 @@ fn js_value_message(value: &JsValue, context: &mut Context) -> String { .unwrap_or_else(|_| "promise rejected".to_string()) } -#[must_use] -pub fn typescript_binding(id: &str, type_name: &str, schema: &Value) -> String { - let args_type = typescript_type(schema, 0); - format!( - "export type {type_name} = {args_type};\n\n\ - export interface CodeModeToolCaller {{\n callTool(id: string, args: unknown): Promise;\n}}\n\n\ - export async function call(caller: CodeModeToolCaller, args: {type_name}): Promise {{\n return caller.callTool({id_literal}, args);\n}}\n", - id_literal = json!(id) - ) -} - -fn typescript_type(schema: &Value, indent: usize) -> String { - if let Some(values) = schema.get("enum").and_then(Value::as_array) { - let literals = values - .iter() - .filter_map(Value::as_str) - .map(|value| json!(value).to_string()) - .collect::>(); - if !literals.is_empty() { - return literals.join(" | "); - } - } - if let Some(any_of) = schema.get("anyOf").and_then(Value::as_array) { - return any_of - .iter() - .map(|schema| typescript_type(schema, indent)) - .collect::>() - .join(" | "); - } - match schema.get("type").and_then(Value::as_str) { - Some("string") => "string".to_string(), - Some("integer" | "number") => "number".to_string(), - Some("boolean") => "boolean".to_string(), - Some("null") => "null".to_string(), - Some("array") => { - let item = schema - .get("items") - .map(|items| typescript_type(items, indent)) - .unwrap_or_else(|| "unknown".to_string()); - format!("{item}[]") - } - Some("object") => object_typescript_type(schema, indent), - _ => "unknown".to_string(), - } -} - -fn object_typescript_type(schema: &Value, indent: usize) -> String { - let Some(properties) = schema.get("properties").and_then(Value::as_object) else { - return "Record".to_string(); - }; - if properties.is_empty() { - return "Record".to_string(); - } - let required = schema - .get("required") - .and_then(Value::as_array) - .into_iter() - .flatten() - .filter_map(Value::as_str) - .collect::>(); - let pad = " ".repeat(indent); - let child_pad = " ".repeat(indent + 2); - let mut lines = vec!["{".to_string()]; - for (name, property_schema) in properties { - let optional = if required.contains(name.as_str()) { - "" - } else { - "?" - }; - lines.push(format!( - "{child_pad}{}{optional}: {};", - typescript_property_name(name), - typescript_type(property_schema, indent + 2) - )); - } - lines.push(format!("{pad}}}")); - lines.join("\n") -} - -fn typescript_property_name(name: &str) -> String { - let mut chars = name.chars(); - let valid_first = chars - .next() - .is_some_and(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphabetic()); - let valid_rest = chars.all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric()); - if valid_first && valid_rest { - name.to_string() - } else { - json!(name).to_string() - } -} - #[cfg(test)] mod tests { use boa_engine::{Context, Source}; use serde_json::json; use super::{ - CodeModeSchemaResponse, CodeModeSearchCandidate, CodeModeToolId, CodeModeToolRef, - code_mode_upstream_error_info, configure_code_mode_runtime_limits, - sanitize_code_mode_schema, + CodeModeCatalogEntry, CodeModeExecutedCall, CodeModeExecutionResponse, CodeModeToolId, + CodeModeToolRef, code_mode_upstream_error_info, configure_code_mode_runtime_limits, + evaluate_code_search, sanitize_code_mode_schema, truncate_execution_response, }; #[test] @@ -1133,51 +1447,20 @@ mod tests { } #[tokio::test] - async fn schema_rejects_lab_id() { + async fn code_search_without_manager_returns_empty_catalog_result() { let registry = super::ToolRegistry::new(); let broker = super::CodeModeBroker::new(®istry, None); - let err = broker - .schema( - "lab::radarr.movie.search", - super::CodeModeCaller::TrustedLocal, - super::CodeModeSurface::Cli, - ) - .await - .expect_err("schema should reject lab:: id"); - - match err { - super::ToolError::Sdk { sdk_kind, message } => { - assert_eq!(sdk_kind, "unknown_tool"); - assert!(message.contains("tool_execute")); - assert!(message.contains("\"radarr\"")); - } - other => panic!("expected unknown_tool, got {other:?}"), - } - } - - #[tokio::test] - async fn code_search_returns_only_upstream_candidates() { - let registry = super::ToolRegistry::new(); - let broker = super::CodeModeBroker::new(®istry, None); - - let results = broker + let result = broker .search( - "movie.search", - 10, + "async () => tools", super::CodeModeCaller::TrustedLocal, super::CodeModeSurface::Cli, ) .await .expect("search ok"); - for candidate in &results { - assert!( - !candidate.id.starts_with("lab::"), - "found lab:: candidate after drop: {}", - candidate.id - ); - } + assert_eq!(result, json!([])); } #[tokio::test] @@ -1206,30 +1489,46 @@ mod tests { } #[test] - fn builds_search_candidate_for_upstream_tool() { - let candidate = CodeModeSearchCandidate::upstream_tool( + fn builds_catalog_entry_for_upstream_tool() { + let candidate = CodeModeCatalogEntry::upstream_tool( "github", "search_issues", "Search issues", - 8.5, Some(json!({"type": "object"})), ); assert_eq!(candidate.id, "upstream::github::search_issues"); assert_eq!(candidate.upstream, "github"); assert_eq!(candidate.name, "search_issues"); - assert!(candidate.schema_available); + assert_eq!(candidate.schema, Some(json!({"type": "object"}))); } #[test] - fn builds_upstream_schema_response() { - let response = CodeModeSchemaResponse::upstream_tool( - "upstream::github::search_issues", - "github", - "search_issues", - json!({"type": "object"}), + fn code_search_evaluates_filter_against_inline_catalog() { + let catalog = vec![ + CodeModeCatalogEntry::upstream_tool( + "github", + "search_issues", + "Search GitHub issues", + Some(json!({"type": "object"})), + ), + CodeModeCatalogEntry::upstream_tool("docker", "logs", "Read container logs", None), + ]; + + let result = evaluate_code_search( + "async () => tools.filter(t => /github/i.test(t.id)).map(t => ({id: t.id, schema: t.schema}))", + &catalog, + ) + .expect("search evaluates"); + + assert_eq!( + result, + json!([ + { + "id": "upstream::github::search_issues", + "schema": {"type": "object"} + } + ]) ); - assert_eq!(response.kind, "upstream_tool"); - assert_eq!(response.schema_format, "json_schema"); } #[test] @@ -1255,6 +1554,35 @@ mod tests { assert!(description.contains("")); } + #[test] + fn truncates_code_execute_response_with_per_call_marker() { + let response = CodeModeExecutionResponse { + calls: vec![ + CodeModeExecutedCall { + id: "upstream::github::search_issues".to_string(), + result: json!({"items": ["small"]}), + }, + CodeModeExecutedCall { + id: "upstream::github::list_issues".to_string(), + result: json!({"payload": "x".repeat(5000)}), + }, + ], + }; + + let truncated = truncate_execution_response(response, 1400, 6000); + + assert_eq!(truncated.calls[0].result, json!({"items": ["small"]})); + assert_eq!(truncated.calls[1].result["truncated"], json!(true)); + assert!(truncated.calls[1].result["original_size"].as_u64().unwrap() > 5000); + assert!( + truncated.calls[1].result["next_action"] + .as_str() + .unwrap() + .contains("narrower") + ); + assert!(serde_json::to_vec(&truncated).unwrap().len() <= 1400); + } + #[test] fn configured_runtime_limits_reject_unbounded_loops() { let mut context = Context::default(); @@ -1266,4 +1594,42 @@ mod tests { assert!(error.to_string().contains("iteration limit")); } + + #[cfg(feature = "code_mode_wasm")] + #[test] + fn wasm_runner_returns_42() { + let result = super::wasm_runner::run_wasm_i32_export_for_smoke( + r#" + (module + (func (export "run") (result i32) + i32.const 42)) + "#, + "run", + super::wasm_runner::DEFAULT_SEARCH_FUEL, + ) + .expect("wasm smoke runs"); + + assert_eq!(result, 42); + } + + #[cfg(feature = "code_mode_wasm")] + #[test] + fn wasm_runner_reports_fuel_exhaustion_kind() { + let err = super::wasm_runner::run_wasm_i32_export_for_smoke( + r#" + (module + (func (export "run") (result i32) + (loop br 0) + i32.const 0)) + "#, + "run", + 1, + ) + .expect_err("fuel should be exhausted"); + + assert_eq!( + super::wasm_runner::trap_kind(&err), + Some("code_mode_fuel_exhausted") + ); + } } diff --git a/crates/lab/src/mcp/CLAUDE.md b/crates/lab/src/mcp/CLAUDE.md index ac73daa0..4bac526e 100644 --- a/crates/lab/src/mcp/CLAUDE.md +++ b/crates/lab/src/mcp/CLAUDE.md @@ -33,8 +33,8 @@ For normal services, `dispatch//dispatch.rs` owns action routing, catal `dispatch/gateway/dispatch.rs` enforces the non-dispatch boundary. Do not add `dispatch/gateway-scout/` unless a second surface consumer is confirmed. -- `code_search`, `code_schema`, and `code_execute` are registered - directly in `mcp/server.rs` as gateway Code Mode meta-tools. MCP owns +- `code_search` and `code_execute` are registered directly in + `mcp/server.rs` as gateway Code Mode meta-tools. MCP owns tool registration, scope extraction, MCP request parsing, and `CallToolResult` envelope conversion. Code Mode business logic lives in `dispatch/gateway/code_mode.rs` so the native CLI can call the same diff --git a/crates/lab/src/mcp/catalog.rs b/crates/lab/src/mcp/catalog.rs index ab0277d0..06e08f48 100644 --- a/crates/lab/src/mcp/catalog.rs +++ b/crates/lab/src/mcp/catalog.rs @@ -10,7 +10,6 @@ use crate::mcp::prompts::list_all as list_builtin_prompts; pub(crate) const TOOL_SEARCH_TOOL_NAME: &str = "scout"; pub(crate) const TOOL_EXECUTE_TOOL_NAME: &str = "invoke"; pub(crate) const CODE_SEARCH_TOOL_NAME: &str = "code_search"; -pub(crate) const CODE_SCHEMA_TOOL_NAME: &str = "code_schema"; pub(crate) const CODE_EXECUTE_TOOL_NAME: &str = "code_execute"; pub(crate) const LEGACY_TOOL_INVOKE_TOOL_NAME: &str = "tool_invoke"; pub(crate) const LEGACY_TOOL_SEARCH_TOOL_NAME: &str = "tool_search"; @@ -201,7 +200,6 @@ impl LabMcpServer { if visibility.exposes_synthetic_tools() { tools.insert(TOOL_SEARCH_TOOL_NAME.to_string()); tools.insert(CODE_SEARCH_TOOL_NAME.to_string()); - tools.insert(CODE_SCHEMA_TOOL_NAME.to_string()); tools.insert(CODE_EXECUTE_TOOL_NAME.to_string()); tools.insert(TOOL_EXECUTE_TOOL_NAME.to_string()); } else { diff --git a/crates/lab/src/mcp/server.rs b/crates/lab/src/mcp/server.rs index 92d49b0f..43e603e4 100644 --- a/crates/lab/src/mcp/server.rs +++ b/crates/lab/src/mcp/server.rs @@ -29,9 +29,9 @@ use crate::dispatch::gateway::SHARED_GATEWAY_OAUTH_SUBJECT; use crate::dispatch::gateway::code_mode::{CodeModeBroker, CodeModeCaller, CodeModeSurface}; use crate::dispatch::gateway::manager::{GatewayManager, GatewayToolSearchResult}; use crate::mcp::catalog::{ - CODE_EXECUTE_TOOL_NAME, CODE_SCHEMA_TOOL_NAME, CODE_SEARCH_TOOL_NAME, - LEGACY_TOOL_EXECUTE_TOOL_NAME, LEGACY_TOOL_INVOKE_TOOL_NAME, LEGACY_TOOL_SEARCH_TOOL_NAME, - TOOL_EXECUTE_TOOL_NAME, TOOL_SEARCH_TOOL_NAME, + CODE_EXECUTE_TOOL_NAME, CODE_SEARCH_TOOL_NAME, LEGACY_TOOL_EXECUTE_TOOL_NAME, + LEGACY_TOOL_INVOKE_TOOL_NAME, LEGACY_TOOL_SEARCH_TOOL_NAME, TOOL_EXECUTE_TOOL_NAME, + TOOL_SEARCH_TOOL_NAME, }; use crate::mcp::elicitation::{ElicitResult, elicit_confirm}; use crate::mcp::envelope::{build_error, build_error_extra, build_success}; @@ -41,6 +41,8 @@ use crate::mcp::logging::{DispatchLogOutcome, logging_level_rank}; use crate::registry::ToolRegistry; const CODE_MODE_MAX_CODE_BYTES: usize = 20_000; +const CODE_EXECUTE_DESCRIPTION: &str = + include_str!("../dispatch/gateway/code_execute_description.md"); #[cfg(test)] use crate::mcp::peers::PeerNotifier; @@ -1119,44 +1121,29 @@ impl ServerHandler for LabMcpServer { let code_search_schema = match serde_json::json!({ "type": "object", "properties": { - "query": { "type": "string", "maxLength": 500 }, - "top_k": { "type": "integer", "minimum": 1, "maximum": 50 } + "code": { + "type": "string", + "maxLength": 4000, + "description": "JavaScript async arrow function that filters the inlined `tools` catalog and returns JSON-serializable results." + } }, - "required": ["query"] + "required": ["code"] }) { Value::Object(map) => Arc::new(map), _ => unreachable!("code_search schema must be an object"), }; tools.push(Tool::new( CODE_SEARCH_TOOL_NAME, - "Schema-first Code Mode discovery for proxied upstream MCP tools. \ - Returns stable tool ids, short descriptions, scores, and whether an \ - exact schema is available. Use code_schema with the returned id before \ - generating tool-call code.", + "Filter the inlined upstream MCP tool catalog with JavaScript. \ + The sandbox has `const tools = [...]`, where each entry has id, upstream, \ + name, description, and schema. Prefer matching on description; upstream \ + names can be terse. Examples: \ + `async () => tools.filter(t => /container.*log/i.test(t.description)).map(t => ({id:t.id, schema:t.schema}))`; \ + `async () => tools.find(t => t.id === \"upstream::github::search_issues\")`; \ + `async () => tools.filter(t => t.upstream === \"github\").slice(0, 20)`.", code_search_schema, )); gateway_tool_count += 1; - let code_schema_schema = match serde_json::json!({ - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Stable id returned by code_search, e.g. upstream::github::search_issues" - } - }, - "required": ["id"] - }) { - Value::Object(map) => Arc::new(map), - _ => unreachable!("code_schema schema must be an object"), - }; - tools.push(Tool::new( - CODE_SCHEMA_TOOL_NAME, - "Return the exact input contract for one Code Mode tool id. \ - Returns the upstream JSON Schema exposed by the gateway for a given \ - upstream:: tool id.", - code_schema_schema, - )); - gateway_tool_count += 1; let code_execute_schema = match serde_json::json!({ "type": "object", "properties": { @@ -1180,11 +1167,17 @@ impl ServerHandler for LabMcpServer { Value::Object(map) => Arc::new(map), _ => unreachable!("code_execute schema must be an object"), }; + debug_assert!(CODE_EXECUTE_DESCRIPTION.len() < 8192); + tracing::info!( + surface = "mcp", + service = "code_execute", + action = "tool.describe", + description_bytes = CODE_EXECUTE_DESCRIPTION.len(), + "registered Code Mode execute description" + ); tools.push(Tool::new( CODE_EXECUTE_TOOL_NAME, - "Execute a sandboxed Code Mode snippet through the Lab gateway broker. \ - Disabled by default; enable [code_mode].enabled to allow child-process execution. \ - Snippets may call callTool(id, params) with ids returned by code_search.", + CODE_EXECUTE_DESCRIPTION, code_execute_schema, )); gateway_tool_count += 1; @@ -1345,16 +1338,12 @@ impl ServerHandler for LabMcpServer { ); return Ok(CallToolResult::error(vec![Content::text(env.to_string())])); } - let query = args - .get("query") + let code = args + .get("code") .and_then(Value::as_str) .unwrap_or_default() .to_string(); - let query_hash = hash_arguments(&Value::String(query.clone())); - let requested_top_k = args - .get("top_k") - .and_then(Value::as_u64) - .map(|value| value as usize); + let code_hash = hash_arguments(&Value::String(code.clone())); let Some(manager) = &self.gateway_manager else { let envelope = build_error( &service, @@ -1366,20 +1355,13 @@ impl ServerHandler for LabMcpServer { envelope.to_string(), )])); }; - let top_k = match requested_top_k { - Some(value) => value, - None => manager.tool_search_config().await.top_k_default, - } - .max(1) - .min(50); tracing::info!( surface = "mcp", service = "code_search", action = "call_tool", subject, - query_hash = %query_hash, - query_len = query.len(), - top_k, + code_hash = %code_hash, + code_len = code.len(), "gateway code search start" ); let broker = CodeModeBroker::new(&self.registry, Some(manager)); @@ -1390,24 +1372,22 @@ impl ServerHandler for LabMcpServer { } }); return match broker - .search(&query, top_k, caller, self.code_mode_surface(false)) + .search(&code, caller, self.code_mode_surface(false)) .await { - Ok(candidates) => { + Ok(response) => { tracing::info!( surface = "mcp", service = "code_search", action = "call_tool", subject, - query_hash = %query_hash, - query_len = query.len(), - top_k, - result_count = candidates.len(), + code_hash = %code_hash, + code_len = code.len(), elapsed_ms = started.elapsed().as_millis(), "gateway code search ok" ); Ok(CallToolResult::success(vec![Content::text( - serde_json::to_string(&candidates).unwrap_or_else(|_| "[]".to_string()), + serde_json::to_string(&response).unwrap_or_else(|_| "null".to_string()), )])) } Err(err) => { @@ -1416,9 +1396,8 @@ impl ServerHandler for LabMcpServer { service = "code_search", action = "call_tool", subject, - query_hash = %query_hash, - query_len = query.len(), - top_k, + code_hash = %code_hash, + code_len = code.len(), elapsed_ms = started.elapsed().as_millis(), kind = err.kind(), error = %err, @@ -1429,85 +1408,6 @@ impl ServerHandler for LabMcpServer { } }; } - if service == CODE_SCHEMA_TOOL_NAME && self.gateway_code_mode_enabled().await { - let started = Instant::now(); - let subject = self.request_subject_log_tag(&context); - let auth = auth_context_from_extensions(&context.extensions); - if !tool_search_include_schema_allowed(auth, true) { - tracing::warn!( - surface = "mcp", - service = %service, - action = "call_tool", - subject, - elapsed_ms = started.elapsed().as_millis(), - kind = "forbidden", - "gateway code schema denied by scope" - ); - let env = build_error_extra( - &service, - "call_tool", - "forbidden", - "code_schema requires one of scopes: lab, lab:admin", - &serde_json::json!({ "required_scopes": ["lab", "lab:admin"] }), - ); - return Ok(CallToolResult::error(vec![Content::text(env.to_string())])); - } - let Some(manager) = &self.gateway_manager else { - let envelope = build_error( - &service, - "call_tool", - "unknown_tool", - "code schema is not enabled", - ); - return Ok(CallToolResult::error(vec![Content::text( - envelope.to_string(), - )])); - }; - let id = args - .get("id") - .and_then(Value::as_str) - .unwrap_or_default() - .to_string(); - let id_hash = hash_arguments(&Value::String(id.clone())); - tracing::info!( - surface = "mcp", - service = "code_schema", - action = "call_tool", - subject, - id_hash = %id_hash, - "gateway code schema start" - ); - let broker = CodeModeBroker::new(&self.registry, Some(manager)); - let caller = auth.map_or(CodeModeCaller::TrustedLocal, |auth| { - CodeModeCaller::Scoped { - scopes: auth.scopes.clone(), - subject: self.request_subject(&context).map(ToOwned::to_owned), - } - }); - return match broker - .schema(&id, caller, self.code_mode_surface(false)) - .await - { - Ok(response) => { - tracing::info!( - surface = "mcp", - service = "code_schema", - action = "call_tool", - subject, - id_hash = %id_hash, - elapsed_ms = started.elapsed().as_millis(), - "gateway code schema ok" - ); - Ok(CallToolResult::success(vec![Content::text( - serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string()), - )])) - } - Err(err) => { - let env = tool_error_envelope(&service, "call_tool", &err); - Ok(CallToolResult::error(vec![Content::text(env.to_string())])) - } - }; - } if service == CODE_EXECUTE_TOOL_NAME && self.gateway_code_mode_enabled().await { let started = Instant::now(); let subject = self.request_subject_log_tag(&context); @@ -3688,7 +3588,6 @@ mod tests { snapshot.tools, [ "code_execute".to_string(), - "code_schema".to_string(), "code_search".to_string(), "invoke".to_string(), "scout".to_string() @@ -3698,6 +3597,19 @@ mod tests { ); } + #[test] + fn code_execute_description_contains_protocol_contract() { + assert!(super::CODE_EXECUTE_DESCRIPTION.contains("callTool")); + assert!( + super::CODE_EXECUTE_DESCRIPTION + .contains("Successful return: the upstream tool's structuredContent") + ); + assert!(super::CODE_EXECUTE_DESCRIPTION.contains("JSON.parse(String(e.message))")); + assert!(super::CODE_EXECUTE_DESCRIPTION.contains("Retry-safe:")); + assert!(super::CODE_EXECUTE_DESCRIPTION.contains("Promise.all")); + assert!(super::CODE_EXECUTE_DESCRIPTION.len() < 8192); + } + fn gateway_test_upstream(name: &str) -> crate::config::UpstreamConfig { crate::config::UpstreamConfig { enabled: true, diff --git a/crates/lab/tests/code_mode_runner.rs b/crates/lab/tests/code_mode_runner.rs index ac8ab985..72d10e7a 100644 --- a/crates/lab/tests/code_mode_runner.rs +++ b/crates/lab/tests/code_mode_runner.rs @@ -1,9 +1,9 @@ -use std::io::{BufRead, BufReader, Write}; +use std::io::{BufRead, BufReader, Read, Write}; use std::process::{Command, Stdio}; use serde_json::{Value, json}; -fn read_protocol_line(reader: &mut BufReader) -> Value { +fn read_protocol_line(reader: &mut BufReader) -> Value { let mut line = String::new(); reader.read_line(&mut line).expect("read runner output"); assert!(!line.is_empty(), "runner closed stdout"); @@ -22,6 +22,7 @@ fn code_mode_runner_evaluates_js_in_a_minimal_host_environment() { let mut stdin = child.stdin.take().expect("runner stdin"); let stdout = child.stdout.take().expect("runner stdout"); + let mut stderr = child.stderr.take().expect("runner stderr"); let mut stdout = BufReader::new(stdout); let code = r#" if (typeof process !== "undefined" || typeof require !== "undefined" || @@ -29,6 +30,7 @@ fn code_mode_runner_evaluates_js_in_a_minimal_host_environment() { typeof Bun !== "undefined") { throw new Error("ambient host API exposed"); } + console.log("runner console check"); const first = await callTool("lab::gateway.first", {"x": 1}); if (first.ok) { await callTool("lab::gateway.second", {"from": first.value}); @@ -91,6 +93,11 @@ fn code_mode_runner_evaluates_js_in_a_minimal_host_environment() { assert_eq!(read_protocol_line(&mut stdout), json!({"type": "done"})); let status = child.wait().expect("wait for runner"); assert!(status.success(), "runner exited with {status}"); + let mut stderr_text = String::new(); + stderr + .read_to_string(&mut stderr_text) + .expect("read runner stderr"); + assert!(stderr_text.contains("runner console check")); } #[test] diff --git a/docs/dev/CODE_MODE.md b/docs/dev/CODE_MODE.md new file mode 100644 index 00000000..56e63d24 --- /dev/null +++ b/docs/dev/CODE_MODE.md @@ -0,0 +1,54 @@ +# Code Mode + +Code Mode exposes two MCP tools when gateway tool search is enabled: + +- `code_search` injects the current upstream MCP tool catalog as `const tools = [...]` + inside a constrained JavaScript search sandbox. Each catalog entry contains + `id`, `upstream`, `name`, `description`, and sanitized `schema`. +- `code_execute` runs JavaScript snippets that call `callTool(id, params)` for + upstream MCP tool IDs returned by `code_search`. + +Lab actions are intentionally not exposed through Code Mode. Use the normal +`invoke`/`tool_execute` surface for Lab service actions. + +## Catalog Budget + +The inline catalog has a 256KB soft cap and 512KB hard cap. Over the soft cap, +the catalog is stably pruned and a `__truncated__` sentinel entry is appended. +Over the hard cap, `code_search` returns `invalid_param` and callers should use +`scout` for RRF discovery. + +## Execute Response Budget + +`code_execute` returns a capped envelope. Defaults: + +- `max_response_bytes = 24576` +- `max_response_tokens = 6000` + +When the envelope is too large, oversized per-call results are replaced with a +truncation marker containing `truncated`, `original_size`, `original_tokens`, +`preview`, and `next_action`. + +## Runner Architecture + +The stdio parent-broker protocol is unchanged: + +1. Parent starts `labby internal code-mode-runner`. +2. Child emits `tool_call` lines for `callTool` requests. +3. Parent dispatches through the gateway broker and replies with `tool_result`. +4. Child emits `done` after all promises settle. + +With `code_mode_wasm` enabled, the child runner uses Javy/QuickJS for snippet +execution. `callTool` returns a real JavaScript promise, so `Promise.all` +fan-out emits independent tool calls before awaiting results. `console.log` and +`console.error` are routed to stderr, and the runner process starts with an +empty environment in a temporary directory with no Node, Deno, Bun, fetch, or +require globals. + +Without `code_mode_wasm`, the runner keeps the Boa fallback implementation for +development builds that do not include the Javy/Wasmtime dependencies. + +The same feature also initializes the Wasmtime engine skeleton with fuel and +epoch interruption enabled. Fuel and timeout traps are normalized to +`code_mode_fuel_exhausted` and `code_mode_timeout` so callers can recover +programmatically as the Wasmtime path grows. diff --git a/docs/dev/ERRORS.md b/docs/dev/ERRORS.md index a3e9d9ce..01bb1979 100644 --- a/docs/dev/ERRORS.md +++ b/docs/dev/ERRORS.md @@ -56,11 +56,12 @@ Dispatch layers may add the following kinds on top of SDK errors: - `unknown_instance` - `conflict` — resource already exists with the given identifier; HTTP 409 - `ambiguous_tool` — unqualified tool name resolved to multiple upstream gateway candidates; envelope carries `valid: Vec` of fully-qualified `{upstream}::{tool}` names the caller must choose from, plus a `hint` explaining that callers may either pass `name = "{upstream}::{tool}"` or set `upstream` separately. HTTP 409. -- `invalid_code_mode_id` — Code Mode tool id parsing failed. Valid ids are `lab::.` and `upstream::::`. HTTP 422. -- `code_mode_disabled` — Code Mode execution was requested while `[code_mode].enabled` is false. Discovery and schema lookup can remain enabled without allowing execution. HTTP 403. +- `invalid_code_mode_id` — Code Mode tool id parsing failed. Valid ids are `upstream::::` only; Lab actions use `tool_execute`/`invoke`. HTTP 422. +- `code_mode_disabled` — Code Mode execution was requested while `[code_mode].enabled` is false. Catalog search can remain enabled without allowing execution. HTTP 403. - `code_execution_failed` — Code Mode child-process JavaScript evaluation failed before completing the runner protocol. HTTP 422. - `tool_call_limit_exceeded` — a Code Mode snippet attempted more host-brokered tool calls than `max_tool_calls` allows. HTTP 429. -- `schema_unavailable` — Code Mode schema lookup found a tool, but its upstream schema was missing or exceeded the safe return size after sanitization. HTTP 422. +- `code_mode_fuel_exhausted` — the Wasmtime Code Mode runner consumed its configured fuel budget before completion. HTTP 408. +- `code_mode_timeout` — the Code Mode wall-clock backstop interrupted execution before completion. HTTP 408. - `queue_saturated` — bounded runtime queue is full; caller should retry after the current work drains. HTTP 429. ### Fleet-WS install hardening kinds (lab-zxx5.18) diff --git a/scripts/refresh-javy-plugin.sh b/scripts/refresh-javy-plugin.sh new file mode 100755 index 00000000..337355cc --- /dev/null +++ b/scripts/refresh-javy-plugin.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +version="${JAVY_VERSION:-v7.0.0}" +dest="${1:-crates/lab/src/dispatch/gateway/code_mode_wasm/plugin.wasm}" +base_url="https://github.com/bytecodealliance/javy/releases/download/${version}" +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + +mkdir -p "$(dirname "$dest")" +curl -fsSL "${base_url}/plugin.wasm.gz" -o "$tmpdir/plugin.wasm.gz" +curl -fsSL "${base_url}/plugin.wasm.gz.sha256" -o "$tmpdir/plugin.wasm.gz.sha256" + +( + cd "$tmpdir" + sha256sum -c plugin.wasm.gz.sha256 + gzip -dc plugin.wasm.gz > plugin.wasm +) + +cp "$tmpdir/plugin.wasm" "$dest" +sha256sum "$dest" From 6329be87553ecc8f90787bb44c7670d3076886c3 Mon Sep 17 00:00:00 2001 From: Jacob Magar Date: Mon, 25 May 2026 04:41:14 -0400 Subject: [PATCH 4/5] fix(code-mode): satisfy epic ci --- Cargo.lock | 226 ++++++++++++++++------------- Cargo.toml | 2 +- config/Dockerfile | 6 + crates/lab/Cargo.toml | 2 +- deny.toml | 11 +- docs/generated/cli-help.md | 39 +---- docs/generated/feature-matrix.json | 16 +- docs/generated/feature-matrix.md | 3 +- 8 files changed, 164 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27fe7933..96f297ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.25.1" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" dependencies = [ "gimli", ] @@ -1013,6 +1013,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpubits" version = "0.1.1" @@ -1039,46 +1048,48 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a04121a197fde2fe896f8e7cac9812fc41ed6ee9c63e1906090f9f497845f6" +checksum = "008f1a8d1da5074ad858f398775a6d1989031892e46927df5ed18d3be1ed8717" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a09e699a94f477303820fb2167024f091543d6240783a2d3b01a3f21c42bc744" +checksum = "9fd76237df1f4e26edb5ad7971d20280ed1e193331fd257f1b4e4dfefd88dda2" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07732c662a9755529e332d86f8c5842171f6e98ba4d5976a178043dad838654" +checksum = "380f0bc43e535df6855bbee649efb00bde39c3f33434c47c8e10ac836d21bf47" dependencies = [ "cranelift-entity", + "wasmtime-internal-core", ] [[package]] name = "cranelift-bitset" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18391da761cf362a06def7a7cf11474d79e55801dd34c2e9ba105b33dc0aef88" +checksum = "4811e3e4502de04257e90c0a93225b56d9b85e0f9ad10b81446b415511009610" dependencies = [ "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3a09b3042c69810d255aef59ddc3b3e4c0644d1d90ecfd6e3837798cc88a3c" +checksum = "82ffadb34d497f3e76fb3b4baf764c24ba8a51512976a1b77f78bdbf8f4aa687" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1090,7 +1101,8 @@ dependencies = [ "cranelift-entity", "cranelift-isle", "gimli", - "hashbrown 0.15.5", + "hashbrown 0.16.1", + "libm", "log", "pulley-interpreter", "regalloc2", @@ -1098,14 +1110,14 @@ dependencies = [ "serde", "smallvec", "target-lexicon", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen-meta" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75817926ec812241889208d1b190cadb7fedded4592a4bb01b8524babb9e4849" +checksum = "be4f6992eb6faf086ddc7deaaa5f279abfe7f5fd5ae5709bd38253450fc7b945" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -1116,35 +1128,36 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859158f87a59476476eda3884d883c32e08a143cf3d315095533b362a3250a63" +checksum = "70e1b2aad7d055925a4ea9cdbfa9d1d987f9dfc8ad6b708be28f901ac620a298" [[package]] name = "cranelift-control" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b65a9aec442d715cbf54d14548b8f395476c09cef7abe03e104a378291ab88" +checksum = "89a355348325e0a63b65c00def3871597b9fcc79d25456397010d16d872b3772" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334c99a7e86060c24028732efd23bac84585770dcb752329c69f135d64f2fc1" +checksum = "43f4847d93ce2c80d2bff929aa1004dfb3ce2cf5d881f6ced54b8d654d967ba3" dependencies = [ "cranelift-bitset", "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-frontend" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43ac6c095aa5b3e845d7ca3461e67e2b65249eb5401477a5ff9100369b745111" +checksum = "ba24e5fe5242cc445e7892ef0a51a4351cf716e3a04ac7a3a05820d056c39818" dependencies = [ "cranelift-codegen", "log", @@ -1154,15 +1167,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d3d992870ed4f0f2e82e2175275cb3a123a46e9660c6558c46417b822c91fa" +checksum = "89bc2035de85c4f04ba7bd57eb5bd3a8b775235bf28852dbf87105115cb8919a" [[package]] name = "cranelift-native" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee32e36beaf80f309edb535274cfe0349e1c5cf5799ba2d9f42e828285c6b52e" +checksum = "5ea6630c16921ab087792750f239d0c0173411e80179ca7c0ce0710ce9e7646a" dependencies = [ "cranelift-codegen", "libc", @@ -1171,9 +1184,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.128.4" +version = "0.131.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9" +checksum = "faa4bbad54fc28cc0da1f9a5d7f7f826ec8cafda3d503b401b2daaaa93c63ef0" [[package]] name = "crc32fast" @@ -2129,11 +2142,12 @@ dependencies = [ [[package]] name = "gimli" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ - "fallible-iterator", + "fnv", + "hashbrown 0.16.1", "indexmap 2.14.0", "stable_deref_trait", ] @@ -2206,7 +2220,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash 0.1.5", - "serde", ] [[package]] @@ -2218,6 +2231,8 @@ dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -2225,6 +2240,9 @@ name = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -3474,12 +3492,12 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", - "hashbrown 0.15.5", + "hashbrown 0.17.0", "indexmap 2.14.0", "memchr", ] @@ -4030,21 +4048,21 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "41.0.4" +version = "44.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9812652c1feb63cf39f8780cecac154a32b22b3665806c733cd4072547233a4" +checksum = "dff0ead8b4616f81b3d3efd41ce41bcf9ea364a5d8df8be8a8a1f98b50104349" dependencies = [ "cranelift-bitset", "log", "pulley-macros", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "pulley-macros" -version = "41.0.4" +version = "44.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56000349b6896e3d44286eb9c330891237f40b27fd43c1ccc84547d0b463cb40" +checksum = "f4389e5820b1b39810ac12a27aa665320cab3caa51913a79637c06f284cfe223" dependencies = [ "proc-macro2", "quote", @@ -4313,13 +4331,13 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.13.5" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.15.5", + "hashbrown 0.17.0", "log", "rustc-hash", "smallvec", @@ -4747,6 +4765,12 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -6394,22 +6418,22 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.243.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", - "wasmparser 0.243.0", + "wasmparser 0.244.0", ] [[package]] name = "wasm-encoder" -version = "0.244.0" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" dependencies = [ "leb128fmt", - "wasmparser 0.244.0", + "wasmparser 0.246.2", ] [[package]] @@ -6449,27 +6473,27 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.243.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", "indexmap 2.14.0", "semver 1.0.28", - "serde", ] [[package]] name = "wasmparser" -version = "0.244.0" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" dependencies = [ "bitflags", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "indexmap 2.14.0", "semver 1.0.28", + "serde", ] [[package]] @@ -6485,30 +6509,27 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.243.0" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b" +checksum = "6e41f7493ba994b8a779430a4c25ff550fd5a40d291693af43a6ef48688f00e3" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.243.0", + "wasmparser 0.246.2", ] [[package]] name = "wasmtime" -version = "41.0.4" +version = "44.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2a83182bf04af87571b4c642300479501684f26bab5597f68f68cded5b098fd" +checksum = "af4eccc0728f061979efa8ff4c962cff7041fead4baadb74973f01b9c47158a4" dependencies = [ "addr2line", - "anyhow", "async-trait", "bitflags", "bumpalo", "cc", "cfg-if", - "hashbrown 0.15.5", - "indexmap 2.14.0", "libc", "log", "mach2", @@ -6522,14 +6543,13 @@ dependencies = [ "serde_derive", "smallvec", "target-lexicon", - "wasmparser 0.243.0", + "wasmparser 0.246.2", "wasmtime-environ", + "wasmtime-internal-core", "wasmtime-internal-cranelift", "wasmtime-internal-fiber", "wasmtime-internal-jit-debug", "wasmtime-internal-jit-icache-coherence", - "wasmtime-internal-math", - "wasmtime-internal-slab", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", "wat", @@ -6538,32 +6558,49 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "41.0.4" +version = "44.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb201c41aa23a3642365cfb2e4a183573d85127a3c9d528f56b9997c984541ab" +checksum = "7e84dbe3208c1336a41546beb75927b3b37e2e4fce06653d214b407136fbe295" dependencies = [ "anyhow", + "cpp_demangle", + "cranelift-bforest", "cranelift-bitset", "cranelift-entity", "gimli", + "hashbrown 0.16.1", "indexmap 2.14.0", "log", "object", "postcard", + "rustc-demangle", "serde", "serde_derive", + "sha2 0.10.9", "smallvec", "target-lexicon", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.246.2", + "wasmparser 0.246.2", "wasmprinter", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-core" +version = "44.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4364d345719bba7fc4c435992ea1cb0c118f1e90a88c6e6f22a7a4fc507700c6" +dependencies = [ + "hashbrown 0.16.1", + "libm", + "serde", ] [[package]] name = "wasmtime-internal-cranelift" -version = "41.0.4" +version = "44.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633e889cdae76829738db0114ab3b02fce51ea4a1cd9675a67a65fce92e8b418" +checksum = "c5a3bc28a172037c7864128bb208017a02bba659a59c27acacc048c09e25c1fc" dependencies = [ "cfg-if", "cranelift-codegen", @@ -6579,18 +6616,18 @@ dependencies = [ "smallvec", "target-lexicon", "thiserror 2.0.18", - "wasmparser 0.243.0", + "wasmparser 0.246.2", "wasmtime-environ", - "wasmtime-internal-math", + "wasmtime-internal-core", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", ] [[package]] name = "wasmtime-internal-fiber" -version = "41.0.4" +version = "44.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb126adc5d0c72695cfb77260b357f1b81705a0f8fa30b3944e7c2219c17341" +checksum = "3c90a899a47d3da6e384e7b4cad61fdcb27535a395742b32440bdf9980ea83fa" dependencies = [ "cc", "cfg-if", @@ -6603,9 +6640,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "41.0.4" +version = "44.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e66ff7f90a8002187691ff6237ffd09f954a0ebb9de8b2ff7f5c62632134120" +checksum = "84f364747aa74c686b18925918e5cfd615a73c9613c7a31fc1cd86f42df12fbe" dependencies = [ "cc", "wasmtime-internal-versioned-export-macros", @@ -6613,36 +6650,21 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "41.0.4" +version = "44.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b96df23179ae16d54fb3a420f84ffe4383ec9dd06fad3e5bc782f85f66e8e08" +checksum = "c3ba98c1492f530833e0d3cc17dbb0c3c57c9f1bb3b078ae44bb55a233e43eba" dependencies = [ - "anyhow", "cfg-if", "libc", + "wasmtime-internal-core", "windows-sys 0.61.2", ] -[[package]] -name = "wasmtime-internal-math" -version = "41.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d1380926682b44c383e9a67f47e7a95e60c6d3fa8c072294dab2c7de6168a0" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmtime-internal-slab" -version = "41.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b63cbea1c0192c7feb7c0dfb35f47166988a3742f29f46b585ef57246c65764" - [[package]] name = "wasmtime-internal-unwinder" -version = "41.0.4" +version = "44.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25c392c7e5fb891a7416e3c34cfbd148849271e8c58744fda875dde4bec4d6a" +checksum = "94b8f8a89e8f3660646f820c7d8310a67094156bb866e9d56f1b00892e011206" dependencies = [ "cfg-if", "cranelift-codegen", @@ -6653,9 +6675,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "41.0.4" +version = "44.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f8b9796a3f0451a7b702508b303d654de640271ac80287176de222f187a237" +checksum = "7a12754f1ffc4a3300d56d324c418b8b32cf029606618da22c7d076213882a3f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index db5d58e9..f26fdfbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ agent-client-protocol = { path = "crates/vendor/agent-client-protocol" } [workspace.package] version = "0.17.4" edition = "2024" -rust-version = "1.90" +rust-version = "1.92" license = "MIT OR Apache-2.0" repository = "https://github.com/jmagar/lab" authors = ["jmagar"] diff --git a/config/Dockerfile b/config/Dockerfile index cb28ecaf..ee1ef2f7 100644 --- a/config/Dockerfile +++ b/config/Dockerfile @@ -4,6 +4,12 @@ FROM rust:1-slim AS builder WORKDIR /build +# GitHub's container builder has less memory than the release-smoke runners. +# Keep the container release binary optimized but avoid ThinLTO/codegen-units=1 +# peak memory during the final link. +ENV CARGO_PROFILE_RELEASE_LTO=false \ + CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 + # System build deps: # pkg-config — lets -sys crates find system headers # build-essential — C compiler for rusqlite's bundled SQLite + other -sys crates diff --git a/crates/lab/Cargo.toml b/crates/lab/Cargo.toml index 2c2a0c3e..d34e2511 100644 --- a/crates/lab/Cargo.toml +++ b/crates/lab/Cargo.toml @@ -44,7 +44,7 @@ thiserror.workspace = true jiff.workspace = true boa_engine = "0.21.1" javy = { version = "7.0.0", optional = true } -wasmtime = { version = "41.0.4", optional = true, default-features = false, features = [ +wasmtime = { version = "44.0.1", optional = true, default-features = false, features = [ "cranelift", "pooling-allocator", "runtime", diff --git a/deny.toml b/deny.toml index 99cb2ad3..6901ae72 100644 --- a/deny.toml +++ b/deny.toml @@ -25,6 +25,7 @@ ignore = [ allow = [ "MIT", "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", "BSD-3-Clause", "ISC", @@ -64,12 +65,18 @@ reason = "Binary-only: lab-apis must stay pure. MCP server lives in `labby`." [[bans.deny]] name = "anyhow" -wrappers = ["labby", "agent-client-protocol", "agent-client-protocol-schema"] +wrappers = [ + "labby", + "agent-client-protocol", + "agent-client-protocol-schema", + "javy", + "wasmprinter", + "wasmtime-environ", +] reason = "lab-apis uses `thiserror`. anyhow is for the binary only." [[bans.deny]] name = "tabled" -wrappers = ["labby"] reason = "Binary-only: table formatting happens in `lab/src/output.rs`, not lab-apis." [sources] diff --git a/docs/generated/cli-help.md b/docs/generated/cli-help.md index 30e28232..3c4a107c 100644 --- a/docs/generated/cli-help.md +++ b/docs/generated/cli-help.md @@ -2075,8 +2075,7 @@ Search, inspect, and execute Code Mode snippets through dispatch Usage: code [OPTIONS] Commands: - search Search Code Mode tool IDs by natural-language query - schema Show the schema and generated bindings for one Code Mode tool ID + search Filter the inlined Code Mode tool catalog with JavaScript exec Execute a sandboxed JavaScript snippet that calls callTool(id, params) Options: @@ -2096,51 +2095,25 @@ Options: ## `labby gateway code search` ```text -Search Code Mode tool IDs by natural-language query +Filter the inlined Code Mode tool catalog with JavaScript -Usage: search [OPTIONS] +Usage: search [OPTIONS] -Arguments: - +Options: + --code -Options: --json Emit JSON instead of human-readable tables - --top-k - [default: 10] - --color Control human-readable CLI styling [default: auto] [possible values: auto, plain, color] - -h, --help - Print help -``` - -## `labby gateway code schema` - -```text -Show the schema and generated bindings for one Code Mode tool ID - -Usage: schema [OPTIONS] - -Arguments: - - - -Options: - --json - Emit JSON instead of human-readable tables - - --color - Control human-readable CLI styling + --file - [default: auto] - [possible values: auto, plain, color] -h, --help Print help diff --git a/docs/generated/feature-matrix.json b/docs/generated/feature-matrix.json index beaa51b5..5cd76479 100644 --- a/docs/generated/feature-matrix.json +++ b/docs/generated/feature-matrix.json @@ -104,7 +104,8 @@ "extract", "mcpregistry", "gateway", - "marketplace" + "marketplace", + "code_mode_wasm" ], "included_in_default": true, "included_in_all": true, @@ -112,6 +113,19 @@ "mapped_crate_feature": "lab-apis/all", "exception_reason": "aggregate/default feature" }, + { + "crate_name": "labby", + "feature": "code_mode_wasm", + "dependencies": [ + "dep:javy", + "dep:wasmtime" + ], + "included_in_default": true, + "included_in_all": true, + "classification": "helper_internal", + "mapped_crate_feature": null, + "exception_reason": "helper/internal feature" + }, { "crate_name": "labby", "feature": "default", diff --git a/docs/generated/feature-matrix.md b/docs/generated/feature-matrix.md index aa2dd398..b76a4ea0 100644 --- a/docs/generated/feature-matrix.md +++ b/docs/generated/feature-matrix.md @@ -14,7 +14,8 @@ Feature invariant status: clean. | lab-apis | `mcpregistry` | ServicePassthrough | false | true | labby/mcpregistry | | | lab-apis | `test-utils` | HelperInternal | false | false | labby/test-utils | | | labby | `acp_registry` | ServicePassthrough | true | true | lab-apis/acp_registry | `lab-apis/acp_registry` | -| labby | `all` | AggregateDefault | true | true | lab-apis/all | `lab-apis/all`
`lab-admin`
`acp_registry`
`deploy`
`extract`
`mcpregistry`
`gateway`
`marketplace` | +| labby | `all` | AggregateDefault | true | true | lab-apis/all | `lab-apis/all`
`lab-admin`
`acp_registry`
`deploy`
`extract`
`mcpregistry`
`gateway`
`marketplace`
`code_mode_wasm` | +| labby | `code_mode_wasm` | HelperInternal | true | true | - | `dep:javy`
`dep:wasmtime` | | labby | `default` | AggregateDefault | false | false | lab-apis/default | `all` | | labby | `deploy` | ServicePassthrough | true | true | lab-apis/deploy | `lab-apis/deploy`
`dep:regex` | | labby | `extract` | ServicePassthrough | true | true | lab-apis/extract | `lab-apis/extract` | From ac5a3740b11d23bc41dfc50dab5b334123c92b40 Mon Sep 17 00:00:00 2001 From: Jacob Magar Date: Mon, 25 May 2026 04:49:45 -0400 Subject: [PATCH 5/5] fix(docker): install libclang for code mode build --- config/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/Dockerfile b/config/Dockerfile index ee1ef2f7..e41dba04 100644 --- a/config/Dockerfile +++ b/config/Dockerfile @@ -1,5 +1,5 @@ # ─── Stage 1: Build ─────────────────────────────────────────────────────────── -# Requires Rust 1.90+ (Cargo.toml: rust-version = "1.90", edition = "2024") +# Requires Rust 1.92+ (Cargo.toml: rust-version = "1.92", edition = "2024") FROM rust:1-slim AS builder WORKDIR /build @@ -14,11 +14,13 @@ ENV CARGO_PROFILE_RELEASE_LTO=false \ # pkg-config — lets -sys crates find system headers # build-essential — C compiler for rusqlite's bundled SQLite + other -sys crates # libssl-dev — headers/pkg-config metadata for crates pulling openssl-sys +# libclang-dev — libclang for rquickjs-sys bindgen during Code Mode builds RUN apt-get update \ && apt-get install -y --no-install-recommends \ pkg-config \ build-essential \ libssl-dev \ + libclang-dev \ && rm -rf /var/lib/apt/lists/* # ── Dependency-caching layer ──────────────────────────────────────────────────