Skip to content

Propose dont apply#37

Merged
themartto merged 42 commits into
devfrom
propose-dont-apply
Jul 4, 2026
Merged

Propose dont apply#37
themartto merged 42 commits into
devfrom
propose-dont-apply

Conversation

@themartto

@themartto themartto commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added interactive permission prompts for tool and shell actions, with allow once/always and deny options.
    • Introduced a new WebSocket endpoint for direct ACP messaging and expanded session history persistence.
    • Added support for richer message content, including images, thinking blocks, and structured tool results.
  • Bug Fixes
    • Improved filesystem safety by enforcing work directory boundaries and stronger path validation.
    • Fixed session cancellation, prompt concurrency issues, and several model/provider handling edge cases.
  • Documentation
    • Updated setup and API docs to reflect the new WebSocket, history, and permission behaviors.

themartto added 30 commits July 2, 2026 06:14
@themartto

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR replaces the string-based Message/tool-call model with typed ContentBlocks, adds PermissionGate and TurnContext for cancellation and tool-call approval threaded through the agent loop and tool executors, introduces ACP session modes/permission-request/cancel flows, splits conversation history into metadata+append-only logs, sandboxes the /ws filesystem sidecar, adds a bare /acp endpoint, and adds a TUI permission prompt.

Changes

Core Model, Permission/Turn Context, and LLM Providers

Layer / File(s) Summary
ContentBlock/Message model
src/core/models.rs, src/lib.rs
Replaces the wire-format Message with typed content blocks (Text/Thinking/Image/ToolUse/ToolResult), adds FinishReason/StopReason, and updates StreamEvent and crate re-exports.
Permission gate & turn context
src/core/permission.rs, src/core/turn.rs, src/core/client_io.rs, src/core/mod.rs
Adds PermissionDecision/PermissionGate/AllowAll, ClientIo/NoClientIo, and TurnContext combining cancellation and permission gating.
Agent loop integration
src/core/agent.rs
Threads TurnContext through run_agent_loop, races LLM calls against cancellation, routes tool execution through permission checks, and tracks stop_reason/iterations_used.
Tool executors and delegate
src/tools/mod.rs, src/tools/sandboxed_executor.rs, src/tools/scoped_executor.rs, src/tools/delegate.rs
Updates ToolExecutor::execute and SandboxedExecutor/ScopedExecutor/DelegateTool to accept TurnContext, with subagents inheriting parent cancellation/permission gate.
LLM providers
src/core/llm/anthropic.rs, src/core/llm/gemini.rs, src/core/llm/openai.rs, src/core/llm/mod.rs, src/core/llm/retry.rs
Converts request/response handling to ContentBlock/FinishReason, adds Anthropic thinking-block replay, and updates tests.
Prompt builder & error variants
src/rag/prompt.rs, src/rag/skills.rs, src/error.rs
Builds system messages via ContentBlock::Text and adds Error::NotFound/JoinError variants.

Estimated code review effort: 5 (Critical) | ~150 minutes

ACP Protocol, Session, and Client Library

Layer / File(s) Summary
Session modes and state
src/acp/mod.rs, src/acp/session.rs
Adds AgentMode, expands SessionState with cancellation/approved_tools/prompt_lock, and adds cancel_session/acp_set_session_mode.
Prompt turn flow
src/acp/mod.rs
Rewires acp_prompt with prompt-lock acquisition, TurnContext, streaming events, and durable checkpoints.
Permission gate & client I/O
src/acp/mod.rs
Implements AcpPermissionGate/AcpClientIo, approval_key scoping, and convert_prompt_blocks.
Client facade
src/client.rs
Makes session accessors async, adds OpenheimBuilder::tool and SessionHandle permission_gate/client_io/cancel.
Transport wiring
src/transport/run.rs, src/transport/stdio.rs
Passes custom_tools to AgentState::new and auto-responds to permission requests in headless mode.

Estimated code review effort: 5 (Critical) | ~120 minutes

