diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 161acd9c59..8f80226bda 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- **GitHub Action reviews:** analysis can export a structured Engine review for a separate trusted publisher job. The publisher checks the current PR revision and line anchors, deduplicates its findings, and keeps uncertain or preexisting claims out of inline comments. +- **Telegram recovery:** prompts and their initiating identity are durable before the polling cursor advances. Restart recovery reuses the original Engine operation, questions preserve human ownership, and uncertain final replies remain available through `/status` without an automatic resend. +- **DingTalk bridge:** an optional internal-app Stream adapter reuses the same Engine, shared admission and approval helpers. Tenant/staff allowlists, durable acceptance before acknowledgement and retained uncertain deliveries protect recovery. Managed account linking is separate from this advanced self-hosted bridge. + - **Answerless output-limit recovery:** opt into `[reasoning_only] on_output_limit = true` to request an answer again after a length stop with no answer or tool call, including hidden reasoning and ordinary Fleet workers. Retries share `max_reprompts`, retain existing output and control limits, and record request-scoped nudge receipts without adding them to resumed conversation context. RLM keeps its existing stop protocol. Thanks [@M-Maciej](https://github.com/M-Maciej) for the long-proof Fleet report. (#6990) - **Linux hardening tests:** the NoNewPrivs child matrices now respect a parent that already holds the irreversible flag, keeping the full assertions on unpinned hosts and checking the kernel ratchet on pinned hosts. Thanks [@asto18089](https://github.com/asto18089) for diagnosing the hardened-container failure. (#7021) diff --git a/crates/tui/src/approval_log.rs b/crates/tui/src/approval_log.rs index 579980151b..bb8456034c 100644 --- a/crates/tui/src/approval_log.rs +++ b/crates/tui/src/approval_log.rs @@ -463,7 +463,7 @@ impl ApprovalReceiptStore { if create { fs::create_dir_all(&self.sessions_dir)?; } - WorkspaceFile::open( + WorkspaceFile::open_resolved_root( &self.sessions_dir, &PathBuf::from(session_id).join(APPROVAL_LOG_FILE), create, diff --git a/crates/tui/src/session_export.rs b/crates/tui/src/session_export.rs index 66cdefc2b2..d98695ff7a 100644 --- a/crates/tui/src/session_export.rs +++ b/crates/tui/src/session_export.rs @@ -215,7 +215,7 @@ pub fn write_session_archive( .expect("artifact collection needs a store"); // Parent directories and the leaf are opened without following links. // Stream this handle directly; never reopen a validated path. - let mut file = WorkspaceFile::open(root, relative, false)?.open_file()?; + let mut file = WorkspaceFile::open_resolved_root(root, relative, false)?.open_file()?; append_member( &mut tar, name, @@ -278,7 +278,11 @@ pub fn session_artifacts_dir(sessions_dir: &Path, session_id: &str) -> io::Resul let relative = Path::new(session_id).join(ARTIFACTS_DIR_NAME); // Opening a confined file reference pins and validates its parent directory; // this checks the artifact root without creating or opening the dummy leaf. - match WorkspaceFile::open(sessions_dir, &relative.join(".export-root-check"), false) { + match WorkspaceFile::open_resolved_root( + sessions_dir, + &relative.join(".export-root-check"), + false, + ) { Ok(_) => Ok(Some(sessions_dir.join(relative))), Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), Err(error) => Err(error), @@ -392,7 +396,11 @@ fn collect_artifact_files_recursive( if file_type.is_dir() { // Reject substituted parent directories before descending. The file // read repeats confinement checks, so directory races cannot leak bytes. - WorkspaceFile::open(sessions_dir, &relative.join(".export-root-check"), false)?; + WorkspaceFile::open_resolved_root( + sessions_dir, + &relative.join(".export-root-check"), + false, + )?; collect_artifact_files_recursive( sessions_dir, &path, diff --git a/crates/tui/src/tui/clipboard.rs b/crates/tui/src/tui/clipboard.rs index 410d2444d4..10ce671a73 100644 --- a/crates/tui/src/tui/clipboard.rs +++ b/crates/tui/src/tui/clipboard.rs @@ -962,7 +962,19 @@ fn clipboard_image_target( let name = dir .file_name() .context("clipboard-images dir has no name")?; - crate::fleet::files::WorkspaceFile::open(root, &Path::new(name).join(file_name), true) + let relative = Path::new(name).join(file_name); + // `dir` sits under the user's own state root unless no home was available + // and it fell back to the workspace (`clipboard_images_dir_for_home`). Only + // the state root may be a link the user made to relocate `~/.codewhale`; a + // caller-supplied workspace root keeps the strict rule. + let state_root = codewhale_paths::user_home() + .is_some_and(|home| root == home.join(codewhale_paths::CODEWHALE_APP_DIR)); + let open = if state_root { + crate::fleet::files::WorkspaceFile::open_resolved_root + } else { + crate::fleet::files::WorkspaceFile::open + }; + open(root, &relative, true) .context("open clipboard-images destination (links are not followed)") } diff --git a/crates/tui/src/tui/file_mention.rs b/crates/tui/src/tui/file_mention.rs index 03bb605c6c..2e92d053f2 100644 --- a/crates/tui/src/tui/file_mention.rs +++ b/crates/tui/src/tui/file_mention.rs @@ -812,8 +812,19 @@ fn stabilize_screencapture_file(path: &Path, artifact_dir: &Path) -> Option io::Result { } pub fn descriptor(root: &Path) -> io::Result { - let file = WorkspaceFile::open(root, Path::new("connection.json"), false)?.open_file()?; + let file = WorkspaceFile::open_resolved_root(root, Path::new("connection.json"), false)? + .open_file()?; let mut text = String::new(); file.take(4097).read_to_string(&mut text)?; if text.len() > 4096 { @@ -312,7 +313,7 @@ pub fn serve(root: PathBuf, requested_port: u16) -> anyhow::Result<()> { } std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700))?; } - let lock_path = WorkspaceFile::open(&root, Path::new("owner.lock"), true)?; + let lock_path = WorkspaceFile::open_resolved_root(&root, Path::new("owner.lock"), true)?; let original = lock_path.open_update(true, false)?; let mut lifetime = fd_lock::RwLock::new(original.try_clone()?); let _guard = lifetime @@ -360,7 +361,7 @@ pub fn serve(root: PathBuf, requested_port: u16) -> anyhow::Result<()> { token: saved.token.clone(), identity: saved.identity.clone(), }; - let connection = WorkspaceFile::open(&root, Path::new("connection.json"), true)?; + let connection = WorkspaceFile::open_resolved_root(&root, Path::new("connection.json"), true)?; let epoch = uuid::Uuid::new_v4().to_string(); let (tx, rx) = mpsc::sync_channel(128); let frames = Arc::new(Mutex::new(VecDeque::new())); diff --git a/crates/tui/src/tui/pet_watch/persistence.rs b/crates/tui/src/tui/pet_watch/persistence.rs index 53a0821811..e7e377195e 100644 --- a/crates/tui/src/tui/pet_watch/persistence.rs +++ b/crates/tui/src/tui/pet_watch/persistence.rs @@ -26,8 +26,8 @@ pub struct Store { impl Store { /// Shared presentation-owner state. Legacy session habitats stay in place. pub fn at(root: &Path) -> io::Result { - let data = WorkspaceFile::open(root, Path::new("habitat.json"), true)?; - let lock = WorkspaceFile::open(root, Path::new("habitat.lock"), true)?; + let data = WorkspaceFile::open_resolved_root(root, Path::new("habitat.json"), true)?; + let lock = WorkspaceFile::open_resolved_root(root, Path::new("habitat.lock"), true)?; let original_lock = lock.open_update(true, false)?; Ok(Self { data,