Route WTA CLI terminal-action proposals directly to Helpers - #484
Route WTA CLI terminal-action proposals directly to Helpers#484vanzue wants to merge 18 commits into
Conversation
Define a typed, helper-bound CLI proposal flow that preserves existing card confirmation without reintroducing MCP. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d998a7af-5b49-496b-8b59-ea48f40e50c8
There was a problem hiding this comment.
Pull request overview
This PR adds a design/specification document that replaces the previously proposed MCP-based typed terminal-action proposal transport with a WTA CLI contract, while preserving the existing recommendation-card confirmation flow and wtcli/COM execution boundary.
Changes:
- Define a
wta propose-terminal-actions --payload-base64 <...>CLI-based proposal submission contract (proposal-only; non-mutating). - Specify helper-local hardened transport (per-helper pipe + short-lived one-use capability) and freshness/origin policies for Terminal Agent vs. Autofix.
- Outline phased rollout, compatibility fallback to assistant-text JSON, and validation expectations.
Comments suppressed due to low confidence (3)
doc/specs/WTA-CLI-terminal-action-proposals.md:78
- The proposed
create_terminalargv example uses--payload-base64even though the payload is specified as base64url-encoded later. To keep the schema consistent for agents and validators, use--payload-base64urlhere as well.
2. The agent requests create_terminal for direct argv:
command = "wta"
args = ["propose-terminal-actions", "--payload-base64", "..."]
3. wta-master routes create_terminal by ACP session id to the owning helper.
doc/specs/WTA-CLI-terminal-action-proposals.md:215
- Unclear term: “App” isn’t defined elsewhere in the doc, and could be read as “Windows Terminal app” rather than the helper-side logic. Consider rephrasing to “The helper remains authoritative…” for clarity.
App remains authoritative for state that the ACP client does not own:
doc/specs/WTA-CLI-terminal-action-proposals.md:109
- The CLI contract uses
--payload-base64even though the payload is specified as base64url-encoded. Renaming the flag to--payload-base64urlwould make the encoding requirement unambiguous for callers.
- --payload-base64
- <base64url-encoded UTF-8 JSON>
Make agent sessions execute WTA directly, then use a prompt-scoped route token and the existing master ACP pipe to reach the owning helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d998a7af-5b49-496b-8b59-ea48f40e50c8
|
Architecture correction: agent sessions are now expected to execute WTA directly. The revised spec removes the helper-proxied |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (3)
doc/specs/WTA-CLI-terminal-action-proposals.md:202
- This list calls the action
send_input, but the current recommendation-card action name issend. Aligning the proposal wire schema with the existing action name will reduce schema drift across specs, prompts, and implementation.
- `send_input`
doc/specs/WTA-CLI-terminal-action-proposals.md:244
- In the Terminal Agent origin policy section, the action name is
send_input, but existing RecommendationSet policy/docs usesend. Keeping the same action name across specs will make the contract easier to implement and reason about.
- `send_input` targets the active pane captured for the prompt.
doc/specs/WTA-CLI-terminal-action-proposals.md:251
- In the Autofix policy,
send_inputdiverges from the existing RecommendationSet action name (send). Unless the new proposal wire schema intentionally renames actions, consider usingsendhere as well for consistency.
- Exactly one choice with exactly one `send_input` action.
Replace master token routing with per-Helper proposal channels, canonical silent AllowOnce handling, secure direct named pipes, and two-phase validation/final feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d998a7af-5b49-496b-8b59-ea48f40e50c8
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d998a7af-5b49-496b-8b59-ea48f40e50c8
…al-action-proposals
Make the direct helper channel the only card-producing path and hide its internal proposal tool calls from chat while preserving normal tool call rendering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7743e270-ccd3-40be-b382-42639b800d46
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7743e270-ccd3-40be-b382-42639b800d46 # Conflicts: # tools/wta/src/app_tests.rs # tools/wta/src/app_turn.rs # tools/wta/src/protocol/acp/client.rs # tools/wta/src/protocol/acp/mock_agent_tests.rs # tools/wta/src/ui/chat.rs
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7743e270-ccd3-40be-b382-42639b800d46
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7743e270-ccd3-40be-b382-42639b800d46
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
tools/wta/src/main.rs:1787
read_proposal_responseenforcesMAX_FRAME_BYTESon the buffer including the trailing newline, but the server-side framing treatsMAX_FRAME_BYTESas the max line length excluding the newline (it reads up toMAX_FRAME_BYTES + 1and then strips\n/\r). This off-by-one can incorrectly reject a maximal-sized response line and also leaves the newline in the JSON slice.
if line.len() + take > proposal_pipe::MAX_FRAME_BYTES {
anyhow::bail!("proposal response exceeds the frame limit");
}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7743e270-ccd3-40be-b382-42639b800d46
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 31 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
tools/wta/Cargo.toml:55
- A new direct dependency (
sha2) was added. Per WTA Rust conventions, dependency graph changes must regenerate and committools/wta/cgmanifest.jsonand the generated<!-- BEGIN wta-rust-deps -->block in/NOTICE.md; otherwise component governance / third‑party notices can drift from the shipped binary.
sha2 = "0.10"
tools/wta/src/proposal_pipe.rs:203
- When the app-side proposal event receiver is dropped (
event_tx.send(...)returns Err), the code marks the proposal as rejected viareject_validation(..., false), which tombstones it asCancelled. That contradicts the returnedunavailablevalidation status and can cause later attempts on the same channel to reportalready_consumedinstead ofunavailable. Prefer marking the transport unavailable (or resolving the proposal asUnavailable) so helper state matches the response and fails closed consistently.
manager.reject_validation(&proposal_id, false);
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7743e270-ccd3-40be-b382-42639b800d46
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 31 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
tools/wta/src/main.rs:1755
open_proposal_pipesleeps after the final failed open attempt, then immediately bails without ever retrying. This adds up to 1s of extra latency on failure paths and makes the backoff list misleading (the last delay never gates a subsequent attempt).
for (attempt, wait_ms) in BACKOFF_MS.iter().enumerate() {
match tokio::net::windows::named_pipe::ClientOptions::new().open(pipe_name) {
Ok(pipe) => return Ok(pipe),
Err(error)
if matches!(
error.raw_os_error(),
Some(ERROR_FILE_NOT_FOUND | ERROR_PIPE_BUSY)
) =>
{
tracing::debug!(
target: "proposal_cli",
pipe = %pipe_name,
attempt = attempt + 1,
wait_ms,
"proposal pipe not ready"
);
tokio::time::sleep(std::time::Duration::from_millis(*wait_ms)).await;
}
tools/wta/src/main.rs:1722
run_propose_terminal_actionsflushes stdout after the validation JSON line but not after the final JSON line. For a protocol-only stdout contract (agent reads JSON Lines), flushing both lines makes delivery behavior consistent across hosts/buffers.
let final_response: proposal_pipe::ProposalFinalResponse =
read_proposal_response(&mut reader).await?;
println!("{}", serde_json::to_string(&final_response)?);
Ok(())
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a828e781-6e7b-4356-9e3d-c952d092587e
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 33 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
tools/wta/src/app_turn.rs:654
- Clearing active_direct_proposal_id immediately after the user confirms will cause any remaining AgentMessageChunk text (arriving before AgentMessageEnd) to be dropped, even though turn_observe_chunk explicitly tries to keep trailing chunks visible for direct proposals. Consider keeping the ID until AgentMessageEnd, then clearing it when the turn is finalized.
tab.active_direct_proposal_id = None;
tools/wta/src/proposal_pipe.rs:165
- Oversized proposal payloads should be retryable: the agent can usually correct this by sending a smaller payload. Returning retryable=false here prevents the agent from attempting a corrected submission even though it’s a recoverable validation failure.
return write_validation_failure(
&mut write_half,
ProposalValidationStatus::InvalidSchema,
format!("payload exceeds the {MAX_PAYLOAD_BYTES}-byte inline limit"),
false,
tools/wta/src/app_turn.rs:533
- active_direct_proposal_id should be cleared once the direct-proposal turn is finalized (after trailing details are committed) to avoid accepting stray late chunks indefinitely. Clearing it here ensures the extra Surfaced-chunk passthrough only applies until AgentMessageEnd.
This issue also appears on line 654 of the same file.
fn turn_commit_trailing_direct_proposal_details(&mut self, session_id: &str) {
let tab = self.session_tab_mut(session_id);
let trailing = tab.current_turn_details();
if let Some(completed) = tab.completed_turns.last_mut() {
completed.details.extend(trailing);
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a828e781-6e7b-4356-9e3d-c952d092587e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a828e781-6e7b-4356-9e3d-c952d092587e
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
tools/wta/src/main.rs:1189
run_propose_terminal_actionsflushes stdout after the validation JSON line, but not after the final JSON line. If the agent reads stdout over a pipe, the final response can be buffered and delayed, breaking the “two JSON Lines” contract.
let final_response: proposal_pipe::ProposalFinalResponse =
read_proposal_response(&mut reader).await?;
println!("{}", serde_json::to_string(&final_response)?);
Ok(())
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
tools/wta/src/proposal_pipe.rs:329
read_frameenforcesMAX_FRAME_BYTESbefore stripping the line terminator, which effectively reduces the maximum JSON frame size by 1 byte and can also reject a max-sized CRLF-terminated frame. SinceMAX_FRAME_BYTESis defined as the maximum encoded JSON-line size, trim\n/optional\rfirst and then enforce the limit on the JSON payload bytes; also increase thetake()bound to allow CRLF at the limit.
if frame.len() > MAX_FRAME_BYTES {
anyhow::bail!("proposal request frame exceeds {MAX_FRAME_BYTES} bytes");
}
if frame.last() != Some(&b'\n') {
anyhow::bail!("proposal request frame is not newline terminated");
tools/wta/src/protocol/acp/spawn.rs:270
proposal_cli_path()?makes agent spawn fail hard ifLOCALAPPDATA/current_exe()can't be resolved (or other alias resolution issues), which can take down the whole agent pane even though proposals are an optional capability. This should be best-effort: warn and continue spawning the agent withoutWTA_CLI_PATHrather than returning an error fromspawn_agent_process.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a828e781-6e7b-4356-9e3d-c952d092587e
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 36 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
tools/wta/src/proposal_invocation.rs:56
validate_payloadcurrently equates “compact JSON” withserde_json::to_string(Value) == payload. Withserde_json = "1"(nopreserve_orderfeature), object key order is not preserved on re-encode, so payloads that are already whitespace-free but have a different key order (or different-but-equivalent escaping) will be rejected and the helper will silently cancel proposal permissions.
This makes the direct-proposal contract unnecessarily brittle for agents, and it’s not implied by the docs (they require compact JSON, not a specific key ordering). Suggest validating “no whitespace outside strings” + “valid JSON” instead, and leaving exact-byte matching to the digest arming/check.
fn validate_payload(payload: &str) -> Result<(), &'static str> {
if payload.is_empty() || payload.len() > MAX_PAYLOAD_BYTES {
return Err("proposal payload is empty or too large");
}
let value: serde_json::Value =
serde_json::from_str(payload).map_err(|_| "proposal payload is invalid JSON")?;
let compact =
serde_json::to_string(&value).map_err(|_| "proposal payload could not be encoded")?;
if compact != payload {
return Err("proposal payload must be compact JSON");
}
Ok(())
Summary
v1.<helper-instance>.<turn-nonce>channel and one canonical PowerShell invocation into each ACP Agent promptAllowOncein the Helper, binding the approved payload digest before the CLI can connectEnd-to-end flow
Proposal payloads, channels, digests, acknowledgements, and final results never transit
wta-master.Contract and security
Canonical invocation:
AwaitingUserproposal before executor enqueueAgent scope
All ACP Agents use one Direct Proposal implementation. Every Agent receives the same prompt-scoped channel and canonical command, and every valid permission envelope enters the same Helper-owned validation and Recommendation Card pipeline. Direct Proposal is the sole card-producing path; incompatible permission envelopes fail closed and expose the integration issue.
Validation
x86_64-pc-windows-msvcWTA build completedwta.exeApp Execution Alias created and invoked successfully from external PowerShellRelationships