feat(agents): custom ACP agent support - #2433
Merged
Merged
Conversation
Widen the agent type enum to include 'custom', add a nullable custom_config JSON column to acp_agents (named migration 0007) plus the bootstrap DDL and migration-history mirror, and round-trip a validated CustomAcpAgentConfig through the agent definition and store (with a new update method).
Add a quote-aware command splitter and a launcher branch that runs a custom agent's full command line as given (no bundled-bun wrapping). Make the agent policy data-driven for custom agents: a per-agent registry id, stored env, system-prompt append, and full-access modes. Skip the full-access mode step when none are configured, and read the reasoning-effort key from config.
Include 'custom' in the conversation target-type enum and dispatch custom chat requests to processAcpMessage alongside claude and codex.
Extend POST /agents to accept and cross-validate customConfig (required for custom agents, rejected for built-ins), and add PUT /agents/:agentId so a custom agent's command can be edited. Refresh running sessions on update so the next turn re-spawns with the new command.
Accept an optional command/env/cwd on POST /acpx/probe (command required for type 'custom') so the settings UI can validate a not-yet-saved agent and enumerate its models before the user commits.
Add a command-splitter suite and custom-agent cases across the store (config round-trip + update through the real migration), agent routes (create cross-validation + PUT), launcher (run-as-given argv, env injection, no bundled-bun wrapping), policy (per-agent registry id, stored env/prompt/modes, empty full-access), and probe (command required, forwarded).
Add customConfig to the app AcpAgent type, extend the create payload, add useUpdateAcpAgent (PUT), and a useProbeCustomAgent mutation that probes a not-yet-saved custom command for the Test-connection flow.
Add the add/edit custom-agent dialog (name, full command line, working dir, Test-connection probe, advanced env/modes/prompt/icon), a nested Popular ACP agents helper backed by a verified registry (opencode, hermes, openclaw, pi; antigravity as docs-only), and the dashed 'Custom ACP agent' template tile.
Wire the custom-agent dialog into the AI settings pane (template tile + edit from the agents list), render a custom agent's stored icon and command in its card, and give custom agents a distinct icon/label fallback in the adapter helpers and the chat provider selector.
Add unit tests for the env/CSV parsing and config builder, and card cases for the custom-agent command, icon, and edit button.
Contributor
Greptile SummaryThe PR adds configurable custom ACP agents across persistence, API routes, runtime policy, process launching, probing, and the AI settings UI.
Confidence Score: 5/5The PR appears safe to merge because the previously reported Windows command-path failure is fixed and no blocking failure remains. No blocking failure remains. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
UI[Custom agent settings] --> Probe[POST /acpx/probe]
UI --> Agents[POST or PUT /agents]
Agents --> Store[(ACP agent store)]
Store --> Runtime[ACP runtime policy]
Probe --> Parser[Command-line parser]
Runtime --> Parser
Parser --> Launcher[Process launcher]
Launcher --> ACP[Custom ACP agent over stdio]
Reviews (2): Last reviewed commit: "fix(server): keep Windows backslash path..." | Re-trigger Greptile |
Contributor
✅ Tests passed: 2504/2507Ran 14 of 15 suites (1 not affected by this change).
|
Treat backslashes as literal in the custom-agent command splitter so Windows paths like C:\Users\me\agent.exe survive; use quotes for arguments with spaces. Previously every backslash was consumed as an escape, launching a nonexistent executable path on Windows.
Contributor
Author
Render svgl brand marks for opencode (currentColor), OpenClaw, and Antigravity in neutral tiles, with monograms for pi and Hermes (no svgl logo). Show each suggested command as a proper code chip so it reads as code, not description.
A GUI-launched server inherits a minimal PATH that omits shell-profile additions (homebrew, nvm/fnm, ~/.local/bin), so a custom agent's binary could be unfindable even when it works in a terminal. Ask the user's login shell for its PATH once (cached, timeout-guarded) and prepend it for custom command spawns. Windows inherits the full registry PATH already, so it is unaffected.
Consolidate agent brand marks (Claude, Codex, opencode, OpenClaw, Antigravity, pi) into a shared module reusing the project's svg components, and use them in the provider templates, agent cards, and the Popular-agents picker. Picking a popular agent now carries its logo onto the saved agent. Removed the manual Icon text field. Hermes keeps a monogram until its logo asset is added.
…ode color Render brand marks in the newtab + sidepanel provider selector by threading a brand key through the chat targets. Add the Hermes icon (object-contain, from the provided asset) and give opencode its own dark/white ink instead of the tile accent color.
…eader The newtab search-bar pill and input avatar and the sidepanel header rendered a generic Bot for the selected agent. Resolve the agent's brand mark from its brand key in all three, falling back to Bot when none.
The header agent selector read as a static title once it showed a brand logo. Add a border, an active (foreground) name, and a rotating chevron caret so it reads as a dropdown, matching the newtab pill.
Dani Akash (DaniAkash)
added a commit
that referenced
this pull request
Aug 27, 2026
* feat(mcp): upgrade both MCP servers to the 2026-07-28 revision (#2290) * feat(server): migrate the MCP server to the v2 SDK with dual-era 2026-07-28 support (#2272) * feat(server): migrate the MCP server to the v2 SDK with dual-era 2026-07-28 support Replace @hono/mcp and the server-side @modelcontextprotocol/sdk 1.x with @modelcontextprotocol/server and @modelcontextprotocol/hono 2.0, serving both the legacy initialize handshake and the modern 2026-07-28 protocol per request via a stateless WebStandardStreamableHTTPServerTransport. v2 derives tools/list JSON schemas only from Zod v4, so the local browser and filesystem tool schemas move to the zod/v4 subpath of the installed zod 3.25; the Klavis connector schemas are bridged through fromJsonSchema until the Strata client migration. Drop the logging capability and the SetLevel handler, both removed in 2026-07-28. The Strata MCP client stays on the 1.x SDK for a follow-up change. Refs #2168 * fix(ci): call the real tabs tool in the MCP integration test The test invoked a non-existent tool name and passed only because the 1.x server returned a lenient error result for unknown tools. The v2 server correctly rejects unknown tools (its sibling test asserts the throw), so call the registered tabs tool with a valid action instead. * refactor(server): migrate the Strata MCP client to the v2 client SDK (#2274) Move the Klavis Strata MCP client and the remaining Klavis type imports from the 1.x @modelcontextprotocol/sdk to @modelcontextprotocol/client 2.0, so apps/server no longer depends on the legacy SDK at runtime. The SDK stays as a devDependency for the integration and end-to-end tests that simulate a legacy client against the v2 server. No behavior change; the connector schema bridge is unchanged and can be cleaned up in a follow-up. Refs #2168 * chore(claw-server): upgrade rmcp to 3.x with a legacy protocol version pin (#2275) * chore(claw-server): upgrade rmcp to 3.x with a legacy protocol version pin Bump rmcp from 2.1 to 3.x. rmcp 3.0 changes the ServerHandler call_tool return type to the MRTR-aware CallToolResponse, so wrap the existing CallToolResult with .into() in the production and reference handlers and the serving test. rmcp 3.x advertises the modern 2026-07-28 protocol by default, but the claw-server's sessionless state model for that revision is not in place yet, so override supported_protocol_versions() on the production handler to pin to legacy versions (2025-11-25 and earlier) and keep this upgrade behavior-neutral. The pin is lifted together with the handle-based session model. All crates compile, cargo test passes, clippy and fmt clean. Refs #2168 * test(claw-server): prove the legacy protocol pin rejects the modern revision Add a serving-level test that initializes against a legacy-pinned handler with protocolVersion 2026-07-28 and asserts the server negotiates down to a legacy revision rather than agreeing to the modern one. This exercises the same supported_protocol_versions() override the claw-server uses and confirms rmcp consults it during initialize negotiation. * feat(claw-server): serve the modern MCP revision with a session handle (#2278) * feat(claw-server): serve the modern MCP revision with a session handle Advertise the 2026-07-28 revision alongside the legacy revisions. rmcp serves 2026-07-28 clients statelessly, so the per-agent tab-group session, previously keyed on the mcp-session-id header that revision removes, is now carried as a server-minted handle passed as an ordinary `session` tool argument: minted as a UUID when absent, returned in structuredContent, and looked up on later calls so the agent keeps its tab group. Legacy clients and stdio keep the existing session model unchanged, and idle sweeping reaps handle sessions independent of transport close. * test(claw-server): expect the injected session arg in the name_session golden schema * fix(claw-server): only reuse live server-minted session handles Resolve a modern session by reusing a handle only when it maps to a live session; any absent or unrecognized handle now mints a fresh server-generated handle instead of being minted under the caller-supplied value. A caller can no longer choose or seed a session id, so it cannot land on another agent's ownership by presenting a chosen id, and because every mint uses a fresh unique id, two concurrent calls can no longer both mint under the same id and orphan one session. Continuity flows through the returned handle. * feat(server): give /mcp clients a server-minted session identity handle (#2289) * feat(server): give /mcp clients a server-minted session identity handle Expose an optional session string argument on each /mcp browser tool. The server mints a UUID when the caller omits it and returns it in the result's structuredContent so an agent can thread it back on later calls to identify its own session; a supplied handle is echoed. The handle is stripped before the tool runs, so tool logic (including the one strict-schema tool) and the internal agent's shared tool schemas stay unchanged. It is gated behind an opt-in registration flag so only the /mcp surface carries it, and it is attributed in the per-tool execution metric. Identity only: no isolation, no per-session state, no change to browser behavior. * fix(server): attribute the /mcp session handle on the per-call log The session handle was added to the aggregated `tool_executed` metric event, whose rollup keeps only tool name, source, and success, so the handle was discarded. Move the attribution to the per-call log context instead, which is the right home for a per-session value and avoids the unbounded cardinality of a UUID in an aggregated metric. The returned handle in structuredContent is unchanged. * fix(server): serve the 2026-07-28 revision on /mcp and harden the endpoint (#2298) * fix(server): deliver the /mcp tool abort signal from the request context The browser tool wrapper read the abort signal from a top-level `extra.signal`, which does not exist on the v2 SDK's tool handler context, so it was always undefined and cancellation never reached the tool over /mcp. Read it from `extra.mcpReq.signal` where the SDK actually places it. The internal AI-SDK agent path threads its own signal and is unaffected. Adds a test that a pre-aborted signal delivered via mcpReq.signal makes a waiting tool abort. * feat(server): reject browser-originated /mcp requests via Sec-Fetch-Site The /mcp endpoint is intentionally reachable across the LAN, so a loopback bind is not an option. Browsers always send a Sec-Fetch-Site header while native MCP clients and the internal ACP client never do, so rejecting requests that carry it filters browser-originated attacks (DNS rebinding, CSRF) without restricting the bind address. Scoped to /mcp so browser-facing routes are unaffected, and mirrors the claw-server's request hygiene. * fix(server): serve the modern 2026-07-28 revision on the TS /mcp endpoint The TS agent server advertised only legacy protocol versions and rejected 2026-07-28: the McpServer was built without supportedProtocolVersions (so it never registered server/discover), and the route dispatched through the bare streamable-HTTP transport, which does not run the modern request machinery. Advertise the modern revision alongside the legacy list on the McpServer, and split the route: legacy (2025-era) requests keep the existing hand-wired transport with enableJsonResponse so the internal ACP client still gets single-JSON responses, while modern requests go through createMcpHandler, which serves server/discover and the stateless 2026-07-28 dispatch. Both eras are exercised end to end by new tests: legacy initialize negotiates 2025-11-25 as JSON, modern server/discover advertises 2026-07-28, and a modern tool call succeeds. * feat(mcp-manager): add read-only tool catalogue endpoint for settings UI (#2410) The settings Available Tools list previously ran a full MCP handshake against /mcp from the browser. With the browser-request guard on /mcp, that path is no longer reachable from the extension. Add a GET /mcp-manager/tools endpoint that returns the same tool set (browser tools plus Klavis connector tools) as name and description pairs, and point the settings section at it through a react-query hook mirroring the existing agents hook. The MCP hardening is left unchanged. * fix(klavis): pass connector tool JSON schemas through unchanged (#2412) * fix(klavis): pass connector tool JSON schemas through unchanged Strata connector tools (Linear and others) advertised an empty input schema, so every parameterized call was rejected with both "required" and "additional properties" errors and the agent could never pass arguments. Their remote JSON schema was routed through zod-from-json-schema and then zod-to-json-schema, which under zod v3 silently drops every property. Pass the remote JSON schema straight through instead: fromJsonSchema for the MCP server path and the ai jsonSchema() helper for the AI SDK tool set. The hand-built connector_mcp_servers schema is unchanged. Removes the now-unused per-session schema map. * chore(server): drop unused zod-from-json-schema dependency * fix(claw-app): drop the audit timeline risk labels and recolor the rows (#2416) The audit timeline tagged act/evaluate/run/download rows as "High risk", which is inaccurate, and highlighted them in warning-amber. Remove the label and switch the row emphasis to the theme accent (blue); errors keep their red highlight. The same tool set still auto-expands on load, now named for what it is (notable actions) rather than risk. * chore(release): update extension alpha feeds to 0.2.17.0 Automated release snapshot update. * chore: bump browserclaw extension version to 0.2.17.0 (#2421) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(claw-server): install the neo skill under browseros-neo and migrate legacy dirs (#2428) * fix(claw-server): install the neo skill under browseros-neo and migrate legacy dirs The managed skill's SKILL.md declares name: browseros-neo but was installed into a directory named browserclaw, so agents that require name == parent directory reject it (e.g. Claude Code: name "browseros-neo" does not match parent directory "browserclaw"). Install the skill under browseros-neo so the directory matches the frontmatter, and add a one-time, idempotent migration that removes the legacy browserclaw directories (only ones whose ownership marker proves BrowserOS installed them) across every harness and purges their manifest records, so the reconcile replants them under the new name. User-authored browserclaw directories are left untouched. The migration runs on every managed-skill reconcile (startup and connect), so existing users heal without any action. Marker internals (managed_by) stay stable for recognition. * fix(harness): keep legacy skill records for directories that remain Purge a legacy manifest record only once its directory is actually gone. If removal or marker inspection fails the directory stays on disk, so keeping its record leaves the manifest consistent and lets the next reconcile retry, instead of orphaning it. * fix(chat): surface the full error in a copyable block (#2434) * fix(chat): surface the full scrubbed upstream error in a copyable block When a turn fails, the classifier only forwarded the short constructed message into the error envelope's details and capped it at 500 characters, so the gateway's real reason (OpenRouter-style gateways hide it in the response body / metadata.raw) never reached the user. Errors the AI SDK could not classify carried no details at all. Forward the full upstream response body (or structured data.raw), pretty-printed as JSON, into details on every path, redacted and bounded at a larger cap so the whole body survives to be copied. The card now renders that detail always expanded under a Full error heading with a copy control that copies the entire error even when the block clips it on screen, while the classified title and message stay on top. The block is omitted when the detail only repeats the message, so a clean one-line error is not shown twice. * fix(chat): broaden secret scrubbing for the full upstream error body Forwarding the whole response body means more credential shapes can appear than the four key patterns caught. Redact value-shaped secrets on the raw text so it works whether or not the body parses as an object: JWTs, PEM private keys, Google/Slack/GitLab/GitHub tokens, URL userinfo credentials (host kept), and sensitive JSON string values matched by key name. Skewed toward over-redaction since a missed token is copied into a bug report. * feat(agents): custom ACP agent support (#2433) * feat(server): add custom ACP agent data model + migration Widen the agent type enum to include 'custom', add a nullable custom_config JSON column to acp_agents (named migration 0007) plus the bootstrap DDL and migration-history mirror, and round-trip a validated CustomAcpAgentConfig through the agent definition and store (with a new update method). * feat(server): launch custom ACP agents from a stored command Add a quote-aware command splitter and a launcher branch that runs a custom agent's full command line as given (no bundled-bun wrapping). Make the agent policy data-driven for custom agents: a per-agent registry id, stored env, system-prompt append, and full-access modes. Skip the full-access mode step when none are configured, and read the reasoning-effort key from config. * feat(server): route custom agent chats through the ACP path Include 'custom' in the conversation target-type enum and dispatch custom chat requests to processAcpMessage alongside claude and codex. * feat(server): validate custom agent config and add an update endpoint Extend POST /agents to accept and cross-validate customConfig (required for custom agents, rejected for built-ins), and add PUT /agents/:agentId so a custom agent's command can be edited. Refresh running sessions on update so the next turn re-spawns with the new command. * feat(server): probe a custom agent by explicit command Accept an optional command/env/cwd on POST /acpx/probe (command required for type 'custom') so the settings UI can validate a not-yet-saved agent and enumerate its models before the user commits. * test(server): cover custom ACP agent config, launch, routes, and probe Add a command-splitter suite and custom-agent cases across the store (config round-trip + update through the real migration), agent routes (create cross-validation + PUT), launcher (run-as-given argv, env injection, no bundled-bun wrapping), policy (per-agent registry id, stored env/prompt/modes, empty full-access), and probe (command required, forwarded). * feat(app): custom agent data layer (config type, update + probe hooks) Add customConfig to the app AcpAgent type, extend the create payload, add useUpdateAcpAgent (PUT), and a useProbeCustomAgent mutation that probes a not-yet-saved custom command for the Test-connection flow. * feat(app): custom ACP agent dialog with Test connection + popular agents Add the add/edit custom-agent dialog (name, full command line, working dir, Test-connection probe, advanced env/modes/prompt/icon), a nested Popular ACP agents helper backed by a verified registry (opencode, hermes, openclaw, pi; antigravity as docs-only), and the dashed 'Custom ACP agent' template tile. * feat(app): surface custom agents in settings and chat Wire the custom-agent dialog into the AI settings pane (template tile + edit from the agents list), render a custom agent's stored icon and command in its card, and give custom agents a distinct icon/label fallback in the adapter helpers and the chat provider selector. * test(app): cover custom agent form helpers and card rendering Add unit tests for the env/CSV parsing and config builder, and card cases for the custom-agent command, icon, and edit button. * test(server): use a neutral path in the command-splitter fixture * fix(ci): expect the custom-agent migration in the bootstrap schema test * fix(server): keep Windows backslash paths intact when splitting commands Treat backslashes as literal in the custom-agent command splitter so Windows paths like C:\Users\me\agent.exe survive; use quotes for arguments with spaces. Previously every backslash was consumed as an escape, launching a nonexistent executable path on Windows. * feat(app): real brand logos and code-block commands in popular agents Render svgl brand marks for opencode (currentColor), OpenClaw, and Antigravity in neutral tiles, with monograms for pi and Hermes (no svgl logo). Show each suggested command as a proper code chip so it reads as code, not description. * fix(server): resolve custom agents against the login-shell PATH A GUI-launched server inherits a minimal PATH that omits shell-profile additions (homebrew, nvm/fnm, ~/.local/bin), so a custom agent's binary could be unfindable even when it works in a terminal. Ask the user's login shell for its PATH once (cached, timeout-guarded) and prepend it for custom command spawns. Windows inherits the full registry PATH already, so it is unaffected. * feat(app): real brand logos across built-in and custom agents Consolidate agent brand marks (Claude, Codex, opencode, OpenClaw, Antigravity, pi) into a shared module reusing the project's svg components, and use them in the provider templates, agent cards, and the Popular-agents picker. Picking a popular agent now carries its logo onto the saved agent. Removed the manual Icon text field. Hermes keeps a monogram until its logo asset is added. * feat(app): agent brand logos in the chat selector, Hermes icon, opencode color Render brand marks in the newtab + sidepanel provider selector by threading a brand key through the chat targets. Add the Hermes icon (object-contain, from the provided asset) and give opencode its own dark/white ink instead of the tile accent color. * fix(app): brand logos in the selected-agent chip, input avatar, and header The newtab search-bar pill and input avatar and the sidepanel header rendered a generic Bot for the selected agent. Resolve the agent's brand mark from its brand key in all three, falling back to Bot when none. * fix(app): make the sidepanel agent selector look clickable The header agent selector read as a static title once it showed a brand logo. Add a border, an active (foreground) name, and a rotating chevron caret so it reads as a dropdown, matching the newtab pill. * fix(mcp): reject unknown arguments on every browser tool (#2432) * fix(mcp): reject unknown arguments on every browser tool Only tabs, windows, and history rejected unknown fields. The other 14 tools dropped them during deserialization and ran with defaults, so a misspelled argument produced a successful but quietly wrong result: screenshot {page: 1, fullpage: true} -> viewport shot, no error grep {page, pattern, maxResults: 5} -> default 50 matches, no error This is the failure mode behind #2161, and the only reason that one surfaced is that tabs happens to be strict, so the retired hidden input raised "unknown field" instead of being ignored. Add deny_unknown_fields to the 14 permissive Rust arg structs and .strict() to the matching zod schemas, which also closes a parity gap where tabs and windows were strict in Rust but permissive in TypeScript. The schema pipeline already handles this: normalize_schema_value rewrites additionalProperties: false into {"not": {}}, so the generated schemas stay free of boolean nodes. Fixes #2431 * fix(mcp): reject unknown fields on nested tool arguments too Top-level strictness did not propagate into nested argument objects, so a misspelled nested key was still silently dropped and defaulted. screenshot size {width, heigth} accepted 'heigth', defaulted height to 768, and returned a successful but wrong capture. Add deny_unknown_fields / .strict() to the nested input objects (screenshot size, act fill fields) in both the Rust and TypeScript implementations, and cover a nested unknown key in the regression test. * test(mcp): walk nested schemas in the Rust strictness check The Rust regression test only inspected the top-level additionalProperties, so the nested structs fixed in the previous commit were guarded on the TypeScript side but not the Rust side. Walk the whole generated schema instead, so any object node that still accepts unknown properties fails and the message names the offending path: screenshot accepts unknown arguments at $.properties.size Verified by removing deny_unknown_fields from ScreenshotSize alone. --------- Co-authored-by: DaniAkash <DaniAkash@users.noreply.github.com> * perf(rust): drop dependency debug info from the dev profile (#2440) A clean dev build of the workspace produces a 3.0G target directory, and every git worktree pays it in full. A strip probe on the largest dependency rlib put debug info at 81% of the artifact: 224M drops to 42M. Dependencies are not stepped into during normal work, so their DWARF is pure disk cost. Turning it off for `*` leaves workspace crates untouched, and line-tables-only on those keeps panics and backtraces resolving to file and line while dropping variable and type DWARF. Measured on a clean build, same commit, same machine: target/ 3.0G -> 1.8G (-40%) target/debug/deps 2.1G -> 1.1G build time 57.33s -> 51.00s The build is faster because there is less debug info to write. Object files carrying DWARF drop from 9964 (1349M) to 1461 (121M). Smaller artifacts also shrink the Swatinem/rust-cache entry in the test workflow, which eases pressure on the per-repo Actions cache limit. `cargo clippy --workspace --all-targets -- -D warnings` passes clean. * fix(analytics): surface unrecognized MCP clients instead of dropping them (#2439) * fix(analytics): surface unrecognized MCP clients instead of dropping them Every client whose name was not in the ~19-entry allowlist was collapsed to a single opaque "unrecognized-client" bucket, discarding the real name before it reached analytics. That hid the largest slice of real users behind one label. Split the unrecognized path into privacy-safe buckets: a blank name becomes unrecognized-empty; a name carrying structural PII markers (email, path, URL, host:port) or an opaque over-long blob becomes unrecognized-redacted; and a safe-shaped name surfaces its own slug so real long-tail clients (roo-code, librechat, 5ire, ...) become visible and can be allowlisted. The PII gate runs on the original raw, not the slug, so stripped fragments cannot leak. * refactor(analytics): record unlisted client names directly, drop the marker gate An unlisted client now records its own slug; only a blank name is reported as unrecognized-empty. Removes the PII-marker and length gates: clientInfo.name is a client-chosen implementation identifier, so recording it verbatim is the intended behaviour. * ci: resolve the bun version from package.json in every workflow (#2441) * ci: resolve the bun version from package.json in every workflow Nine setup-bun steps had no version pinned, so they installed whatever Bun was newest at run time: the whole of Code Quality (biome, typecheck, claw-api-codegen, claw-api-contract, fallow), both Tests jobs, Audit, and the Turbo cache warmer. Those are the workflows that gate merges, so a Bun release could change install layout, resolution, or test behaviour with no commit and no warning. It also let Tests pass on a Bun the release workflows never use. The remaining six hardcoded "1.3.6" in YAML, a third place to keep in sync with what the repo already declares. All fifteen now read the version from the existing source of truth: packageManager: "bun@1.3.6" setup-bun's bun-version-file accepts package.json and prefers packageManager, falling back to engines.bun. Both are present and agree, so behaviour for the six release workflows is unchanged. Bumping Bun for the entire repo is now a one-line change. Every workflow was parsed to confirm valid YAML and that each setup-bun step resolves from the file with no leftover literal: 15 steps, 0 problems. * test(release): assert the release build pins bun via the version file The release extensions workflow test asserted the literal `bun-version: "1.3.6"` in the build job. The guard's intent is that the release build pins Bun rather than floating, and that still holds; the mechanism is now `bun-version-file` reading packageManager from package.json. Point the assertion at the new form so the guard keeps its meaning instead of pinning a string that no longer appears. * fix(ci): keep the macos builder on a literal bun pin The macOS build job runs on the self-hosted builder against the persistent tree at BROWSEROS_REPO_PATH and has no actions/checkout, so GITHUB_WORKSPACE never holds the source. setup-bun resolves bun-version-file from GITHUB_WORKSPACE, finds nothing, and falls back to latest, which would have left source-mode macOS releases on a floating toolchain. Restore the literal pin for this one job and record why it cannot read the version file. A job-level audit confirms it is the only one of the fifteen setup-bun steps without a checkout ahead of it. * feat(analytics): let a session declare its task category (#2443) * feat(analytics): let a session declare its task category name_session gains an optional category argument from a fixed enum. The free-form name stays local and drives the tab group title exactly as before; only the enum category is emitted, on a new agent_session_task_declared event (task_category + client_name), so we can aggregate what kind of work sessions do without any free-form text leaving the machine. An unrecognized category is coerced to "other" so the declaration still counts; a non-string is dropped. The prompt and skill now nudge the agent to pass a category. * fix(analytics): declare task category at most once per session; fix catalog boundary test Guard the task-declared emit with a per-session once-flag so a later name_session rename does not emit a second agent_session_task_declared, which would overcount the category mix and the declaration rate. Also register the new wire name in the analytics single-source boundary test and its catalog count. * fix(ci): update name_session route test for the category argument * refactor(server): replace custom compaction with AI SDK guided pruning (#2445) * refactor(server): replace custom compaction with AI SDK guided pruning Collapse the four-stage compaction pipeline (prune, tool-output reduction, LLM summarization, sliding-window fallback) into the shape the AI SDK compaction guide recommends: one trigger threshold, one prune pass, one deterministic prune-only fallback. The sliding-window fallback is replaced by pruneMessages hard mode at all four of its former call sites, which cannot orphan a tool call from its result. LLM summarization and the split-turn dual-summary path are removed. Token estimation stays image-aware rather than adopting the guide's JSON.stringify heuristic, which would read a single base64 screenshot as roughly 250K tokens. Fixes three latent issues along the way: the chars-per-token constant was 3 in the estimator and 4 in the inflation guard, the fallback targeted a looser budget than the trigger so a compaction could succeed and re-fire immediately, and a zero or negative client-supplied context window produced a threshold every request exceeded. * fix(server): stop compaction returning a transcript over the budget Pruning has no lever against plain user and assistant text, so once the floor pass has cleared every tool exchange a prose-heavy transcript came back untouched and went to the provider over the model's limit. Verified at a 200K window: 212,003 tokens in, 212,003 tokens out against a 180,000 threshold. Adds a message-drop floor that keeps the first message and the longest recent suffix that fits. It runs only after every tool call has already been pruned, so unlike a general sliding window it cannot separate a tool call from its result: there are no pairs left to break. A single message larger than the window still cannot be shrunk, which is now surfaced on the log line rather than left as a bare provider error. Also tightens the context window guard from > 0 to >= 1. A fractional value below one passed the check and then floored to zero, producing a zero-token threshold that every step exceeded. The test that should have caught the first bug asserted only that the message count did not grow, which passes when nothing changes at all. It now asserts the transcript lands under the threshold, at three window sizes. * perf(rust): share cargo intermediates across checkouts (#2446) * perf(rust): share cargo intermediates across checkouts Every checkout compiles its own copy of the dependency graph. Anyone keeping more than one clone or worktree open pays that in full each time, around 1.6G apiece. build-dir moves only the intermediate artifacts out of the checkout, and it supports path templating, so {cargo-cache-home} resolves to CARGO_HOME and one shared location covers every checkout on a machine. Nothing absolute or machine specific is committed. target-dir was the obvious alternative and does not work here: it has no templating, cargo expands neither ~ nor $HOME, so a committed value could only be relative to the checkout. That would limit sharing to sibling directories, and because it also moves the final artifacts it would break the three places the BrowserClaw release locates a built binary. Final artifacts still land in <checkout>/target, so nothing that resolves a build output by path changes. Measured across two checkouts of the same branch: cold build 52.36s target 227M shared 1.6G second checkout 16.14s target 227M shared 2.1G A release build against a warm shared directory still produces target/release/browseros-claw-server-rs. rust-cache saves only workspace target dirs plus the registry and git caches, and never reads a build dir setting, so the shared directory is named to it explicitly. Without that, CI would recompile the dependency graph on every run. * ci(rust): warm the rust cache on main and drop it fortnightly Three related gaps around the shared cargo build directory. The Rust cache was never warm for a new pull request. Tests run only on pull_request, so rust-cache saved under a PR branch's scope, and branches cannot read each other's caches. This is the same problem the Turbo warm run already solves, and Rust was simply never covered. It matters more now that the intermediates live in a cache-directories entry: without a warm run, every PR recompiles the dependency graph. Warming alone would not have worked. rust-cache builds its key from GITHUB_JOB unless shared-key is set, and the existing keys show it: v0-rust-test-Linux-x64-<hash>-<hash> A warm job under any other name would have written a cache nothing else could read. Both steps now pin the same shared-key, workspaces, cache-directories and toolchain, since the toolchain hashes into the key too. The new warm job mirrors what the Rust suites compile, test binaries and clippy's separate artifacts, and deliberately omits -D warnings because it exists to populate a cache rather than to gate on lints. Finally, rust-cache prunes only workspace target dirs and never extra cache-directories, so the shared build directory is cached wholesale and grows without bound. It is already the larger part of the problem: v0-rust 25 entries 6.97 GB all caches 262 entries 10.35 GB against a 10 GB allowance Being over the allowance means LRU eviction is already discarding other caches. Dropping the Rust entries on the 1st and 15th keeps that bounded, matched on the prefix so nothing else is touched, and the warm workflow is dispatched straight after so no branch waits for the next merge. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Athul Nambiar <108534940+athul-22@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Lets a user connect any ACP (Agent Client Protocol) agent, not just the two built-ins (Claude Code, Codex), by supplying a launch command through the AI settings UI. The
acpx-ai-providerengine already runs arbitrary ACP agents over stdio; this exposes that through a data-driven server model and a settings dialog.How it works
A new
customagent type carries aCustomAcpAgentConfig(command, optionalenv,fullAccessModes,reasoningEffortKey,systemPromptAppend,icon) stored as a nullable JSON column onacp_agents. The built-inclaude/codexagents keep their curated behavior unchanged;customis a strictly additive third path.Server
0007_add_custom_acp_agentsadds thecustom_configcolumn (plus the bootstrap DDL + migration-history mirror for packaged builds).POST /agentscross-validatescustomConfig(required for custom, rejected for built-ins); a newPUT /agents/:agentIdedits an agent and refreshes its running sessions.POST /acpx/probeaccepts an explicit command so a not-yet-saved agent can be validated.App
Tests
Server: command splitter, config round-trip through the real migration, create cross-validation + PUT, launcher run-as-given argv + env, policy (per-agent registry id, stored env/prompt/modes, empty full-access), and probe. App: form-helper parsing/config builder and card rendering. Full monorepo typecheck, biome, and the touched suites are green.
Follow-ups
envsecrets; today it is stored locally in plaintext with a clear warning, matching the built-ins' existing full-filesystem access.