Skip to content

Route WTA CLI terminal-action proposals directly to Helpers - #484

Open
vanzue wants to merge 21 commits into
mainfrom
dev/vanzue/cli-terminal-action-proposals
Open

Route WTA CLI terminal-action proposals directly to Helpers#484
vanzue wants to merge 21 commits into
mainfrom
dev/vanzue/cli-terminal-action-proposals

Conversation

@vanzue

@vanzue vanzue commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the earlier CLI-to-master route-token design with a short-lived WTA CLI that connects directly to the Helper owning the current turn
  • inject one opaque v1.<helper-instance>.<turn-nonce> channel and one canonical PowerShell invocation into each ACP Agent prompt
  • resolve exact proposal permission requests silently as AllowOnce in the Helper, binding the approved payload digest before the CLI can connect
  • validate channel ownership, session epoch, turn freshness, lease, SHA-256 payload digest, retry budget, and replay state in the Helper
  • surface the existing recommendation card and keep user confirmation as the only terminal-mutation boundary
  • return two JSON Lines to the Agent: immediate validation, then the final confirmed/cancelled/lifecycle result
  • remove the obsolete Master proposal token/router/client/result path; Master remains responsible only for agent process ownership, ACP multiplexing, session-to-Helper routing, and permission forwarding

End-to-end flow

Helper starts turn and issues opaque channel
  -> Agent requests permission for exact canonical WTA command
  -> ACP Master forwards permission request to owning Helper
  -> Helper silently AllowOnce + arms payload digest
  -> Agent starts short-lived WTA CLI
  -> WTA CLI derives and connects to per-Helper named pipe
  -> Helper validates and stages recommendation card
  -> CLI receives validation response
  -> user confirms/cancels, or lifecycle invalidates proposal
  -> CLI receives final response and exits

Proposal payloads, channels, digests, acknowledgements, and final results never transit wta-master.

Contract and security

Canonical invocation:

& "$env:WTA_CLI_PATH" propose-terminal-actions --channel <channel> --payload-json '<compact-json>'
  • stdin, pipelines, here-strings, redirects, temporary files, extra arguments, and alternate executable spellings are rejected and silently cancelled before Permission UI
  • the per-Helper pipe is local-only and ACL-restricted to SYSTEM plus the current user; proposal startup fails closed if the descriptor cannot be built
  • proposal JSON cannot choose session, Helper, window, tab, or pane identity
  • channels are one-use, have a 30-second armed lease, and are invalidated on newer turns, session replacement, restart, transport loss, cancellation, or timeout
  • stale/timed-out cards cannot dispatch because confirmation atomically claims the live AwaitingUser proposal before executor enqueue
  • this remains a bearer-channel design rather than process attestation; the residual same-user race is bounded by the visible recommendation card and mandatory user confirmation

Agent 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

  • WTA tests: 1236 passed
  • real Windows named-pipe validation/final round trip covered
  • explicit x86_64-pc-windows-msvc WTA build completed
  • Terminal Debug build completed with 0 errors
  • Debug package deployed and launched; live Helper reached ACP Connected and logged its per-Helper proposal pipe
  • package-family-specific wta.exe App Execution Alias created and invoked successfully from external PowerShell

Relationships

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

Copilot AI 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.

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_terminal argv example uses --payload-base64 even though the payload is specified as base64url-encoded later. To keep the schema consistent for agents and validators, use --payload-base64url here 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-base64 even though the payload is specified as base64url-encoded. Renaming the flag to --payload-base64url would make the encoding requirement unambiguous for callers.
  - --payload-base64
  - <base64url-encoded UTF-8 JSON>

Comment thread doc/specs/WTA-CLI-terminal-action-proposals.md Outdated
Comment thread doc/specs/WTA-CLI-terminal-action-proposals.md Outdated
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
Copilot AI review requested due to automatic review settings July 23, 2026 14:06
@vanzue

vanzue commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Architecture correction: agent sessions are now expected to execute WTA directly. The revised spec removes the helper-proxied create_terminal assumption and routes the short-lived CLI through the existing master ACP pipe using a prompt-scoped capability.

Copilot AI 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.

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 is send. 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 use send. 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_input diverges from the existing RecommendationSet action name (send). Unless the new proposal wire schema intentionally renames actions, consider using send here as well for consistency.
- Exactly one choice with exactly one `send_input` action.

