Plan ACP runtime method denylist enforcement (2.6.2) - #90
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
WalkthroughAdd Step 2.6.2 ExecPlan and implement ACP runtime denylist: policy module, newline-frame assembler with overflow/raw-fallback rules, runtime adapter with stdin sink and synthesized denial responses, CapabilityPolicy wiring, and unit/BDD/integration tests; update docs and feature scenarios. ChangesRuntime Denylist ExecPlan
Sequence Diagram(s)sequenceDiagram
participant Agent as Container stdout
participant Adapter as OutboundPolicyAdapter
participant Host as Host stdout
participant Sink as StdinSink (run_container_stdin_sink)
participant Container as Container stdin
Agent->>Adapter: chunked stdout bytes
Adapter->>Adapter: assemble frames (OutboundFrameAssembler)
Adapter->>Adapter: call evaluate_agent_outbound_frame
alt Forward
Adapter->>Host: write bytes verbatim
else BlockRequest
Adapter->>Sink: enqueue Synthesised JSON-RPC error (preserve id & line ending)
Sink->>Container: write & flush synthesized bytes (ordered)
else BlockNotification
Adapter->>Adapter: drop notification (no sink enqueue)
end
Sink->>Container: tolerate BrokenPipe, log once, drain remaining commands
Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/execplans/2-6-2-runtime-denylist.md`:
- Around line 270-271: The sentence uses first-person wording "in our proxy";
update the text so it uses neutral phrasing (e.g., replace "in our proxy" with
"in the protocol proxy" or "in the proxy") in the sentence that reads "so the
bytes flow agent stdout to host stdout in our proxy" to remove personal pronouns
and match repository documentation tone rules.
- Around line 519-520: The doc uses two different helper names for the same
concept—`allows_runtime_enforcement` and `enforces_runtime_denylist`—which is
confusing; pick a single canonical name (suggest `allows_runtime_enforcement`)
and update all mentions in Stage E, the "Interfaces and dependencies" section,
and the other occurrences around lines 1018–1020 to use that name consistently,
and ensure the helper signatures shown (e.g., `pub(crate) const fn
allows_runtime_enforcement(self) -> bool` and `pub(crate) const fn
rewrites_initialize(self) -> bool`) reflect the chosen name everywhere in the
document.
- Line 21: The document uses Oxford '-ise' spellings; update all occurrences to
the OED '-ize' forms (e.g., change initialisation→initialization,
synthesised→synthesized, parameterised→parameterized, parallelise→parallelize,
optimisation→optimization) across this file (including the other referenced
locations). Search for the specific tokens "initialisation", "synthesised",
"parameterised", "parallelise", "optimisation" and replace with their '-ize'
counterparts, and run a pass for other '-ise' suffixed words to convert them to
'-ize' to comply with the en-GB-oxendict rule. Ensure changes preserve
surrounding punctuation and capitalization (e.g., Initialisation→Initialization)
and update all listed lines (35, 62, 90, 540, 630, 769) plus any additional
occurrences.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: 2ac0614e-8061-4174-a50c-33d00fa353c6
📒 Files selected for processing (1)
docs/execplans/2-6-2-runtime-denylist.md
|
@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. src/engine/connection/exec/acp_runtime_bdd_tests.rs Comment on lines +69 to +79 fn permitted_request_frame(method: &str, id: i64) -> Vec<u8> {
let mut bytes = serde_json::to_vec(&serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": {},
}))
.expect("permitted request serializes");
bytes.push(b'\n');
bytes
}❌ New issue: Code Duplication |
|
@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. src/engine/connection/exec/acp_runtime_tests.rs Comment on lines +85 to +95 fn permitted_frame() -> Vec<u8> {
let mut bytes = serde_json::to_vec(&serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "session/new",
"params": {},
}))
.expect("permitted frame serializes");
bytes.push(b'\n');
bytes
}❌ New issue: Code Duplication |
|
@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. src/engine/connection/exec/acp_frame.rs Comment on lines +100 to +142 pub(crate) fn ingest_chunk(
&mut self,
chunk: &[u8],
) -> (Vec<FrameOutput>, Option<FallbackReason>) {
if self.raw_fallback {
return (
if chunk.is_empty() {
Vec::new()
} else {
vec![FrameOutput::Forward(chunk.to_vec())]
},
None,
);
}
let mut outputs = Vec::new();
let mut fallback = None;
let mut cursor = 0;
while cursor < chunk.len() {
let remaining = chunk.get(cursor..).unwrap_or(&[]);
if let Some(newline_offset) = remaining.iter().position(|byte| *byte == b'\n') {
let frame_end = cursor + newline_offset + 1;
let frame_slice = chunk.get(cursor..frame_end).unwrap_or(&[]);
outputs.push(self.complete_frame(frame_slice));
cursor = frame_end;
continue;
}
// No newline in the rest of the chunk; append and stop.
let pending = chunk.get(cursor..).unwrap_or(&[]);
if self.buffer.len() + pending.len() > MAX_RUNTIME_FRAME_BYTES {
outputs.push(self.flush_buffer_for_overflow(pending));
fallback = Some(FallbackReason::BufferOverflow);
self.raw_fallback = true;
break;
}
self.buffer.extend_from_slice(pending);
break;
}
(outputs, fallback)
}❌ New issue: Bumpy Road Ahead |
|
@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. src/engine/connection/exec/protocol.rs Comment on lines +290 to +324 async fn forward_host_stdin_to_channel<HostStdin>(
host_stdin: HostStdin,
sender: tokio::sync::mpsc::Sender<WriteCmd>,
rewrite_acp_initialize: bool,
) -> io::Result<()>
where
HostStdin: AsyncRead + Unpin,
{
use tokio::io::AsyncReadExt;
let mut buffered_stdin = tokio::io::BufReader::with_capacity(STDIN_BUFFER_CAPACITY, host_stdin);
if rewrite_acp_initialize {
let bytes = acp_helpers::read_and_mask_initial_acp_frame(&mut buffered_stdin).await?;
if !bytes.is_empty() && sender.send(WriteCmd::Forward(bytes)).await.is_err() {
return Ok(());
}
}
let mut buf = vec![0u8; STDIN_BUFFER_CAPACITY];
loop {
let bytes_read = buffered_stdin.read(&mut buf).await?;
if bytes_read == 0 {
break;
}
let chunk = buf
.get(..bytes_read)
.map(<[u8]>::to_vec)
.unwrap_or_default();
if sender.send(WriteCmd::Forward(chunk)).await.is_err() {
break;
}
}
Ok(())
}❌ New issue: Bumpy Road Ahead |
|
@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. src/engine/connection/exec/acp_policy_tests.rs Comment on lines +43 to +53 fn jsonrpc_request(id: &Value, method: &str) -> Vec<u8> {
let payload = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": {},
});
let mut bytes = serde_json::to_vec(&payload).expect("request serializes");
bytes.push(b'\n');
bytes
}❌ New issue: Code Duplication |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
|
@coderabbitai Have the following failed checks now been resolved? If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures. Do not treat warnings as optional or aspirational. Where a change is out of scope for this PR, propose a GitHub issue unless one exists already. ❌ Failed checks (2 warnings)
|
This comment was marked as resolved.
This comment was marked as resolved.
Draft execplan for Step 2.6.2 of the Podbot roadmap: enforce a runtime denylist for blocked Agentic Control Protocol (ACP) methods after initialisation. The plan extends the protocol proxy seam established by Step 2.6.1 with a pure policy module, a newline-bounded frame assembler, an output-direction adapter, and a dedicated container-stdin sink task that drains synthesised JSON-RPC error responses before shutdown. The opt-in surface collapses two prior booleans into a single CapabilityPolicy enum. The plan incorporates a Logisphere pre-implementation review covering structure, alternatives, scale, JSON-RPC contract correctness, failure modes, and long-term viability, and includes an architecture spike checkpoint comparing the sink-task model against a single bidirectional select! task. Status: DRAFT — awaiting approval before implementation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace first-person "in our proxy" with "in the protocol proxy" for neutral documentation tone (line 270). - Rename CapabilityPolicy::enforces_runtime_denylist to allows_runtime_enforcement so the helper signatures shown in Stage E and the Interfaces and dependencies section agree on a single name. - Convert non-exception -ise/-isa tokens to Oxford -ize/-iza forms per the en-GB-oxendict rule recorded in AGENTS.md, covering initialization, synthesized, destabilizes, synchronized, serialization, serialize, recognized, unauthorized, parameterized, parallelize, optimization, randomized, and weaponizing. Preserved Oxford exceptions (advertise, improvise, surprise, otherwise, raise) and the -yse forms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce src/engine/connection/exec/acp_policy.rs containing the value types and pure functions that decide whether an agent-emitted Agentic Control Protocol (ACP) JSON-RPC frame must be blocked. The policy layer deliberately depends on neither tokio nor tracing so it remains trivially testable; the runtime adapter in Stage D will own all I/O, channel sends, and observability. Highlights: - MethodFamily values match a method-name prefix only when followed by a non-empty operation, so terminal/create matches but terminal/ and terminalize do not. - DEFAULT_BLOCKED_FAMILIES covers terminal/ and fs/. - evaluate_agent_outbound_frame returns Forward, BlockNotification, or BlockRequest with the original JSON-RPC id type-preserved. - build_method_blocked_error synthesizes a JSON-RPC 2.0 error response with code -32001, a stable message, and a data.reason discriminator. It returns a serde_json::Result so production code remains expect-free. - 25 rstest cases cover boundary matching, every id shape, malformed JSON, response/notification distinctions, CRLF terminators, and the error payload round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer's GuidePlans and scaffolds ACP runtime method denylist enforcement (Step 2.6.2) by introducing pure policy/assembly/runtime modules, a container-stdin sink architecture, and a new CapabilityPolicy enum, and wires the design plus contracts into developer, design, and user docs while adding minimal code changes to support the future implementation. Sequence diagram for handling a blocked ACP method at runtimesequenceDiagram
participant Agent
participant CO as ContainerStdout
participant Out as run_output_loop_with_adapter
participant Ad as OutboundPolicyAdapter
participant Fr as OutboundFrameAssembler
participant Po as acp_policy
participant Tx as mpsc_Sender_WriteCmd
participant Sink as run_container_stdin_sink
participant CSI as ContainerStdin
participant HS as HostStdout
Agent->>CO: blocked ACP request frame
CO->>Out: LogOutput::StdOut{message}
Out->>Ad: handle_chunk(message)
Ad->>Fr: ingest_chunk(message)
Fr-->>Ad: FrameOutput::Decision(BlockRequest, line_ending)
Ad->>Po: build_method_blocked_error(id, method, line_ending)
Po-->>Ad: synthesized_error_bytes
Ad->>Tx: send(WriteCmd::Synthesised(synthesized_error_bytes))
Tx-->>Sink: WriteCmd::Synthesised
Sink->>CSI: write_all(bytes) + flush
Note over Ad,HS: No FrameOutput::Forward emitted -> nothing written to host stdout
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8f1b9ca423
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Rename the sink command variant to `Synthesized` and document that the container-stdin sink terminates on channel close rather than an explicit shutdown command. Make `FrameOutput::Decision` carry only non-forward decisions so the runtime adapter no longer needs an unreachable `FrameDecision::Forward` branch. Add protocol-level coverage for `CapabilityPolicy` selection across `Disabled`, `MaskOnly`, and `MaskAndDeny`.
Return output and stdin-forwarding failures before awaiting the runtime sink task in the ACP enforcement path. Abort the sink on those early exits so a timed-out stdin forwarder cannot keep the channel open and hide the intended session error. Add a regression test for `MaskAndDeny` with a pending stdin reader to ensure the session reports the stdin timeout instead of hanging behind the sink.
Forward parsed objects that do not declare `jsonrpc: "2.0"` before checking their method name against the ACP runtime denylist. This keeps unrelated NDJSON payloads with fields such as `method: "terminal/..."` on the byte-transparent path and preserves runtime denylist enforcement for real JSON-RPC requests and notifications.
Update the runtime-denylist plan and Rustdocs to match the implemented capability-policy builder, channel-close sink termination, and fallible blocked-method error response builder. Keep the changes documentation-only so the existing runtime behaviour remains unchanged while generated docs and planning notes describe the current code.
Clarify that ACP metrics and span instrumentation are planned for Step 2.6.3 or the production rollout, while Step 2.6.2 only ships the existing stderr and `tracing::warn!` diagnostics.
|
@coderabbitai Have the following now been resolved? Annul any requirements that violate the en-GB-oxendict spelling (-ize / -yse / -our) conventions (for example a request to replace "normalize" with "normalise" or "artefact" with "artifact"), or where the requirement unnecessarily increases cyclomatic complexity. |
This comment was marked as resolved.
This comment was marked as resolved.
Replace the stale `with_acp_initialize_rewrite_enabled` references with `with_capability_policy` so the exec plan matches the current `CapabilityPolicy`-based API without changing the intended history.
Exercise the protocol session caller with each `CapabilityPolicy` variant so runtime enforcement is selected only for `MaskAndDeny`. The new tests use in-memory IO and a blocked `terminal/create` frame to verify that `Disabled` and `MaskOnly` keep the raw forwarding path while `MaskAndDeny` suppresses host stdout and writes a synthesized denial response to container stdin.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/engine/connection/exec/protocol_acp_tests.rs`:
- Around line 388-390: Add a module-level documentation comment for the test
module by inserting a `//!` comment immediately inside `mod
capability_policy_routing` (right after the opening brace) that briefly states
the module's purpose and utility (e.g., what behaviors or scenarios the tests
cover and any important context), so the module complies with the guideline that
every Rust module begins with a `//!` module comment; update the comment text to
be concise and relevant to `capability_policy_routing`.
- Around line 467-505: Replace the three near-duplicate tests
mask_and_deny_routes_through_enforcement_path,
disabled_policy_forwards_all_frames_raw, and
mask_only_policy_forwards_blocked_frames_raw with a single parameterised rstest
that iterates over CapabilityPolicy cases and expected routing outcomes; keep
the shared arrange/act using blocked_terminal_create_frame() and
run_policy_output_frame(), and drive assertions from the case data (e.g. for
each case assert host_stdout != frame and
synthesized_response_for_terminal_create(&container_stdin) for MaskAndDeny, and
assert host_stdout == frame for Disabled and MaskOnly). Use a small per-case
flag (e.g. expect_forward_raw or expect_synthesized_response) to choose between
asserting equality of host_stdout to frame or asserting
synthesized_response_for_terminal_create(&container_stdin).
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: 86f29399-620f-47c6-83f8-5666549d69fd
📒 Files selected for processing (1)
src/engine/connection/exec/protocol_acp_tests.rs
Add the routing test module documentation and collapse the three capability-policy cases into one `rstest` table. Keep the existing in-memory protocol harness while making each `CapabilityPolicy` expectation explicit in the case data.
Summary
docs/execplans/2-6-2-runtime-denylist.md, the governing execution plan for Step 2.6.2 ofdocs/podbot-roadmap.md: enforce a runtime denylist for blocked Agentic Control Protocol (ACP) methods after initialisation.acp_policy), a newline-bounded frame assembler (acp_frame), and an output-direction adapter plus dedicated container-stdin sink task (acp_runtime). The two prior boolean opt-ins collapse into a singleCapabilityPolicy::{Disabled, MaskOnly, MaskAndDeny}enum.-32001) and the stderr denial line into 2.6.2 because enforcement without a response would hang hosted agents. Subsequent roadmap items 2.6.3 (richer diagnostics), 2.6.4 (operator override), and 2.6.5 (override-test battery) remain explicitly out of scope.tokio::select!task before committing to either.Status
DRAFT — this PR is the planning artefact only. No production code or tests are added. Implementation begins after the plan is approved.
Test plan
docs/execplans/2-6-2-runtime-denylist.mdend-to-end and confirms the scope, constraints, tolerances, and decision log match intent.-32001withdata.reason = "podbot_capability_policy") and the trailing-slash prefix matching forterminal/andfs/.CapabilityPolicyenum collapsing the two prior booleans is acceptable as an internal API change.make markdownlintalready runs clean on the new file (verified locally).🤖 Generated with Claude Code
Summary by Sourcery
Plan and partially wire a new ACP runtime method denylist enforcement path, introducing capability policy selection, runtime ACP framing and policy modules, and documenting the architecture and behaviour across developer, design, and user guides.
New Features:
Enhancements:
Documentation:
Tests: