fix(extension-host): offer reviewed plugin tools on the first turn (CW-7054)

The turn tool build started plugin reconciliation in the background, so a
newly enabled DSH/Native plugin's tools were missing from turn one (and from
the deferred catalog tool_search reads). The attachment now records the
plugin snapshot a finished reconcile activated; until the current snapshot is
settled, the tool build waits up to 10 s for a reconcile (run as its own task,
so a timeout never cancels it mid-activation). Settled snapshots keep the
background path. The DSH import receipt now says why `dsh.client` UI stays
off (WebKit restrictions do not stop WebRTC; isolated renderer is CW-7549).

Paths: crates/tui/src/core/engine.rs, crates/tui/src/extension_host/mod.rs,
crates/tui/src/plugins/install/dsh.rs, CHANGELOG.md, crates/tui/CHANGELOG.md
(via scripts/sync-changelog.sh --check: up to date).

Verified:
- scripts/dev-cargo.sh build -p codewhale-cli --bin codewhale: passed.
- scripts/dev-test.sh tui core::engine::tests:: extension_host:: :
  712 run, 712 passed. scripts/dev-test.sh tui plugins::install:: :
  66 run, 66 passed. Doc-comment removal and rustfmt line join only after.
- Live macOS Engine HTTP probe (artifacts/dsh-e2e-20261027, loopback fixture
  provider, no provider spend): DSH bundle preview -> exact-hash install ->
  review -> trust -> enable. Installed 3400ebb5c9: turn-one tool_search did
  not offer dsh_probe. This build: offered, approval card shown and allowed,
  tool ran, /probe-count reported 1.