History Durability and Config Defaults

Layer / File(s) Summary
Split history persistence
src/rag/history.rs
Splits conversation storage into .json metadata and .jsonl append-only message logs with atomic writes.
Provider defaults
src/config/mod.rs, src/config/resolve.rs, src/config/types.rs, src/config/config.toml.default
Adds builtin_provider_defaults/resolve_provider_default helpers and updates shipped default models.

Estimated code review effort: 3 (Moderate) | ~30 minutes

WebSocket Transport Sandboxing

Layer / File(s) Summary
/ws and /acp routing
src/transport/ws.rs
Adds the bare /acp endpoint alongside /ws with updated documentation and DTOs.
History HTTP handlers
src/transport/ws.rs
Moves history reads to spawn_blocking with NotFound-to-404 mapping.
FsState sandboxing
src/transport/ws.rs
Validates all filesystem operations against work_dir via a shared validator with new tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

TUI Permission Prompt

Layer / File(s) Summary
TuiPermissionGate
src/tui/types.rs, src/tui/permission.rs
Adds PermissionPrompt screen and a channel-based PermissionGate implementation.
App wiring and rendering
src/tui/app.rs, src/tui/render.rs, src/tui/mod.rs
Wires the permission channel into the event loop and renders a selectable permission overlay.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Documentation and Changelog

