Skip to content

feat(cli): expose sealed WebSocket sessions - #25

Merged
copyleftdev merged 3 commits into
mainfrom
agent/websocket-cli-vertical-slice
Aug 8, 2026
Merged

copyleftdev merged 3 commits into
mainfrom
agent/websocket-cli-vertical-slice

Conversation

@copyleftdev

@copyleftdev copyleftdev commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

Closes #14

Summary

  • detect and inspect direct WebSocket session JSON/YAML through the existing CLI grammar
  • seal, store, load, and execute WebSocket plans by handle or path with exact grants and WebSocket policy fingerprints
  • retrieve bounded transcript, JSON, and binary payload evidence through kahea explain
  • document the finite-session CLI contract and stable terminal exit mapping

Validation

  • scripts/gates.sh
  • black-box CLI tests for text/binary success, normal close, denial, handshake failure, expectation failure, timeout, invalid source, override rejection, and bounded evidence selection

Summary by CodeRabbit

  • New Features

    • Added finite WebSocket session support for inspection, planning, and execution.
    • Supports text and binary messages, session transcripts, selective evidence retrieval, grants, secret injection, filtering, and pagination.
    • Added WebSocket-specific validation, policy enforcement, timeouts, and clear handling of denied or failed connections.
    • Direct WebSocket JSON and YAML sources are now supported.
  • Documentation

    • Expanded guidance for WebSocket sessions, grants, exit codes, evidence handling, and untrusted message content.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@copyleftdev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 45c4c773-5222-4e83-9845-c153b390007e

📥 Commits

Reviewing files that changed from the base of the PR and between 4f79f30 and 624e82d.

📒 Files selected for processing (2)
  • crates/kahea-exec/src/websocket.rs
  • crates/kahea/tests/cli.rs
📝 Walkthrough

Walkthrough

The CLI now supports finite WebSocket sources through inspection, planning, grant-controlled invocation, transcript and payload evidence, and documented terminal outcomes. The plan library adds operation inspection, identity validation, filtering, pagination, and risk metadata.

Changes

WebSocket CLI support

Layer / File(s) Summary
WebSocket inspection and identity validation
crates/kahea-plan/src/lib.rs
Adds WebSocket detection and inspection APIs. Inspection validates source identity, derives deterministic operation metadata, infers write risk, supports filtering and pagination, and shares validation with plan construction.
CLI inspection and plan wiring
crates/kahea/src/main.rs
Routes WebSocket sources through dedicated inspection and planning paths. The CLI rejects HTTP-specific overrides, validates requested operations, stores sealed plans, and emits WebSocket-specific errors.
WebSocket invocation and validation
crates/kahea/src/main.rs, crates/kahea/tests/cli.rs, crates/kahea/Cargo.toml, README.md
Selects WebSocket policy fingerprints, executes stored plans, reports observations and denials, and handles non-terminal results. Integration tests cover successful exchanges, grants, evidence, timeouts, handshakes, expectations, and invalid plans. README documents the WebSocket flow and supported source formats.

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

Possibly related issues

Possibly related PRs

  • copyleftdev/kahea#20 — Introduces WebSocket contracts, plans, observations, and validation types used by this change.
  • copyleftdev/kahea#22 — Adds the WebSocket transport and execution APIs integrated by the CLI.
  • copyleftdev/kahea#23 — Adds WebSocket execution integration extended here with CLI inspection, planning, and end-to-end tests.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant StoredPlan
  participant execute_websocket
  participant WebSocketServer
  CLI->>StoredPlan: Load WebSocket plan
  CLI->>execute_websocket: Invoke with grants and policy fingerprint
  execute_websocket->>WebSocketServer: Connect and exchange bounded actions
  execute_websocket-->>CLI: Return observations, denials, and exit status
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: exposing sealed WebSocket sessions through the CLI.
Linked Issues check ✅ Passed The changes implement WebSocket inspect, plan, invoke, and explain support with integration tests for the required session outcomes and evidence selection.
Out of Scope Changes check ✅ Passed The documented code, dependencies, and tests directly support the linked WebSocket CLI objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/websocket-cli-vertical-slice

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.

@copyleftdev
copyleftdev marked this pull request as ready for review August 8, 2026 16:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
crates/kahea-plan/src/lib.rs (2)

2501-2510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the cursor == count boundary case.

The test covers cursor = 2 with one operation, which fails. It does not cover cursor = 1, which the implementation accepts and returns an empty page for. Add that assertion so the accepted cursor range stays pinned.