Not verified: TUI/App UI walkthrough, Windows/Linux, questions, child agents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-27 02:20:55 -07:00
1 parent 0cf390f01e
commit 25225ef051
5 files changed
+50 -6

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Remote control works again:** `codewhale rc` (`/rc`) can attach to the hosted control plane for web Chat again. The control plane had only accepted the retired 0.9.13 runtime identity, so every 0.10.x Engine was refused with `runner_runtime_identity_unavailable`; it now accepts the exact released runtime identity (commit, image and digest) and fails closed when they are not configured.
- **Modern MCP client:** MCP servers now connect through the official MCP SDK (2.4) behind Codewhale's Rust broker by default (`mcp_backend = "host"`; `"rust"` stays available for one release). Servers that speak the 2026-07-28 revision are discovered with `server/discover`; other stdio and HTTP servers fall back to the legacy `initialize` handshake, and configured SSE servers keep theirs. Rust keeps process, network, credential, catalog, approval and cancellation authority. (CW-6713)
- **MCP compatibility fixes:** older servers that answer the discovery probe with an error or an unrelated result now connect through the legacy handshake instead of being refused after the probe. A server that rejects sign-in reports its real 401 and the `/mcp login` hint rather than the probe's "method not found". A stale OAuth token is refreshed once when the probe itself is rejected. A server whose command cannot start now says why (for example, command not found) instead of a generic broker refusal.
- **DeepSeek Harness plugins:** reviewed DSH plugins (definition API pinned to DSH 0.2.1-rc.2 in a shared compatibility table) run in the Native extension host. Tool output is validated against its declared schema, unsupported options are refused instead of ignored, and plugin commands can submit a prompt with visible plugin attribution. Terminal command receipts are kept in thread snapshots without entering model history. Changing the compatibility table invalidates earlier trust. Plugin UI views remain disabled. (CW-7054)
- **DeepSeek Harness plugins:** reviewed DSH plugins (definition API pinned to DSH 0.2.1-rc.2 in a shared compatibility table) run in the Native extension host. Tool output is validated against its declared schema, unsupported options are refused instead of ignored, and plugin commands can submit a prompt with visible plugin attribution. Terminal command receipts are kept in thread snapshots without entering model history. Changing the compatibility table invalidates earlier trust. A newly enabled plugin's tools are offered on the first turn: the turn waits up to 10 seconds for the plugin host to finish loading instead of adding them only from the next turn. Plugin UI views stay disabled, and the import receipt says why: WebKit's network restrictions do not stop WebRTC, so plugin UI waits for an isolated renderer. (CW-7054, CW-7549)
- **Computer Use is installed, not bundled:** the Computer Use plugin is no longer vendored into the binary. Install it from the `codewhale` marketplace, which points at its independent release; its consent tools load on first use, and decisions still require a human approval card. (CW-6732)
- **Tool image validation:** an image returned by a tool must decode before it is sent, so truncated or corrupt files are omitted with a receipt even when they are already within the delivery budget.
+1 -1
View File
@@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Remote control works again:** `codewhale rc` (`/rc`) can attach to the hosted control plane for web Chat again. The control plane had only accepted the retired 0.9.13 runtime identity, so every 0.10.x Engine was refused with `runner_runtime_identity_unavailable`; it now accepts the exact released runtime identity (commit, image and digest) and fails closed when they are not configured.
- **Modern MCP client:** MCP servers now connect through the official MCP SDK (2.4) behind Codewhale's Rust broker by default (`mcp_backend = "host"`; `"rust"` stays available for one release). Servers that speak the 2026-07-28 revision are discovered with `server/discover`; other stdio and HTTP servers fall back to the legacy `initialize` handshake, and configured SSE servers keep theirs. Rust keeps process, network, credential, catalog, approval and cancellation authority. (CW-6713)
- **MCP compatibility fixes:** older servers that answer the discovery probe with an error or an unrelated result now connect through the legacy handshake instead of being refused after the probe. A server that rejects sign-in reports its real 401 and the `/mcp login` hint rather than the probe's "method not found". A stale OAuth token is refreshed once when the probe itself is rejected. A server whose command cannot start now says why (for example, command not found) instead of a generic broker refusal.
- **DeepSeek Harness plugins:** reviewed DSH plugins (definition API pinned to DSH 0.2.1-rc.2 in a shared compatibility table) run in the Native extension host. Tool output is validated against its declared schema, unsupported options are refused instead of ignored, and plugin commands can submit a prompt with visible plugin attribution. Terminal command receipts are kept in thread snapshots without entering model history. Changing the compatibility table invalidates earlier trust. Plugin UI views remain disabled. (CW-7054)
- **DeepSeek Harness plugins:** reviewed DSH plugins (definition API pinned to DSH 0.2.1-rc.2 in a shared compatibility table) run in the Native extension host. Tool output is validated against its declared schema, unsupported options are refused instead of ignored, and plugin commands can submit a prompt with visible plugin attribution. Terminal command receipts are kept in thread snapshots without entering model history. Changing the compatibility table invalidates earlier trust. A newly enabled plugin's tools are offered on the first turn: the turn waits up to 10 seconds for the plugin host to finish loading instead of adding them only from the next turn. Plugin UI views stay disabled, and the import receipt says why: WebKit's network restrictions do not stop WebRTC, so plugin UI waits for an isolated renderer. (CW-7054, CW-7549)
- **Computer Use is installed, not bundled:** the Computer Use plugin is no longer vendored into the binary. Install it from the `codewhale` marketplace, which points at its independent release; its consent tools load on first use, and decisions still require a human approval card. (CW-6732)
- **Tool image validation:** an image returned by a tool must decode before it is sent, so truncated or corrupt files are omitted with a receipt even when they are already within the delivery budget.
+9 -3
View File
@@ -103,6 +103,7 @@ const SUBAGENT_COMPLETION_CHANNEL_CAPACITY: usize = 256;
/// tolerated by design (`let _ =`), and the terminal `Finished` waits for a
/// slot instead of being dropped.
const MCP_BOOT_CHANNEL_CAPACITY: usize = 64;
const EXTENSION_FIRST_SYNC_WAIT: Duration = Duration::from_secs(10);
const GOAL_CONTINUATION_FAILURE_DETAIL_MAX_BYTES: usize = 512;
const PLAN_SHELL_NETWORK_DENIED_HINT: &str = "Shell command blocked: in Plan mode shell commands run in a read-only sandbox with no writes and no network access. The user can change modes with /mode.";
@@ -2169,8 +2170,9 @@ impl Engine {
.as_ref()
.filter(|registry| registry.workspace() == config.workspace)
.cloned();
// Experimental extension host: start in the background, never on the
// first-prompt path. Its tools join at the next turn's rebuild. Only
// Experimental extension host: start in the background here. The
// first turn's tool build waits briefly for this snapshot to activate
// so reviewed plugin tools are present on turn one. Only
// an engine with its own plugin snapshot attaches; the empty fallback
// below would desire nothing and must not affect other engines.
let extension_host = match host.as_mut() {
@@ -6187,7 +6189,11 @@ impl Engine {
attachment
.manager()
.note_native_names(tool_registry.names());
attachment.sync_in_background();
if attachment.selection_settled() {
attachment.sync_in_background();
} else {
attachment.sync_within(EXTENSION_FIRST_SYNC_WAIT).await;
}
}
let (mut plugin_tool_names, refused_overrides) =
configure_plugin_tools(&mut tool_registry, self.config.tools.as_ref());
+38
View File
@@ -545,6 +545,7 @@ struct AttachmentState {
selection: composition_scope::CompositionSelection,
revision: u64,
selection_cancel: CancellationToken,
settled: Option<composition_scope::SelectionRevision>,
session_id: Option<String>,
agent_id: Option<String>,
}
@@ -1717,6 +1718,7 @@ impl ExtensionHostManager {
selection: composition_scope::CompositionSelection::default(),
revision: 1,
selection_cancel: CancellationToken::new(),
settled: None,
session_id: None,
agent_id: None,
},
@@ -2143,6 +2145,16 @@ impl ExtensionHostManager {
} else {
Ok(())
};
for state in shared
.attachments
.lock()
.expect("attachments lock")
.values_mut()
{
if state.selection.revision == state.plugins.caller_selection() {
state.settled = state.selection.revision;
}
}
match (plugins, builtin) {
(Err(error), Err(other)) => {
shared.diagnostic(other);
@@ -3338,6 +3350,32 @@ impl HostAttachment {
self.manager.reconcile_in_background();
}
pub fn selection_settled(&self) -> bool {
self.manager
.shared
.attachments
.lock()
.expect("attachments lock")
.get(&self.id)
.is_some_and(|state| {
state.settled.is_some() && state.settled == state.plugins.caller_selection()
})
}
pub async fn sync_within(&self, limit: Duration) {
if tokio::runtime::Handle::try_current().is_err() {
return;
}
let manager = Arc::clone(&self.manager);
let policy = activation::extension_host_policy_enabled();
let task = tokio::spawn(async move {
if let Err(error) = manager.reconcile_with_policy(policy).await {
manager.shared.diagnostic(error);
}
});
let _ = tokio::time::timeout(limit, task).await;
}
/// Add this engine's live extension tools to `tool_registry`.
pub fn install_tools(&self, tool_registry: &mut crate::tools::ToolRegistry) -> Vec<String> {
self.manager.install_tools_for(self.id, tool_registry)
+1 -1
View File
@@ -547,7 +547,7 @@ fn load_bundle(package: &Package) -> Result<LoadedBundle> {
.is_some_and(|client| !matches!(client, Value::Null))
{
notes.push(
"package declares `dsh.client`; the client UI half has no Codewhale equivalent and was not converted"
"package declares `dsh.client`; its UI half was not converted and stays off until Codewhale has a renderer that can keep plugin UI off the network (WebKit restrictions do not stop WebRTC)"
.to_string(),
);
}