Layer / File(s) Summary
Changelog
CHANGELOG.md
Documents security, protocol, feature, fix, and breaking-change entries.
Docs pages
README.md, docs/*
Updates docs to describe the content-block schema, sandboxed fs channel, /acp endpoint, permission gating, and history layout.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AgentState
  participant PromptLock
  participant AgentLoop as run_agent_streaming_with_history
  participant PermissionGate
  participant History as HistoryManager

  Client->>AgentState: session/prompt
  AgentState->>PromptLock: try_acquire_prompt_lock
  AgentState->>History: persist_conversation (pre-turn)
  AgentState->>AgentLoop: run(TurnContext)
  AgentLoop->>PermissionGate: check(tool_call)
  PermissionGate-->>AgentLoop: decision
  AgentLoop-->>AgentState: StreamEvent updates
  AgentState->>History: persist_conversation (final)
  AgentState-->>Client: PromptResponse(stop_reason)
Loading
sequenceDiagram
  participant WsClient as /ws Client
  participant FsState
  participant Disk as Filesystem

  WsClient->>FsState: FsRequest{path}
  FsState->>FsState: validate(path, work_dir)
  alt inside work_dir
    FsState->>Disk: perform operation
    Disk-->>FsState: result
    FsState-->>WsClient: FsResponse::Success
  else outside work_dir
    FsState-->>WsClient: FsResponse::Error
  end
Loading

Possibly related PRs

Poem

A rabbit hopped through blocks of text,
Thinking, tool-use, images next 🐇
Turns now cancel, gates now guard,
History logs append, not hard.
Sandboxed burrows, safe and sound —
Hop, hop, hooray, new ground! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is too vague to convey the main change in the pull request. Replace it with a concise, specific summary of the primary change, such as the main protocol, API, or behavior update.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/tools/delegate.rs (1)

51-70: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Subagent I/O still bypasses ACP client_io

DelegateTool::build_executor hardcodes Arc::new(NoClientIo), so subagents always read and write local disk even when the parent turn is using ACP-backed client_io. That can miss unsaved editor buffers or write outside the editor’s authoritative filesystem view. Thread client_io through DelegateTool (or TurnContext) and pass it to SandboxedExecutor instead of NoClientIo.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/delegate.rs` around lines 51 - 70, DelegateTool::build_executor is
still hardcoding NoClientIo, so subagents bypass the parent ACP client_io and
operate on local disk instead of the editor-backed filesystem. Thread the
existing client_io through DelegateTool (or TurnContext) by storing it in
DelegateTool::new and passing it along when constructing SandboxedExecutor,
replacing the NoClientIo fallback so subagent I/O uses the same authoritative
client_io as the parent turn.
src/tools/sandboxed_executor.rs (1)

69-119: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Architect mode still bypasses the allowlist for built-in tools

src/tools/sandboxed_executor.rs:69-116 intercepts write_file/execute_command before ScopedExecutor can reject them, and src/core/agent.rs does not re-check ToolUse.name against the offered tool list before dispatch. In architect mode, a hallucinated or injected write_file call still runs; execute_command is affected too when shell is enabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/sandboxed_executor.rs` around lines 69 - 119, The sandboxed
built-in tool handling is bypassing the allowed tool list, so a hallucinated
`write_file` or `execute_command` can still run. Update
`SandboxedExecutor::execute` to reject built-in names unless they were
explicitly offered, and add a pre-dispatch validation in `Agent` before calling
the executor. Use the existing `ToolUse.name`, `ScopedExecutor`, and
`SandboxedExecutor::execute` flow to enforce the allowlist consistently for
built-ins and fall back only for permitted tools.
🧹 Nitpick comments (2)
src/transport/ws.rs (2)

248-260: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Errors are swallowed into generic 500s without logging.

Both handlers collapse spawn_blocking join failures and underlying history errors into a bare "failed to load ..." 500 via _ =>, discarding the actual error. Logging it (e.g., tracing::warn!) before returning would materially help diagnosing production issues without changing the response contract.

Also applies to: 262-290

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/transport/ws.rs` around lines 248 - 260, The sessions_handler and the
related history-loading handler currently map all spawn_blocking and history
failures to a generic 500 via the _ match arm, which hides the real cause.
Update the match around tokio::task::spawn_blocking in sessions_handler (and the
sibling handler in the same ws transport module) to log both the join error and
the inner history error with tracing before returning the existing JSON error
response. Keep the response contract unchanged, but include enough context in
the log to identify which operation failed.

299-374: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate ACP-bridge wiring between /ws and /acp.

handle_socket and handle_acp_socket both build the acp_out_tx/acp_in_tx channels, wrap acp_out_tx in a sink, and tokio::spawn(acp::serve(Lines::new(...), state)) identically. Consider extracting this into a shared helper (returning the two ends the caller-specific outbound loop needs) to avoid the two copies drifting apart.

fn spawn_acp_bridge(
    state: Arc<AgentState>,
) -> (mpsc::UnboundedReceiver<String>, mpsc::UnboundedSender<std::io::Result<String>>) {
    let (acp_out_tx, acp_out_rx) = mpsc::unbounded::<String>();
    let (acp_in_tx, acp_in_rx) = mpsc::unbounded::<std::io::Result<String>>();
    let acp_sink = acp_out_tx
        .sink_map_err(|e| std::io::Error::new(std::io::ErrorKind::BrokenPipe, e.to_string()));
    tokio::spawn(acp::serve(Lines::new(acp_sink, acp_in_rx), state));
    (acp_out_rx, acp_in_tx)
}

Also applies to: 376-414

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/transport/ws.rs` around lines 299 - 374, `handle_socket` duplicates the
ACP bridge setup that also exists in `handle_acp_socket`, so extract the shared
channel/sink/spawn logic into a helper like `spawn_acp_bridge` and reuse it from
both paths. Move the `acp_out_tx/acp_in_tx` creation, `sink_map_err` wrapping,
and `tokio::spawn(acp::serve(Lines::new(...), state))` into that helper,
returning the receiver and sender each caller needs. Keep the caller-specific
outbound loops in `handle_socket` and `handle_acp_socket` unchanged except for
wiring them to the helper’s վերադարձed ends.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/acp/mod.rs`:
- Around line 63-66: The `Architect` variant’s doc comment is incorrect because
`run_agent_loop` still routes `read_file` through
`turn.permission_gate.check(...)`, and `AcpPermissionGate` can emit
`session/request_permission` unless the decision is already cached. Update the
comment near `Architect` in `mod.rs` to say it only limits the tool list to
`read_file` and does not guarantee no permission prompt, or change
`run_agent_loop`/`AcpPermissionGate` to explicitly bypass the gate for read-only
tools if that is the intended behavior.

In `@src/core/agent.rs`:
- Around line 159-185: The permission approval path in agent::turn is still
blocking independently of cancellation, so a pending turn can remain stuck in
turn.permission_gate.check while waiting for user approval. Update the tool-call
loop in Agent::run/handle_turn to race the permission_gate.check future against
turn.cancel (or otherwise abort the approval wait on cancel), and ensure any
pending approval flow is released when cancellation wins so prompt_lock is not
held until the gate resolves.

In `@src/tools/sandboxed_executor.rs`:
- Around line 78-81: The file I/O path in SandboxedExecutor still waits on
client_io directly, so slow fs/read_text_file and fs/write_text_file requests
can block a turn. Update the relevant read_file/write_file handling in
SandboxedExecutor to race the client_io await against turn.cancel using
tokio::select! (or an equivalent timeout), and return early on cancellation so
session/cancel can interrupt unresponsive clients.

---

Outside diff comments:
In `@src/tools/delegate.rs`:
- Around line 51-70: DelegateTool::build_executor is still hardcoding
NoClientIo, so subagents bypass the parent ACP client_io and operate on local
disk instead of the editor-backed filesystem. Thread the existing client_io
through DelegateTool (or TurnContext) by storing it in DelegateTool::new and
passing it along when constructing SandboxedExecutor, replacing the NoClientIo
fallback so subagent I/O uses the same authoritative client_io as the parent
turn.

In `@src/tools/sandboxed_executor.rs`:
- Around line 69-119: The sandboxed built-in tool handling is bypassing the
allowed tool list, so a hallucinated `write_file` or `execute_command` can still
run. Update `SandboxedExecutor::execute` to reject built-in names unless they
were explicitly offered, and add a pre-dispatch validation in `Agent` before
calling the executor. Use the existing `ToolUse.name`, `ScopedExecutor`, and
`SandboxedExecutor::execute` flow to enforce the allowlist consistently for
built-ins and fall back only for permitted tools.

---

Nitpick comments:
In `@src/transport/ws.rs`:
- Around line 248-260: The sessions_handler and the related history-loading
handler currently map all spawn_blocking and history failures to a generic 500
via the _ match arm, which hides the real cause. Update the match around
tokio::task::spawn_blocking in sessions_handler (and the sibling handler in the
same ws transport module) to log both the join error and the inner history error
with tracing before returning the existing JSON error response. Keep the
response contract unchanged, but include enough context in the log to identify
which operation failed.
- Around line 299-374: `handle_socket` duplicates the ACP bridge setup that also
exists in `handle_acp_socket`, so extract the shared channel/sink/spawn logic
into a helper like `spawn_acp_bridge` and reuse it from both paths. Move the
`acp_out_tx/acp_in_tx` creation, `sink_map_err` wrapping, and
`tokio::spawn(acp::serve(Lines::new(...), state))` into that helper, returning
the receiver and sender each caller needs. Keep the caller-specific outbound
loops in `handle_socket` and `handle_acp_socket` unchanged except for wiring
them to the helper’s վերադարձed ends.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 82875e89-68be-4cf4-be21-c3e3f0ff43dd

📥 Commits

Reviewing files that changed from the base of the PR and between 3a4eaae and 8387e8d.

📒 Files selected for processing (45)
  • CHANGELOG.md
  • README.md
  • docs/api.md
  • docs/architecture.md
  • docs/configuration.md
  • docs/custom-llm-provider.md
  • docs/custom-tools.md
  • docs/deployment.md
  • docs/library.md
  • docs/subagents.md
  • src/acp/mod.rs
  • src/acp/session.rs
  • src/client.rs
  • src/config/config.toml.default
  • src/config/mod.rs
  • src/config/resolve.rs
  • src/config/types.rs
  • src/core/agent.rs
  • src/core/client_io.rs
  • src/core/llm/anthropic.rs
  • src/core/llm/gemini.rs
  • src/core/llm/mod.rs
  • src/core/llm/openai.rs
  • src/core/llm/retry.rs
  • src/core/mod.rs
  • src/core/models.rs
  • src/core/permission.rs
  • src/core/turn.rs
  • src/error.rs
  • src/lib.rs
  • src/rag/history.rs
  • src/rag/prompt.rs
  • src/rag/skills.rs
  • src/tools/delegate.rs
  • src/tools/mod.rs
  • src/tools/sandboxed_executor.rs
  • src/tools/scoped_executor.rs
  • src/transport/run.rs
  • src/transport/stdio.rs
  • src/transport/ws.rs
  • src/tui/app.rs
  • src/tui/mod.rs
  • src/tui/permission.rs
  • src/tui/render.rs
  • src/tui/types.rs

Comment thread src/acp/mod.rs Outdated
Comment on lines +63 to +66
/// Read-only: only `read_file` is offered to the LLM, regardless of
/// permission decisions. No `session/request_permission` prompts occur
/// since nothing mutating is ever on the tool list.
Architect,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm read_file is not special-cased/auto-allowed in the permission path.
rg -nP -C3 '\bread_file\b' src/acp/mod.rs src/core/agent.rs src/tools/ | rg -n -i 'permission|allow|check|approv|read_file'
# Show the gate.check call site to confirm all tool calls are gated.
rg -nP -C4 'permission_gate\.check' src/core/agent.rs

Repository: weirdstuff-dev/openheim

Length of output: 6987


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Architect mode docs and the tool/permission flow around the relevant call sites.
sed -n '55,80p' src/acp/mod.rs
printf '\n---\n'
sed -n '160,205p' src/core/agent.rs
printf '\n---\n'
sed -n '730,780p' src/acp/mod.rs
printf '\n---\n'
rg -n -C3 'session/request_permission|request_permission|PermissionGate|check\(' src/acp/mod.rs src/core/agent.rs src -g '!target'

Repository: weirdstuff-dev/openheim

Length of output: 29049


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the ACP permission gate implementation and the Architect tool list setup.
sed -n '640,730p' src/acp/mod.rs
printf '\n---\n'
sed -n '300,330p' src/acp/mod.rs
printf '\n---\n'
rg -n -C2 'Architect|read_file|session/request_permission|AllowAlways|RejectAlways' src/acp/mod.rs src/acp/session.rs src/core/permission.rs

Repository: weirdstuff-dev/openheim

Length of output: 12479


Architect still prompts on read_file. run_agent_loop calls turn.permission_gate.check(...) for every tool call, and AcpPermissionGate sends session/request_permission unless the decision is already remembered. Since Architect only narrows the tool list to read_file, the “No session/request_permission prompts occur” line is inaccurate. Reword it, or bypass the gate for read-only tools if that’s the intended behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/acp/mod.rs` around lines 63 - 66, The `Architect` variant’s doc comment
is incorrect because `run_agent_loop` still routes `read_file` through
`turn.permission_gate.check(...)`, and `AcpPermissionGate` can emit
`session/request_permission` unless the decision is already cached. Update the
comment near `Architect` in `mod.rs` to say it only limits the tool list to
`read_file` and does not guarantee no permission prompt, or change
`run_agent_loop`/`AcpPermissionGate` to explicitly bypass the gate for read-only
tools if that is the intended behavior.

Comment thread src/core/agent.rs
Comment on lines +159 to 185
for tool_call in &tool_calls {
if turn.cancel.is_cancelled() {
stop_reason = StopReason::Cancelled;
break;
}

let id = &tool_call.id;
let tool_name = &tool_call.name;
let arguments = &tool_call.arguments;

if let Some(cb) = callback.as_mut() {
cb(StreamEvent::ToolCall {
id: id.clone(),
tool_name: tool_name.clone(),
arguments: arguments.clone(),
});
}

let (result, is_error) = match tool_executor.execute(tool_name, arguments).await {
Ok(r) => (r, false),
Err(e) => (format!("Error: {e}"), true),
let decision = turn.permission_gate.check(id, tool_name, arguments).await;
let (result, is_error) = if decision.is_allowed() {
match tool_executor.execute(tool_name, arguments, turn).await {
Ok(r) => (r, false),
Err(e) => (format!("Error: {e}"), true),
}
} else {
("Permission denied by user.".to_string(), true)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether PermissionGate implementations race against cancellation internally.
rg -n -A 20 'impl PermissionGate for' --type=rust
rg -n -B3 -A 15 'trait PermissionGate' --type=rust

Repository: weirdstuff-dev/openheim

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- files mentioning permission_gate / PermissionGate ---'
rg -n --hidden --glob '!target' 'PermissionGate|permission_gate' .

echo '--- agent.rs around cited lines ---'
sed -n '130,230p' src/core/agent.rs

echo '--- acp mod around prompt_guard / cancellation ---'
sed -n '1,260p' src/acp/mod.rs

Repository: weirdstuff-dev/openheim

Length of output: 21122


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- core permission trait/impls ---'
sed -n '1,220p' src/core/permission.rs

echo '--- ACP permission gate ---'
sed -n '620,720p' src/acp/mod.rs

echo '--- TUI permission gate ---'
sed -n '1,220p' src/tui/permission.rs

echo '--- delegate/subagent permission plumbing ---'
sed -n '200,340p' src/tools/delegate.rs

Repository: weirdstuff-dev/openheim

Length of output: 13604


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- acp_prompt / prompt_lock / cancel handling ---'
rg -n -A 30 -B 10 'acp_prompt|prompt_lock|prompt_guard|session/request_permission|cancel_session|CancelNotification|CancellationToken' src/acp/mod.rs src/acp/session.rs

Repository: weirdstuff-dev/openheim

Length of output: 32200


Race the permission await with cancellation. turn.permission_gate.check(...) still waits independently of turn.cancel, and the concrete ACP/TUI gates don’t observe that token themselves. If session/cancel arrives while approval is pending, the turn can stay stuck holding prompt_lock until the gate returns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/agent.rs` around lines 159 - 185, The permission approval path in
agent::turn is still blocking independently of cancellation, so a pending turn
can remain stuck in turn.permission_gate.check while waiting for user approval.
Update the tool-call loop in Agent::run/handle_turn to race the
permission_gate.check future against turn.cancel (or otherwise abort the
approval wait on cancel), and ensure any pending approval flow is released when
cancellation wins so prompt_lock is not held until the gate resolves.

Comment thread src/tools/sandboxed_executor.rs Outdated
Comment on lines +78 to +81
match self.client_io.read_file(&validated).await {
Some(result) => result,
None => read_file(&validated).await,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== sandboxed_executor.rs ==\n'
nl -ba src/tools/sandboxed_executor.rs | sed -n '1,180p'

printf '\n== search: client_io + cancel/select ==\n'
rg -n "client_io\.read_file|client_io\.write_file|turn\.cancel|tokio::select!|cancel token|session/cancel|read_text_file|write_text_file" -S src CHANGELOG* . || true

printf '\n== relevant client_io definitions ==\n'
rg -n "trait .*ClientIo|struct .*ClientIo|read_file\(|write_file\(" -S src || true

Repository: weirdstuff-dev/openheim

Length of output: 230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== sandboxed_executor.rs (1-180) ==\n'
awk 'NR>=1 && NR<=180 { printf "%5d  %s\n", NR, $0 }' src/tools/sandboxed_executor.rs

printf '\n== search: client_io + cancel/select ==\n'
rg -n "client_io\.read_file|client_io\.write_file|turn\.cancel|tokio::select!|session/cancel|read_text_file|write_text_file|cancel token" -S src CHANGELOG* . || true

printf '\n== client_io definitions ==\n'
rg -n "trait .*ClientIo|struct .*ClientIo|read_file\(|write_file\(" -S src || true

Repository: weirdstuff-dev/openheim

Length of output: 21490


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== core/client_io.rs ==\n'
awk 'NR>=1 && NR<=120 { printf "%5d  %s\n", NR, $0 }' src/core/client_io.rs

printf '\n== acp/mod.rs (AcpClientIo) ==\n'
awk 'NR>=720 && NR<=790 { printf "%5d  %s\n", NR, $0 }' src/acp/mod.rs

printf '\n== acp/mod.rs nearby request handling ==\n'
awk 'NR>=930 && NR<=1085 { printf "%5d  %s\n", NR, $0 }' src/acp/mod.rs

printf '\n== turn/cancel context ==\n'
awk 'NR>=1 && NR<=120 { printf "%5d  %s\n", NR, $0 }' src/core/turn.rs

Repository: weirdstuff-dev/openheim

Length of output: 15152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== core/agent.rs (tool execution + cancel) ==\n'
awk 'NR>=70 && NR<=260 { printf "%5d  %s\n", NR, $0 }' src/core/agent.rs

printf '\n== search: block_task/send_request/cancel around ACP client I/O ==\n'
rg -n "block_task\(\)|send_request\(|CancellationToken|cancelled\(\)|tokio::select!|read_text_file|write_text_file" -S src/acp src/core src/tools || true

printf '\n== acp transport / request helper definitions ==\n'
rg -n "struct ConnectionTo|fn send_request|block_task\(" -S src || true

Repository: weirdstuff-dev/openheim

Length of output: 12805


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== block_task definitions/usages ==\n'
rg -n "block_task\(" -S src
printf '\n== send_request definition ==\n'
rg -n "fn send_request|struct .*Request|impl .*send_request" -S src

printf '\n== transport/run.rs ==\n'
awk 'NR>=1 && NR<=130 { printf "%5d  %s\n", NR, $0 }' src/transport/run.rs

printf '\n== acp/mod.rs around request permission call ==\n'
awk 'NR>=670 && NR<=705 { printf "%5d  %s\n", NR, $0 }' src/acp/mod.rs

Repository: weirdstuff-dev/openheim

Length of output: 8095


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== block_task definitions/usages ==\n'
rg -n "block_task\(" -S src

printf '\n== send_request definition ==\n'
rg -n "fn send_request|struct .*Request|impl .*send_request" -S src

printf '\n== transport/run.rs ==\n'
awk 'NR>=1 && NR<=130 { printf "%5d  %s\n", NR, $0 }' src/transport/run.rs

printf '\n== acp/mod.rs around request permission call ==\n'
awk 'NR>=670 && NR<=705 { printf "%5d  %s\n", NR, $0 }' src/acp/mod.rs

Repository: weirdstuff-dev/openheim

Length of output: 8095


Race ACP file I/O against turn.cancel

read_file and write_file await client_io directly here, so an fs/read_text_file / fs/write_text_file round trip can still block the turn until the client replies. Wrap these awaits in a tokio::select! with turn.cancel (or add a timeout) so session/cancel can interrupt a slow or unresponsive client.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/sandboxed_executor.rs` around lines 78 - 81, The file I/O path in
SandboxedExecutor still waits on client_io directly, so slow fs/read_text_file
and fs/write_text_file requests can block a turn. Update the relevant
read_file/write_file handling in SandboxedExecutor to race the client_io await
against turn.cancel using tokio::select! (or an equivalent timeout), and return
early on cancellation so session/cancel can interrupt unresponsive clients.

@themartto themartto marked this pull request as ready for review July 4, 2026 05:21
@themartto themartto merged commit f6dafc6 into dev Jul 4, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant