Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/tui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/src/approval_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 11 additions & 3 deletions crates/tui/src/session_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 13 additions & 1 deletion crates/tui/src/tui/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}

Expand Down
15 changes: 13 additions & 2 deletions crates/tui/src/tui/file_mention.rs
Original file line number Diff line number Diff line change
Expand Up @@ -812,8 +812,19 @@ fn stabilize_screencapture_file(path: &Path, artifact_dir: &Path) -> Option<Path
let name = stable_attachment_name(path.file_name()?);
let dest = artifact_dir.join(&name);
let relative = Path::new(artifact_dir.file_name()?).join(&name);
let target =
crate::fleet::files::WorkspaceFile::open(artifact_dir.parent()?, &relative, true).ok()?;
let root = artifact_dir.parent()?;
// `artifact_dir` sits under the user's own state root unless no home was
// available and it fell back to the workspace
// (`screenshot_stabilization_dir`). Only the state root may be a link the
// user made to relocate `~/.codewhale`.
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
};
let target = open(root, &relative, true).ok()?;
// An existing regular destination is reused; a link there fails to open.
if target.open_file().is_ok() {
return Some(dest);
Expand Down
7 changes: 4 additions & 3 deletions crates/tui/src/tui/pet_watch/owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ pub fn directory() -> io::Result<PathBuf> {
}

pub fn descriptor(root: &Path) -> io::Result<Descriptor> {
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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()));
Expand Down
4 changes: 2 additions & 2 deletions crates/tui/src/tui/pet_watch/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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,
Expand Down
Loading