mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
Bind agent turns to the selected Codewhale account model
Advertise account_model_owner on the Runtime API and Chat relay. Bind each account-owned turn to the exact built-in Codewhale route and its signed-in secure session, using the account gateway credential without local, environment, keyring or other-provider fallback. Keep saved Chat defaults independent. Include the account owner in durable turn fingerprints and require any supplied profile constitution to share that owner. Return completed exact-operation receipts before fresh credential admission, and preflight account overrides even when the Engine has a cached client. Preserve ordinary turn compatibility. Extend existing account-overlay coverage for conflicting local keys, mismatched owners and logout. Use the protected ConfigStore snapshot for saved Claude generation checks. Preserve the outer compiler cache and shared machine lock through hermetic test homes. Split Windows Terminal composer readiness from the clipboard-paste deadline, retaining real paste/readback/one-Enter assertions. Verification on the canonical checkout: - Required npm test: 1,312 passed, 7 skipped, 0 failed. - npm run check:web: passed, including production web build. - Focused native nextest: 290 passed, 0 failed (1 protocol, 134 OAuth, 23 account/runtime, 6 registry, plus peer-owned 122 cloud and 4 workspace). - Native Rust/Cargo/registry input hashes unchanged throughout the batch. - Existing hermetic-home fixture: 10 passed; existing CI wiring checks: 19 passed. - Rustfmt, diff check and provider registry passed. Working-tree blocking-call guard passed at 536 sites/165 files; peer-owned cloud budget change is excluded. - Windows helper syntax passed; hosted Windows clipboard qualification pending. Only the nine account hunks in runtime_threads.rs and runtime-api registry summary are included; concurrent activity and Computers hunks remain uncommitted. No deployment, provider spend, GitHub review publication or release performed. Refs APPS-264, APPS-265, APPS-267.
This commit is contained in:
1 parent
251ff4e7ce
commit
e8717e427f
12 files changed
+191
-8
No files matched your search
@@ -59,6 +59,8 @@ pub struct RuntimeCapabilities {
|
||||
pub client_token_intents: bool,
|
||||
#[serde(default)]
|
||||
pub account_session: bool,
|
||||
#[serde(default)]
|
||||
pub account_model_owner: bool,
|
||||
pub threads: bool,
|
||||
/// Explicit per-thread shell opt-in is checked against loaded policy and
|
||||
/// cannot broaden a conversation while it has an active turn.
|
||||
@@ -435,6 +437,7 @@ mod tests {
|
||||
turn_output_token_limit: false,
|
||||
profile_constitution: false,
|
||||
account_session: true,
|
||||
account_model_owner: false,
|
||||
threads: true,
|
||||
thread_shell_consent: true,
|
||||
turns: true,
|
||||
|
||||
@@ -2205,6 +2205,8 @@ pub struct Config {
|
||||
#[serde(skip)]
|
||||
pub(crate) account_model_access:
|
||||
std::sync::Arc<parking_lot::RwLock<Option<AccountModelAccess>>>,
|
||||
#[serde(skip)]
|
||||
pub(crate) required_account_model_owner: Option<String>,
|
||||
/// Persisted exact-route declarations, separate from provider credentials.
|
||||
#[serde(
|
||||
default,
|
||||
@@ -6342,6 +6344,30 @@ impl Config {
|
||||
&& base_url_uses_local_host(&self.active_route_base_url()))
|
||||
}
|
||||
|
||||
pub(crate) fn bind_account_model_owner(&mut self, owner: &str) -> Result<()> {
|
||||
anyhow::ensure!(
|
||||
!owner.is_empty()
|
||||
&& owner.len() <= 240
|
||||
&& !owner.chars().any(|c| c.is_control() || c.is_whitespace()),
|
||||
"Invalid account model owner"
|
||||
);
|
||||
let access = self
|
||||
.account_model_access
|
||||
.read()
|
||||
.clone()
|
||||
.context("Connect account model access before running the Codewhale agent")?;
|
||||
self.account_model_access = std::sync::Arc::new(parking_lot::RwLock::new(Some(access)));
|
||||
self.required_account_model_owner = Some(owner.to_string());
|
||||
let identity = self
|
||||
.resolve_persisted_provider_identity(Some("codewhale"), Some("codewhale"))
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
anyhow::ensure!(
|
||||
self.account_model_api_key(&identity).is_some(),
|
||||
"Codewhale agent model access must belong to the signed-in account"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn account_model_api_key(&self, identity: &ProviderIdentity) -> Option<String> {
|
||||
let provider = identity.provider;
|
||||
// Exact endpoint binding, including path. A custom Codewhale route
|
||||
@@ -6370,7 +6396,11 @@ impl Config {
|
||||
.runtime_info_at(chrono::Utc::now())
|
||||
.ok()?;
|
||||
(account.state == codewhale_secrets::account::AccountSessionState::Authenticated
|
||||
&& account.session_id.as_deref() == Some(access.session_id.as_str()))
|
||||
&& account.session_id.as_deref() == Some(access.session_id.as_str())
|
||||
&& self
|
||||
.required_account_model_owner
|
||||
.as_ref()
|
||||
.is_none_or(|owner| account.account_id.as_ref() == Some(owner)))
|
||||
.then(|| access.credential.expose_secret().to_string())
|
||||
}
|
||||
|
||||
@@ -6381,6 +6411,9 @@ impl Config {
|
||||
&self,
|
||||
identity: &ProviderIdentity,
|
||||
) -> Option<crate::route_receipt::CredentialGeneration> {
|
||||
if self.required_account_model_owner.is_some() {
|
||||
return None;
|
||||
}
|
||||
self.verify_provider_identity(identity).ok()?;
|
||||
let mut scoped = self.clone();
|
||||
scoped.scope_to_provider_identity(identity).ok()?;
|
||||
@@ -6565,6 +6598,11 @@ impl Config {
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
self.verify_provider_identity(&identity)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
if self.required_account_model_owner.is_some() {
|
||||
return self.account_model_api_key(&identity)
|
||||
.map(|key| (key, "Codewhale account".to_string()))
|
||||
.context("The selected account model access is unavailable; no credential fallback is allowed");
|
||||
}
|
||||
let provider = identity.provider;
|
||||
let keyless = || (String::new(), "none (keyless route)".to_string());
|
||||
|
||||
@@ -10539,6 +10577,7 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
|
||||
legacy_root: base.legacy_root,
|
||||
legacy_root_custom_generation: base.legacy_root_custom_generation,
|
||||
account_model_access: base.account_model_access,
|
||||
required_account_model_owner: base.required_account_model_owner,
|
||||
account_profile: override_cfg.account_profile.or(base.account_profile),
|
||||
plugin_oauth_read_only: base.plugin_oauth_read_only || override_cfg.plugin_oauth_read_only,
|
||||
runtime_chat_isolated: override_cfg.runtime_chat_isolated || base.runtime_chat_isolated,
|
||||
|
||||
@@ -3752,9 +3752,12 @@ fn get_owned_credentials_with(
|
||||
fn validate_saved_claude_generation(config: &Config, name: &str, locked: bool) -> Result<()> {
|
||||
let path = crate::config_persistence::config_toml_path(config.loaded_config_path.as_deref())?;
|
||||
let validate = |path: &Path| {
|
||||
let contents = std::fs::read_to_string(path)
|
||||
let store = codewhale_config::ConfigStore::load(Some(path.to_path_buf()))
|
||||
.context("Could not verify current Claude sign-in configuration")?;
|
||||
let saved = Config::from_saved_document(&contents, config.account_profile.as_deref())
|
||||
let contents = store
|
||||
.original_body()
|
||||
.context("Current Claude sign-in configuration is missing")?;
|
||||
let saved = Config::from_saved_document(contents, config.account_profile.as_deref())
|
||||
.map_err(|_| anyhow::anyhow!("Current Claude sign-in configuration is invalid"))?;
|
||||
let identity = saved
|
||||
.builtin_provider_identity(ProviderKind::Anthropic)
|
||||
|
||||
@@ -1236,6 +1236,7 @@ fn default_runtime_capabilities() -> RuntimeCapabilities {
|
||||
RuntimeCapabilities {
|
||||
client_token_intents: true,
|
||||
account_session: true,
|
||||
account_model_owner: true,
|
||||
threads: true,
|
||||
thread_shell_consent: true,
|
||||
turns: true,
|
||||
@@ -8159,6 +8160,7 @@ async fn retry_thread_turn(
|
||||
.start_turn_from_stored_images(
|
||||
&forked_thread.id,
|
||||
StartTurnRequest {
|
||||
account_model_owner: None,
|
||||
profile_constitution: None,
|
||||
expected_workspace: None,
|
||||
max_output_tokens,
|
||||
@@ -9091,6 +9093,7 @@ async fn stream_turn(
|
||||
.start_turn(
|
||||
&thread.id,
|
||||
StartTurnRequest {
|
||||
account_model_owner: None,
|
||||
profile_constitution: None,
|
||||
max_output_tokens: req.max_output_tokens,
|
||||
prompt,
|
||||
@@ -10432,6 +10435,34 @@ pub(crate) fn runtime_chat_relay_catalog(
|
||||
return Err("Codewhale returned an invalid Runtime Chat relay challenge.".to_string());
|
||||
}
|
||||
|
||||
let mut account_config;
|
||||
let ready = config
|
||||
.active_provider_identity()
|
||||
.ok()
|
||||
.is_some_and(|identity| {
|
||||
matches!(
|
||||
crate::provider_readiness::credential_state_for_provider(config, &identity),
|
||||
CredentialState::Saved
|
||||
| CredentialState::ImportedToken
|
||||
| CredentialState::Local
|
||||
| CredentialState::NoAuth
|
||||
)
|
||||
});
|
||||
let config = if ready {
|
||||
config
|
||||
} else {
|
||||
account_config = config.clone();
|
||||
let identity = account_config
|
||||
.resolve_persisted_provider_identity(Some("codewhale"), Some("codewhale"))?;
|
||||
if account_config.account_model_api_key(&identity).is_none() {
|
||||
return Err(
|
||||
"Connect account model access before using the Codewhale agent.".to_string(),
|
||||
);
|
||||
}
|
||||
account_config.scope_to_provider_identity(&identity)?;
|
||||
&account_config
|
||||
};
|
||||
|
||||
let identity = config
|
||||
.active_provider_identity()
|
||||
.map_err(|_| "The active Runtime provider identity is invalid.".to_string())?;
|
||||
@@ -10483,6 +10514,7 @@ pub(crate) fn runtime_chat_relay_catalog(
|
||||
"turn_image_inputs": true,
|
||||
"turn_output_token_limit": true,
|
||||
"profile_constitution": true,
|
||||
"account_model_owner": true,
|
||||
"tool_execution": false,
|
||||
"stable_event_ids": true,
|
||||
},
|
||||
|
||||
@@ -744,7 +744,24 @@ mod tests {
|
||||
crate::provider_catalog_live::record_success_if_current(&ticket, delta()),
|
||||
Some(CatalogStatus::Fresh)
|
||||
);
|
||||
let mut strict = config.clone();
|
||||
strict.bind_account_model_owner("fixture-user").unwrap();
|
||||
strict
|
||||
.provider_config_for_mut(&strict.test_identity_for_kind(ProviderKind::Codewhale))
|
||||
.unwrap()
|
||||
.api_key = Some("other-local-key".into());
|
||||
assert_eq!(
|
||||
strict.active_route_api_key_read_only().unwrap(),
|
||||
"cwc_fixture_device_credential_not_real"
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.clone()
|
||||
.bind_account_model_owner("other-account")
|
||||
.is_err()
|
||||
);
|
||||
store.clear().unwrap();
|
||||
assert!(strict.active_route_api_key_read_only().is_err());
|
||||
invalidate_stale_account_catalog(&config);
|
||||
assert!(
|
||||
crate::provider_catalog_live::cached_entry_for_route(
|
||||
|
||||
@@ -71,6 +71,8 @@ fn take_state_persist_failure(path: &Path) -> bool {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(crate) struct RuntimeChatPrompt {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub account_model_owner: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub profile_constitution:
|
||||
Option<codewhale_config::user_constitution::ProfileConstitutionSnapshot>,
|
||||
@@ -765,6 +767,7 @@ impl RuntimeChatRelayHost {
|
||||
.start_turn_with_reserved_id(
|
||||
&binding.native_thread_id,
|
||||
StartTurnRequest {
|
||||
account_model_owner: command.account_model_owner.clone(),
|
||||
profile_constitution: command.profile_constitution.clone(),
|
||||
expected_workspace: None,
|
||||
max_output_tokens: command.max_output_tokens,
|
||||
@@ -986,6 +989,16 @@ impl RuntimeChatRelayHost {
|
||||
}
|
||||
|
||||
fn validate_route(&self, command: &RuntimeChatPrompt) -> Result<(), String> {
|
||||
if let Some(owner) = &command.account_model_owner {
|
||||
let expected = owner_scope_fingerprint(owner, &self.target_ref, &self.session_id);
|
||||
if self.state.lock().owner_scope_fingerprint.as_deref() != Some(expected.as_str()) {
|
||||
return Err("The agent model belongs to another account.".to_string());
|
||||
}
|
||||
let mut config = (*self.config).clone();
|
||||
return config
|
||||
.bind_account_model_owner(owner)
|
||||
.map_err(|error| error.to_string());
|
||||
}
|
||||
let catalog = self.catalog("a2345678901234567890123456789012")?;
|
||||
let provider = catalog
|
||||
.get("providers")
|
||||
@@ -1202,6 +1215,20 @@ impl RuntimeChatRelayHost {
|
||||
|
||||
impl RuntimeChatPrompt {
|
||||
pub(crate) fn validate_shape(&self) -> Result<(), String> {
|
||||
if let Some(owner) = &self.account_model_owner {
|
||||
validate_owner_component(owner, "account")?;
|
||||
if self.model_provider != "codewhale"
|
||||
|| self.model_provider_id != "codewhale"
|
||||
|| !self.model.contains('/')
|
||||
|| self.model.starts_with("codewhale/")
|
||||
|| !self.images.is_empty()
|
||||
|| self.reasoning_effort.is_some()
|
||||
{
|
||||
return Err(
|
||||
"The agent model requires an exact account provider and model.".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
crate::image_attach::prepare_runtime_images(&self.images)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if !self.images.is_empty() && self.model.trim().eq_ignore_ascii_case("auto") {
|
||||
@@ -1231,6 +1258,16 @@ impl RuntimeChatPrompt {
|
||||
}
|
||||
if let Some(snapshot) = &self.profile_constitution {
|
||||
snapshot.validate().map_err(|error| error.to_string())?;
|
||||
if self
|
||||
.account_model_owner
|
||||
.as_ref()
|
||||
.is_some_and(|owner| &snapshot.account_id != owner)
|
||||
{
|
||||
return Err(
|
||||
"The profile constitution and agent model must belong to the same account."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(system_prompt) = self.system_prompt.as_deref()
|
||||
&& (system_prompt.trim().is_empty()
|
||||
@@ -1897,6 +1934,7 @@ mod tests {
|
||||
#[test]
|
||||
fn chat_command_shape_requires_empty_tools_and_exact_chat_modes() {
|
||||
let mut prompt = RuntimeChatPrompt {
|
||||
account_model_owner: None,
|
||||
profile_constitution: None,
|
||||
images: Vec::new(),
|
||||
max_output_tokens: None,
|
||||
@@ -1955,6 +1993,7 @@ mod tests {
|
||||
.unwrap();
|
||||
host.authorize_run("run_fixture").unwrap();
|
||||
let prompt = RuntimeChatPrompt {
|
||||
account_model_owner: None,
|
||||
profile_constitution: None,
|
||||
images: Vec::new(),
|
||||
max_output_tokens: None,
|
||||
@@ -2024,6 +2063,7 @@ mod tests {
|
||||
.unwrap_or_else(|| provider.as_str())
|
||||
.to_string();
|
||||
let prompt = RuntimeChatPrompt {
|
||||
account_model_owner: None,
|
||||
profile_constitution: None,
|
||||
images: Vec::new(),
|
||||
max_output_tokens: None,
|
||||
|
||||
@@ -4894,6 +4894,8 @@ pub struct UpdateThreadRequest {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct StartTurnRequest {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub account_model_owner: Option<String>,
|
||||
/// Account-authorized data, rendered only by the Engine for this turn.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub profile_constitution:
|
||||
@@ -8849,6 +8851,7 @@ impl RuntimeThreadManager {
|
||||
continuation_index: u32,
|
||||
) -> Result<TurnRecord> {
|
||||
let req = StartTurnRequest {
|
||||
account_model_owner: None,
|
||||
profile_constitution: None,
|
||||
expected_workspace: None,
|
||||
max_output_tokens: None,
|
||||
@@ -9379,6 +9382,7 @@ impl RuntimeThreadManager {
|
||||
.start_turn_with_source(
|
||||
thread_id,
|
||||
StartTurnRequest {
|
||||
account_model_owner: None,
|
||||
profile_constitution: None,
|
||||
expected_workspace: None,
|
||||
max_output_tokens: None,
|
||||
@@ -14221,7 +14225,7 @@ impl RuntimeThreadManager {
|
||||
)
|
||||
};
|
||||
let mode = policy.mode;
|
||||
let cfg_snapshot = self.config.read().clone();
|
||||
let mut cfg_snapshot = self.config.read().clone();
|
||||
// Optional per-turn provider override: routes this turn only. The
|
||||
// saved thread keeps its provider; `route_thread` is the view the
|
||||
// route, fingerprint and turn receipt are resolved from.
|
||||
@@ -14245,6 +14249,13 @@ impl RuntimeThreadManager {
|
||||
}
|
||||
None => req.model.as_deref().unwrap_or(&thread.model).to_string(),
|
||||
};
|
||||
if req.account_model_owner.is_some() {
|
||||
let identity = self.provider_identity_for_thread(&cfg_snapshot, &route_thread)?;
|
||||
anyhow::ensure!(identity.provider == ProviderKind::Codewhale && identity.key.as_str() == "codewhale"
|
||||
&& requested_model.split_once('/').is_some_and(|(provider, model)| !provider.is_empty() && !provider.eq_ignore_ascii_case("codewhale") && !model.is_empty())
|
||||
&& !requested_model.chars().any(|c| c.is_whitespace() || c.is_control()),
|
||||
"The agent model requires an exact account provider and model");
|
||||
}
|
||||
let auto_model = requested_model.trim().eq_ignore_ascii_case("auto");
|
||||
if !image_blocks.is_empty() && (requested_model.is_empty() || requested_model.trim() != requested_model) {
|
||||
bail!("image inputs require an exact nonempty named model");
|
||||
@@ -14270,7 +14281,11 @@ impl RuntimeThreadManager {
|
||||
if req.max_output_tokens.is_some() && auto_model {
|
||||
bail!("maxOutputTokens requires an exact model; Auto routing is unsupported");
|
||||
}
|
||||
if let Some(snapshot) = &req.profile_constitution { snapshot.validate()?; }
|
||||
if let Some(snapshot) = &req.profile_constitution {
|
||||
snapshot.validate()?;
|
||||
anyhow::ensure!(req.account_model_owner.as_ref().is_none_or(|owner| &snapshot.account_id == owner),
|
||||
"The profile constitution and agent model must belong to the same account");
|
||||
}
|
||||
let operation = if let Some(operation_key) = req.operation_key.as_deref() {
|
||||
validate_runtime_turn_operation_key(operation_key)?;
|
||||
let request_fingerprint = runtime_turn_request_fingerprint(
|
||||
@@ -14302,6 +14317,13 @@ impl RuntimeThreadManager {
|
||||
"profile_constitution": snapshot,
|
||||
})).as_bytes())
|
||||
} else { request_fingerprint };
|
||||
let request_fingerprint = if let Some(owner) = &req.account_model_owner {
|
||||
crate::hashing::sha256_hex(crate::client::canonical_json(&json!({
|
||||
"domain": "codewhale:account-model-owner-turn:v1",
|
||||
"historical_fingerprint": request_fingerprint,
|
||||
"account_model_owner": owner,
|
||||
})).as_bytes())
|
||||
} else { request_fingerprint };
|
||||
let request_fingerprint=narrowing.request_fingerprint(request_fingerprint);
|
||||
self.prepare_runtime_turn_operation(
|
||||
thread_id,
|
||||
@@ -14317,6 +14339,9 @@ impl RuntimeThreadManager {
|
||||
{
|
||||
return Ok((original_turn, true));
|
||||
}
|
||||
if let Some(owner) = &req.account_model_owner {
|
||||
cfg_snapshot.bind_account_model_owner(owner)?;
|
||||
}
|
||||
if !image_blocks.is_empty() || req.max_output_tokens.is_some() {
|
||||
let identity = self.provider_identity_for_thread(&cfg_snapshot, &route_thread)?;
|
||||
let route = resolve_runtime_thread_route_for_identity(&cfg_snapshot, &identity, Some(&requested_model))?;
|
||||
@@ -14449,7 +14474,7 @@ impl RuntimeThreadManager {
|
||||
None,
|
||||
)
|
||||
};
|
||||
let route = if client_preflight_required || turn_provider.is_some() {
|
||||
let route = if client_preflight_required || turn_provider.is_some() || req.account_model_owner.is_some() {
|
||||
route
|
||||
.preflight()
|
||||
.map_err(|reason| anyhow!("Failed to validate runtime thread route: {reason}"))?
|
||||
|
||||
@@ -1220,6 +1220,19 @@ Without `model`, the turn uses that provider's default model (an `auto` thread
|
||||
stays `auto`). The override is always preflighted and is part of the
|
||||
`operation_key` fingerprint.
|
||||
|
||||
Account-owned model turns require `capabilities.account_model_owner: true` before
|
||||
submission. Set `account_model_owner` to the signed-in account ID, both
|
||||
`model_provider` and `model_provider_id` to `codewhale`, and `model` to the exact
|
||||
`provider/model` account route. The Runtime requires current model access for
|
||||
that same secure account session and uses only that account credential; missing,
|
||||
expired or revoked access is refused. Local keys and other configured routes are
|
||||
not substitutes. A supplied profile constitution must belong to the same account.
|
||||
The owner participates in the durable operation fingerprint; replay of an
|
||||
already completed exact operation returns its receipt before fresh credential
|
||||
admission. The account gateway resolves the selected provider's current saved
|
||||
API key at use. Ordinary turns and saved Chat defaults retain their existing
|
||||
behavior. The Runtime Chat relay carries the same assertion as `accountModelOwner`.
|
||||
|
||||
Resolution is deterministic: a turn override wins over the thread default,
|
||||
which wins over the Runtime's normal configuration. For tools, reaching normal
|
||||
configuration means the ordinary configured catalog; `[]` is never treated as
|
||||
|
||||
+1
-1
@@ -477,7 +477,7 @@ owner = "crates/tui/src/lsp"
|
||||
[[feature]]
|
||||
id = "runtime-api"
|
||||
name = "Runtime API"
|
||||
summary = "Local HTTP API for threads, events and approvals, with scoped watch/drive device tokens."
|
||||
summary = "Local HTTP API for threads, events and approvals, with scoped watch/drive device tokens and account-bound model turns."
|
||||
status = "stable"
|
||||
surfaces = { api = "stable" }
|
||||
docs = "docs/RUNTIME_API.md"
|
||||
|
||||
@@ -125,7 +125,10 @@ try {
|
||||
desktop.stdout.on('data', data => { void fs.appendFile(path.join(evidence, 'desktop.log'), data); });
|
||||
desktop.stderr.on('data', data => { desktopError += data; void fs.appendFile(path.join(evidence, 'desktop-error.log'), data); });
|
||||
desktopExit = new Promise((resolve, reject) => { desktop.once('error', reject); desktop.once('exit', resolve); });
|
||||
await until(() => exists('ready.json'), 'Windows Terminal composer readiness', 120_000);
|
||||
receipt.readyAt = new Date().toISOString();
|
||||
await until(() => exists('pasted.json'), 'actual clipboard paste');
|
||||
receipt.pastedAt = new Date().toISOString();
|
||||
assert.equal(requests.length, 0, 'pasting must not submit any partial turn');
|
||||
receipt.requestsBeforeEnter = 0;
|
||||
await fs.writeFile(path.join(evidence, 'submit'), 'enter once');
|
||||
|
||||
@@ -76,6 +76,8 @@ try {
|
||||
Focus-OwnedWindow
|
||||
Wait-For { (Read-Terminal).Contains('Type a message') } 'Codewhale composer'
|
||||
Capture 'ready'
|
||||
@{ processId = $ownedPid; readyAt = [DateTime]::UtcNow.ToString('o') } | ConvertTo-Json |
|
||||
Set-Content -Encoding UTF8 (Join-Path $Evidence 'ready.json')
|
||||
$payload = [System.IO.File]::ReadAllText((Join-Path $Evidence 'expected.txt'), [System.Text.Encoding]::UTF8)
|
||||
[System.Windows.Forms.Clipboard]::SetText($payload)
|
||||
if ([System.Windows.Forms.Clipboard]::GetText() -ne $payload) { throw 'Clipboard readback mismatch' }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# Shared test-home boundary for local and CI workspace tests.
|
||||
# Rust toolchain homes stay real.
|
||||
# Rust toolchain homes and build cache stay real.
|
||||
set -eu
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
@@ -10,11 +10,15 @@ fi
|
||||
|
||||
real_cargo_home=${CARGO_HOME:-${HOME}/.cargo}
|
||||
real_rustup_home=${RUSTUP_HOME:-${HOME}/.rustup}
|
||||
real_build_cache=${CODEWHALE_CACHE_ROOT:-${XDG_CACHE_HOME:-${HOME}/.cache}/codewhale}
|
||||
real_build_lock=${CODEWHALE_BUILD_LOCK_FILE:-${real_build_cache}/build.lock}
|
||||
# Git Bash hands MSYS paths to native Windows processes, which cannot
|
||||
# resolve them. `-m` keeps forward slashes, so MSYS tools still work.
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
real_cargo_home=$(cygpath -m "$real_cargo_home")
|
||||
real_rustup_home=$(cygpath -m "$real_rustup_home")
|
||||
real_build_cache=$(cygpath -m "$real_build_cache")
|
||||
real_build_lock=$(cygpath -m "$real_build_lock")
|
||||
fi
|
||||
|
||||
rustc_bin=$(RUSTUP_HOME="$real_rustup_home" rustup which rustc)
|
||||
@@ -52,6 +56,8 @@ mkdir -p "$test_home_root/kimi-share" "$test_home_root/claude"
|
||||
unset CODEWHALE_HOME CODEWHALE_CONFIG_PATH DEEPSEEK_CONFIG_PATH DEEPSEEK_HOME
|
||||
|
||||
env \
|
||||
CODEWHALE_CACHE_ROOT="$real_build_cache" \
|
||||
CODEWHALE_BUILD_LOCK_FILE="$real_build_lock" \
|
||||
HOME="$test_home_root/home" \
|
||||
USERPROFILE="$test_home_root/home" \
|
||||
APPDATA="$test_home_root/home/AppData/Roaming" \
|
||||
|
||||
Reference in new issue
Block a user