fix(engine): keep injected-route config when preflight fails [skip ci]

Fixes the remaining hosted test failures on 549ff01ebb (CI 42381817495).

Runtime: a host-provided route with no preflighted client took its config
with mem::take before the optional credential preflight, so a failed
preflight left the route with a default config and lost its session
isolation and approval deadlines. It now clones, so the configured route
survives a failure. The existing privacy/timeout assertions are unchanged.

Exports: the four capabilities the runtime already serializes
(thread_scoped_git_writes, thread_scoped_snapshots, worktrees,
job_operations) are restored to the committed JSON schema, the TypeScript
declarations and the runtime SDK copy; the JSON matches the Rust-generated
contract.

Existing expectations realigned with shipped behavior: `present` is an eager
core tool (hook category stays other) in the five tool-surface tests; Native
extension entries accept .cjs and .cts; account logout is account-only and
keeps provider keys (two TUI tests renamed to say so). The router HTTP-status
unit test disables retry because it mounts a single response; runtime retries
are unchanged. THIRD_PARTY_NOTICES restores the semver 7.8.5 ISC text the
bundle-notice check requires. Both changelogs record the fixes.

Verified on this Mac: the 13 hosted failing cases, each run by exact name and
each executing exactly one test, 13/13 pass (RUST_MIN_STACK=8388608, hermetic
test home, locked/offline/-j1); clippy --lib --bins --all-features over 11
packages exit 0 with zero diagnostics; runtime SDK tests and strict TypeScript
passed earlier on these same bytes (exit 0); rustfmt --check and git diff
--check pass. The full suite runs in the final hosted validation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-11-07 10:41:23 -08:00
1 parent ed2b69e4dd
commit b175b1f9ed
14 files changed
+92 -15

No files matched your search

+4
View File
@@ -275,6 +275,10 @@ until the matching tag, packages, checksums, and release assets exist.
### Fixed
- **Host-provided model clients:** keep configured session isolation and
approval deadlines when optional credential preflight fails.
- **Runtime SDK exports:** include the existing thread-scoped Git, snapshot,
worktree and job-operation capabilities in the committed schema and types.
- **Plugin enablement revalidates its review:** enabling rechecks the staged
snapshot and, under the existing state lock, refuses a changed review
generation or trust receipt. The macOS stale-enable check passed (1/1);
+21
View File
@@ -933,6 +933,27 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
### `semver` 7.8.5
```text
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
```
## Optional compiled extension host
Node remains the default extension-host runtime. An explicitly installed
@@ -289,6 +289,25 @@
"type": "boolean",
"default": false
},
"thread_scoped_git_writes": {
"type": "boolean",
"description": "Git stage, unstage, discard, commit, push and branch accept\n`?thread_id=` and act on that thread's workspace.",
"default": false
},
"thread_scoped_snapshots": {
"type": "boolean",
"description": "`GET /v1/snapshots` and `POST /v1/snapshots/{id}/restore` accept\n`?thread_id=` and act on that thread's workspace.",
"default": false
},
"worktrees": {
"type": "boolean",
"description": "`GET`/`POST /v1/worktrees` and `POST /v1/worktrees/remove`.",
"default": false
},
"job_operations": {
"type": "boolean",
"default": false
},
"thread_shell_consent": {
"type": "boolean",
"description": "Explicit per-thread shell opt-in is checked against loaded policy and\ncannot broaden a conversation while it has an active turn.",
@@ -454,6 +473,10 @@
"thread_idle_precondition",
"thread_patch_undo_turn_id",
"thread_scoped_reads",
"thread_scoped_git_writes",
"thread_scoped_snapshots",
"worktrees",
"job_operations",
"thread_shell_consent",
"turns",
"turn_operation_idempotency",
@@ -62,6 +62,10 @@ export type RuntimeCapabilities = {
thread_idle_precondition: boolean
thread_patch_undo_turn_id: boolean
thread_scoped_reads: boolean
thread_scoped_git_writes: boolean
thread_scoped_snapshots: boolean
worktrees: boolean
job_operations: boolean
thread_shell_consent: boolean
turns: boolean
turn_operation_idempotency: boolean
+1 -1
View File
@@ -2056,7 +2056,7 @@ impl Engine {
let concrete_client = match route.take_preflighted_client() {
Some(client) => Ok(client),
None => {
let config = std::mem::take(&mut route.config);
let config = route.config.clone();
match CodewhaleClient::capture_candidate_preflight(config, &route.candidate).await {
Ok((config, captured)) => {
route.config = config;
@@ -1,6 +1,6 @@
#[test]
fn default_active_contract_keeps_discovery_and_core_tools_eager() {
const EXPECTED_NATIVE: [&str; 15] = [
const EXPECTED_NATIVE: [&str; 16] = [
"read",
"write",
"edit",
@@ -13,6 +13,7 @@ fn default_active_contract_keeps_discovery_and_core_tools_eager() {
"get_goal",
"update_goal",
"task_shell_wait",
"present",
"load_skill",
"context_archive_get",
"context_archive_search",
@@ -113,6 +113,7 @@ fn first_turn_surface_is_stable_across_plan_work_and_operate() {
"get_goal",
"update_goal",
"task_shell_wait",
"present",
"load_skill",
"context_archive_get",
"context_archive_search"
@@ -127,6 +128,7 @@ fn first_turn_surface_is_stable_across_plan_work_and_operate() {
"edit",
"file_search",
"load_skill",
"present",
"read",
"task_shell_wait",
"todo_write",
@@ -152,6 +154,7 @@ fn first_turn_surface_is_stable_across_plan_work_and_operate() {
"get_goal",
"update_goal",
"task_shell_wait",
"present",
"Git",
"Run",
"tasks",
+1 -1
View File
@@ -1956,7 +1956,7 @@ fn native_entry_rule_fails_validation_when_the_host_is_enabled() {
let errors = native_errors(&registry, name);
assert_eq!(errors.len(), 1, "{name}: {errors:?}");
assert!(
errors[0].contains(".mjs, .js or .mts"),
errors[0].contains(".mjs, .js, .cjs, .mts or .cts"),
"{name}: {errors:?}"
);
}
+1
View File
@@ -6806,6 +6806,7 @@ command = "echo project"
("get_goal", "other"),
("update_goal", "other"),
("task_shell_wait", "other"),
("present", "other"),
// Reads the skill registry, not caller-named paths, so it keeps
// the classification it already had as a deferred tool. Making it
// eager must not silently re-gate it.
+2 -1
View File
@@ -3951,7 +3951,8 @@ mod decision_router_tests {
.expect(1)
.mount(&server)
.await;
let config = decision_config(&format!("{}/api/v1", server.uri()), false, 2);
let mut config = decision_config(&format!("{}/api/v1", server.uri()), false, 2);
config.retry = Some(toml::from_str("enabled = false").unwrap());
let selection = route(&config, "Explain the diff", "").await;
assert_eq!(selection.model, "deepseek-v4-pro", "status {status}");
@@ -8883,6 +8883,7 @@ fn small_surface_starts_with_core_tools_and_read_only_goal_control() {
"file_search",
"get_goal",
"load_skill",
"present",
"read",
"task_shell_wait",
"todo_write",
@@ -9269,6 +9270,7 @@ fn small_surface_depth_cap_removes_only_agent() {
"file_search",
"get_goal",
"load_skill",
"present",
"read",
"task_shell_wait",
"todo_write",
+4
View File
@@ -275,6 +275,10 @@ until the matching tag, packages, checksums, and release assets exist.
### Fixed
- **Host-provided model clients:** keep configured session isolation and
approval deadlines when optional credential preflight fails.
- **Runtime SDK exports:** include the existing thread-scoped Git, snapshot,
worktree and job-operation capabilities in the committed schema and types.
- **Plugin enablement revalidates its review:** enabling rechecks the staged
snapshot and, under the existing state lock, refuses a changed review
generation or trust receipt. The macOS stale-enable check passed (1/1);
+20 -11
View File
@@ -6191,7 +6191,7 @@ context_window = 262144
}
#[test]
fn test_logout_clears_api_key_state() {
fn test_logout_starts_account_logout_without_clearing_api_key_state() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
@@ -6209,19 +6209,23 @@ context_window = 262144
fs::write(&config_path, "api_key = \"test-key\"\n").unwrap();
let mut app = create_test_app();
app.onboarding = OnboardingState::None;
app.onboarding_needs_api_key = false;
app.onboarding_missing_key_recovery = false;
let result = logout_command(&mut app);
assert!(result.message.is_some());
assert_eq!(app.onboarding, OnboardingState::Provider);
assert!(app.onboarding_needs_api_key);
assert!(app.onboarding_missing_key_recovery);
assert_eq!(result.action, Some(AppAction::OpenProviderPicker));
assert!(!result.is_error);
assert!(result.message.is_none());
assert_eq!(app.onboarding, OnboardingState::None);
assert!(!app.onboarding_needs_api_key);
assert!(!app.onboarding_missing_key_recovery);
assert_eq!(result.action, Some(AppAction::StartAccountLogout));
let updated = fs::read_to_string(config_path).unwrap();
assert!(!updated.contains("api_key"));
assert_eq!(updated, "api_key = \"test-key\"\n");
}
#[test]
fn logout_clears_only_exact_named_custom_provider_key() {
fn logout_preserves_named_custom_provider_keys() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
@@ -6245,10 +6249,15 @@ context_window = 262144
let result = logout_command(&mut app);
assert!(result.message.is_some());
assert!(!result.is_error);
assert!(result.message.is_none());
assert_eq!(result.action, Some(AppAction::StartAccountLogout));
assert_eq!(app.provider_identity_for_persistence(), "custom-a");
let updated = fs::read_to_string(config_path).unwrap();
assert!(!updated.contains("a-key"), "{updated}");
assert!(updated.contains("b-key"), "{updated}");
assert_eq!(
updated,
"[providers.custom-a]\napi_key = \"a-key\"\n\n[providers.custom-b]\napi_key = \"b-key\"\n"
);
}
#[test]
+4
View File
@@ -62,6 +62,10 @@ export type RuntimeCapabilities = {
thread_idle_precondition: boolean
thread_patch_undo_turn_id: boolean
thread_scoped_reads: boolean
thread_scoped_git_writes: boolean
thread_scoped_snapshots: boolean
worktrees: boolean
job_operations: boolean
thread_shell_consent: boolean
turns: boolean
turn_operation_idempotency: boolean