Skip to content

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

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

Route WTA CLI terminal-action proposals directly to Helpers#484
vanzue wants to merge 18 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: 7743e270-ccd3-40be-b382-42639b800d46
Copilot AI review requested due to automatic review settings July 28, 2026 09:28

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.

Comments suppressed due to low confidence (1)

tools/wta/src/main.rs:1787

  • read_proposal_response enforces MAX_FRAME_BYTES on the buffer including the trailing newline, but the server-side framing treats MAX_FRAME_BYTES as the max line length excluding the newline (it reads up to MAX_FRAME_BYTES + 1 and 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
Copilot AI review requested due to automatic review settings July 29, 2026 03:46

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 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 commit tools/wta/cgmanifest.json and 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 via reject_validation(..., false), which tombstones it as Cancelled. That contradicts the returned unavailable validation status and can cause later attempts on the same channel to report already_consumed instead of unavailable. Prefer marking the transport unavailable (or resolving the proposal as Unavailable) 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
Copilot AI review requested due to automatic review settings July 29, 2026 03:55

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 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_pipe sleeps 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_actions flushes 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

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

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