Comment thread doc/specs/WTA-CLI-terminal-action-proposals.md Outdated
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
Copilot AI review requested due to automatic review settings July 24, 2026 09:30
@vanzue vanzue changed the title Design WTA CLI terminal-action proposals Route WTA CLI terminal-action proposals directly to Helpers Jul 24, 2026
@github-actions

This comment has been minimized.

Copilot AI 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.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

Comment thread tools/wta/src/protocol/acp/client.rs
vanzue added 4 commits July 27, 2026 13:42
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d998a7af-5b49-496b-8b59-ea48f40e50c8
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
Copilot AI review requested due to automatic review settings July 28, 2026 04:12
@github-actions

This comment has been minimized.

Copilot AI 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.

Pull request overview

Copilot reviewed 25 out of 26 changed files in this pull request and generated 2 comments.

Comment thread tools/wta/src/proposal_channel.rs Outdated
Comment thread tools/wta/src/app_turn.rs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7743e270-ccd3-40be-b382-42639b800d46
Copilot AI review requested due to automatic review settings July 28, 2026 06:00

Copilot AI 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.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a828e781-6e7b-4356-9e3d-c952d092587e

Copilot AI 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.

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
Copilot AI review requested due to automatic review settings July 30, 2026 06:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a828e781-6e7b-4356-9e3d-c952d092587e

Copilot AI 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.

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_actions flushes 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(())

Copilot AI review requested due to automatic review settings July 30, 2026 06:07

Copilot AI 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.

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_frame enforces MAX_FRAME_BYTES before 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. Since MAX_FRAME_BYTES is defined as the maximum encoded JSON-line size, trim \n/optional \r first and then enforce the limit on the JSON payload bytes; also increase the take() 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 if LOCALAPPDATA/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 without WTA_CLI_PATH rather than returning an error from spawn_agent_process.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a828e781-6e7b-4356-9e3d-c952d092587e
Copilot AI review requested due to automatic review settings July 30, 2026 06:25

Copilot AI 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.

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_payload currently equates “compact JSON” with serde_json::to_string(Value) == payload. With serde_json = "1" (no preserve_order feature), 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(())

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a828e781-6e7b-4356-9e3d-c952d092587e
Copilot AI review requested due to automatic review settings July 30, 2026 06:44

Copilot AI 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.

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 (3)

tools/wta/src/terminal_action_proposal.rs:392

  • Similarly, OpenAndSend forwards cwd, title, and profile without trimming or bounds checks. Since these fields are model-controlled, they should be filtered for empties and capped like the other free-text fields to avoid unexpected downstream behavior and unbounded UI/log growth.
    tools/wta/src/cli/proposals.rs:41
  • The CLI flushes stdout after printing the validation JSON, but not after printing the final JSON line. If stdout is piped (typical for agents), the final response can be buffered and never observed reliably by the caller before process exit.
    let final_response: crate::proposal_pipe::ProposalFinalResponse =
        read_response(&mut reader).await?;
    println!("{}", serde_json::to_string(&final_response)?);
    Ok(())

tools/wta/src/terminal_action_proposal.rs:387

  • The proposal wire format documents size bounds to prevent UI/log bloat, but Open currently forwards cwd, title, and profile without trimming or length checks. This allows empty strings (Some("")) and unbounded text to flow into the RecommendationSet.

This issue also appears on line 388 of the same file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a828e781-6e7b-4356-9e3d-c952d092587e
Copilot AI review requested due to automatic review settings July 30, 2026 08:15

Copilot AI 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.

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/cli/proposals.rs:41

  • The CLI prints the final JSON line but does not flush stdout afterward. If the agent process reads stdout via a pipe, the final response can remain buffered, causing the agent to hang waiting for the completion line.
    let final_response: crate::proposal_pipe::ProposalFinalResponse =
        read_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
Copilot AI review requested due to automatic review settings July 30, 2026 09:02

Copilot AI 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.

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/Cargo.toml:56

  • A new direct Rust dependency (sha2) was added, which changes the WTA dependency graph. Per tools/wta/AGENTS.md, this requires regenerating and committing tools/wta/cgmanifest.json and the <!-- BEGIN wta-rust-deps --> block in /NOTICE.md via pwsh -File .\build\scripts\Generate-WtaThirdPartyNotices.ps1, in the same commit as the Cargo change.
uuid = { version = "1", features = ["v4"] }
sha2 = "0.10"
rust-i18n = "3"

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.

3 participants