💚 Proposed addition
+        let exhausted = inspect_websocket_session(
+            Path::new("session.json"),
+            include_bytes!("../../../fixtures/websocket/session.json"),
+            None,
+            50,
+            1,
+        )
+        .unwrap();
+        assert!(exhausted.operations.is_empty());
         assert!(matches!(
             inspect_websocket_session(
🤖 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 `@crates/kahea-plan/src/lib.rs` around lines 2501 - 2510, Add a boundary
assertion alongside the existing inspect_websocket_session test for cursor ==
count (cursor 1 with the one-operation fixture), asserting it is accepted and
returns an empty page. Keep the existing cursor 2 InvalidWebSocketSource
assertion unchanged.

402-413: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared risk inference.

This block duplicates the inference in build_websocket_plan_with_configuration at lines 498-516. The two copies can drift. Extract one helper that takes the declared risk and the actions, and call it from both sites. The plan path keeps its configuration override on top of the helper result.

♻️ Proposed helper
+fn infer_websocket_risk(declared: Option<RiskClass>, actions: &[WebSocketAction]) -> RiskClass {
+    let sends_data = actions.iter().any(|action| {
+        matches!(
+            action,
+            WebSocketAction::SendText { .. } | WebSocketAction::SendBinary { .. }
+        )
+    });
+    match declared {
+        Some(RiskClass::Read | RiskClass::Unknown) if sends_data => RiskClass::Write,
+        Some(risk) => risk,
+        None if sends_data => RiskClass::Write,
+        None => RiskClass::Unknown,
+    }
+}

Then in inspect_websocket_session:

-    let sends_data = source.actions.iter().any(|action| {
-        matches!(
-            action,
-            WebSocketAction::SendText { .. } | WebSocketAction::SendBinary { .. }
-        )
-    });
-    let risk = match source.risk {
-        Some(RiskClass::Read | RiskClass::Unknown) if sends_data => RiskClass::Write,
-        Some(risk) => risk,
-        None if sends_data => RiskClass::Write,
-        None => RiskClass::Unknown,
-    };
+    let risk = infer_websocket_risk(source.risk, &source.actions);
🤖 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 `@crates/kahea-plan/src/lib.rs` around lines 402 - 413, Extract the shared risk
inference from the current block into a helper accepting the declared risk and
action collection, preserving the SendText/SendBinary detection and
Write/Unknown behavior. Replace the duplicated logic in both
inspect_websocket_session and build_websocket_plan_with_configuration with calls
to this helper, while keeping the plan path’s configuration override applied
after the helper result.
crates/kahea/tests/cli.rs (3)

525-637: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Split the three scenarios into separate tests.

This function covers expectation failure, action timeout, and handshake failure in sequence. If the first scenario fails, the other two never run, and their temporary directories are never created or cleaned. Three #[test] functions give independent signals and allow parallel execution.

🤖 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 `@crates/kahea/tests/cli.rs` around lines 525 - 637, Split
websocket_cli_maps_expectation_timeout_and_handshake_failures into three
independent #[test] functions covering expectation failure, action timeout, and
handshake failure. Move each scenario’s setup, invocation, assertions, server
join, and temporary-directory cleanup into its own test, preserving the existing
symbols and expected outcomes.

447-459: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bound the accept in the test server thread.

listener.accept() blocks without a deadline. If the CLI never connects, server.join() at line 476 blocks forever and the test hangs instead of failing. Set a read timeout on the accepted stream, or set the listener to non-blocking with a bounded poll, so a connection failure surfaces as a test failure. The failure-path tests at lines 548-552, 585-589, and 619-626 use the same pattern.

🤖 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 `@crates/kahea/tests/cli.rs` around lines 447 - 459, Bound the server-thread
accept flow around listener.accept() so missing CLI connections fail within a
timeout instead of blocking server.join() indefinitely. Apply the same
bounded-accept handling to the analogous failure-path test servers, preserving
connection handling while ensuring timeout errors propagate as test failures.

665-688: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover every rejected override, not only --server.

crates/kahea/src/main.rs lines 326-338 reject --input, --set, --server, --auth, --content-type, and --check. This test exercises only --server. If one condition is dropped from that guard, the test still passes. Loop over each flag and assert invalid-websocket-plan-options for all of them.

💚 Proposed change
-    let override_attempt = Command::new(binary())
-        .args([
-            "plan",
-            source.to_str().unwrap(),
-            "cliSession",
-            "--server",
-            "ws://attacker.example.test/socket",
-            "--store",
-            store.to_str().unwrap(),
-        ])
-        .output()
-        .unwrap();
-    assert_eq!(override_attempt.status.code(), Some(2));
-    let error: Value = serde_json::from_slice(&override_attempt.stdout).unwrap();
-    assert_eq!(error["code"], "invalid-websocket-plan-options");
+    for override_flag in [
+        vec!["--server", "ws://attacker.example.test/socket"],
+        vec!["--auth", "chat-sandbox"],
+        vec!["--content-type", "application/json"],
+        vec!["--check", "status:200"],
+        vec!["--set", "body.injected=true"],
+    ] {
+        let mut arguments = vec!["plan", source.to_str().unwrap(), "cliSession"];
+        arguments.extend(override_flag.iter().copied());
+        arguments.extend(["--store", store.to_str().unwrap()]);
+        let override_attempt = Command::new(binary()).args(&arguments).output().unwrap();
+        assert_eq!(
+            override_attempt.status.code(),
+            Some(2),
+            "override {override_flag:?} was accepted"
+        );
+        let error: Value = serde_json::from_slice(&override_attempt.stdout).unwrap();
+        assert_eq!(error["code"], "invalid-websocket-plan-options");
+    }
🤖 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 `@crates/kahea/tests/cli.rs` around lines 665 - 688, Expand the override
rejection test around the existing `override_attempt` command to iterate over
`--input`, `--set`, `--server`, `--auth`, `--content-type`, and `--check`,
supplying each flag’s required value. Execute the CLI for every override and
assert each response exits with status 2 and reports
`invalid-websocket-plan-options`, while preserving the existing WebSocket plan
setup.
crates/kahea/src/main.rs (1)

605-621: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the exit computation into the terminal arms.

result.exit() returns None only for WebSocketConnectResult::Connected, and that arm returns an error before exit is used. The unwrap_or(3) default is therefore unreachable. Bind the exit inside each terminal arm instead, so the code states the invariant directly.

♻️ Proposed simplification
-                let exit = result.exit().unwrap_or(3);
-                match result {
+                let exit = match result {
                     WebSocketConnectResult::Observation(observation) => {
+                        let exit = observation.exit;
                         write_envelope(&observation).map_err(io_error)?;
+                        exit
                     }
                     WebSocketConnectResult::Denied(denial) => {
+                        let exit = denial.exit;
                         write_envelope(&denial).map_err(io_error)?;
+                        exit
                     }
                     WebSocketConnectResult::Connected(_) => {
                         return Err(CliError {
                             code: "websocket-invocation-failed",
                             message: "WebSocket executor returned a non-terminal connection".into(),
                             exit: 3,
                         });
                     }
-                }
+                };
                 return Ok(exit);
🤖 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 `@crates/kahea/src/main.rs` around lines 605 - 621, Move the result.exit()
computation out of the shared pre-match binding and into the terminal
Observation and Denied arms, using the matched value to obtain and return the
exit status after writing the envelope. Keep the Connected arm returning the
existing websocket-invocation-failed error without computing a fallback exit.
🤖 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 `@crates/kahea/tests/cli.rs`:
- Around line 585-597: Update the timeout server logic in the timeout_server
thread to keep the accepted WebSocket connection open until the client
disconnects, or otherwise wait beyond the configured 2000 ms total timeout.
Preserve the existing invoke_websocket assertions so the client consistently
reports terminal_cause as action-timeout.

---

Nitpick comments:
In `@crates/kahea-plan/src/lib.rs`:
- Around line 2501-2510: Add a boundary assertion alongside the existing
inspect_websocket_session test for cursor == count (cursor 1 with the
one-operation fixture), asserting it is accepted and returns an empty page. Keep
the existing cursor 2 InvalidWebSocketSource assertion unchanged.
- Around line 402-413: Extract the shared risk inference from the current block
into a helper accepting the declared risk and action collection, preserving the
SendText/SendBinary detection and Write/Unknown behavior. Replace the duplicated
logic in both inspect_websocket_session and
build_websocket_plan_with_configuration with calls to this helper, while keeping
the plan path’s configuration override applied after the helper result.

In `@crates/kahea/src/main.rs`:
- Around line 605-621: Move the result.exit() computation out of the shared
pre-match binding and into the terminal Observation and Denied arms, using the
matched value to obtain and return the exit status after writing the envelope.
Keep the Connected arm returning the existing websocket-invocation-failed error
without computing a fallback exit.

In `@crates/kahea/tests/cli.rs`:
- Around line 525-637: Split
websocket_cli_maps_expectation_timeout_and_handshake_failures into three
independent #[test] functions covering expectation failure, action timeout, and
handshake failure. Move each scenario’s setup, invocation, assertions, server
join, and temporary-directory cleanup into its own test, preserving the existing
symbols and expected outcomes.
- Around line 447-459: Bound the server-thread accept flow around
listener.accept() so missing CLI connections fail within a timeout instead of
blocking server.join() indefinitely. Apply the same bounded-accept handling to
the analogous failure-path test servers, preserving connection handling while
ensuring timeout errors propagate as test failures.
- Around line 665-688: Expand the override rejection test around the existing
`override_attempt` command to iterate over `--input`, `--set`, `--server`,
`--auth`, `--content-type`, and `--check`, supplying each flag’s required value.
Execute the CLI for every override and assert each response exits with status 2
and reports `invalid-websocket-plan-options`, while preserving the existing
WebSocket plan setup.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 96aac1e6-f346-44f1-b2b1-f8b2904a9ca6

📥 Commits

Reviewing files that changed from the base of the PR and between 0f65360 and 4f79f30.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • README.md
  • crates/kahea-plan/src/lib.rs
  • crates/kahea/Cargo.toml
  • crates/kahea/src/main.rs
  • crates/kahea/tests/cli.rs

Comment thread crates/kahea/tests/cli.rs
@copyleftdev
copyleftdev merged commit 489b033 into main Aug 8, 2026
13 checks passed
@copyleftdev
copyleftdev deleted the agent/websocket-cli-vertical-slice branch August 8, 2026 17:08
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.

websocket: expose inspect/plan/invoke/explain through the CLI

1 participant