# External Action Controller

Wall-E treats outbound communication and other irreversible actions as
validated external actions, not raw model tool calls.

## Contract

The model can propose a tool call such as `mail_send`, `mail_reply`, or
`slack_send_message`.
Before execution, `chat.js` routes the call through
`external-action-controller.js`, which builds an action envelope:

- `domain` and `operation`: email, Slack, SMS, calendar, reminder, notification.
- `intent`: whether the user asked for prepared content, dispatch, or something
  ambiguous.
- `actor`: sender account or runtime identity.
- `target`: recipient, channel, attendees, or device.
- `payload`: message body, subject, event fields, or notification text.
- `validation`: blocking issues and confirmation-required warnings.
- `payloadHash` and `idempotencyKey`: stable identifiers for approval and retry
  safety.

If there is no matching approved envelope, the controller does not execute the
tool. It returns a structured tool result that tells the model to present the
draft or staged action and not claim that anything was sent.

## CTM Approval Flow

The approval path mirrors OpenCode's permission handshake: the model proposes a
tool call, Wall-E stages a deterministic envelope, the UI captures an explicit
user confirmation, and the exact approved envelope is replayed back to Wall-E.
Wall-E then executes the original payload directly rather than asking the model
to recreate it.

There are two valid CTM approval handoffs:

- **Exact-payload replay**: the prior assistant turn already produced blocked
  external-action envelopes. CTM reconstructs those envelopes from either
  durable `toolCalls` or provider-native `tool_result` blocks and sends exact
  `actionId`/`payloadHash` approvals.
- **Current-turn dispatch approval**: the user approves visible drafts with
  explicit domain language such as `yes, please send both emails` before any
  envelope exists. CTM sends a one-turn approval scoped to the matching external
  domain. Wall-E may execute same-turn tool calls only if validation has no
  issues and the only remaining confirmation is
  `external_action_confirmation_required`.

## Approval Tiers

Wall-E uses two approval tiers:

1. **Local permission approval** is for reversible project-local work such as
   shell test commands, builds, and file edits. The UI may offer `Yes`,
   `Always yes for this project/pattern`, and `No`. The persisted rule is scoped
   to the project plus the permission pattern, never to vague model wording.
2. **External action approval** is for real-world side effects: email, calendar,
   Slack, SMS, reminders, and notifications. These approvals are exact-payload
   approvals. The default choices are `Approve once` or `Cancel`; broad
   `always yes` is intentionally disabled because the next payload may target a
   different person, account, calendar, or time.

This keeps the fast Claude Code-style flow for local coding work without giving
models an ambient ability to send messages or schedule events.

## Side-Effect Gateway

All side-effecting routes pass through a host-side gateway before normal
permission checks. The gateway blocks shell, AppleScript, and generic MCP calls
that try to dispatch external actions directly, for example:

- `osascript`/JXA creating Calendar events.
- `gws calendar events insert`.
- Gmail `messages.send` through shell, curl, or MCP.
- Slack `chat.postMessage` through shell or MCP.

The gateway returns a structured `external_action_gateway` tool result that
tells the model which dedicated tool to use. The action is not executed, and the
model is instructed not to claim success.

Sequence:

1. Wall-E blocks `mail_send`, `mail_reply`, `calendar_create`, and other external actions and
   emits a tool result with `external_action: true`, `blocked: true`, `action.id`,
   and `action.payloadHash`.
2. CTM persists that tool result in the Wall-E session JSONL as part of the
   assistant turn.
3. On the next user message, CTM reads the latest assistant turn. If the message
   is a narrow confirmation such as `go ahead`, `send it`, or `schedule it`, CTM
   builds `externalActionApprovals`.
4. CTM posts those approvals to `/api/wall-e/chat`. The chat API forwards them
   to `chat()`.
5. `chat()` validates that each approval id/hash still matches the reconstructed
   tool input, checks validation issues, applies a session-scoped idempotency
   guard, executes the local tool, runs read-after-write verification where the
   connector supports it, and emits normal `tool_call` / `tool_result` progress
   events.

This means provider wording is not part of the safety decision. DeepSeek,
Anthropic, OpenAI, and other providers all use the same envelope replay path.

## Current Guards

- Draft/compose/write style requests stage a preview instead of dispatching.
- Explicit dispatch requests still require a matching approved envelope.
- Email sends require recipient, subject, body, and sender-account validation.
- Email replies use `mail_reply`, not `mail_send`. `mail_reply` is anchored to
  the original Gmail `message_id`, derives recipients from the source message,
  and sends through Gmail `messages.send` with the original `threadId`,
  `References`, `In-Reply-To`, and a matching reply subject. This prevents new
  threads and invented recipients.
- If the user asks to reply/respond to an existing email but the model tries
  `mail_send`, the controller returns a blocking validation issue instructing
  the model to use `mail_reply`.
- Email bodies that invite a phone call without a phone number are flagged.
- Calendar creation resolves the requested calendar against live Google/macOS
  calendar lists. Missing or ambiguous calendar names return structured
  clarification data with available calendars instead of raw AppleScript errors
  or guessed calendars.
- Natural calendar aliases such as `Work` and `Personal` resolve to writable
  primary work/personal Google calendars when there is exactly one matching
  account.
- Retry tracing classifies outbound external actions as unsafe side effects.
- CTM learned approval rules cannot auto-allow external action tools.
- CTM converts the next prompt into an exact approval when the latest pending
  action group contains staged external actions and the user text is a clear
  external action confirmation. It can also create a one-turn domain-scoped
  approval for explicit dispatch confirmations such as `yes, please send both
  emails`. Bare confirmations such as `yes` do not create current-turn
  approvals without a pending action.
- Coding prompts such as `go ahead with the fix` do not approve mail/calendar
  side effects.
- Approved envelopes are idempotent per Wall-E session and payload hash to avoid
  accidental duplicate sends from double-submit or retry.
- Calendar approval envelopes preserve `account`, `source`, `calendarId`,
  `calendar`, `location`, and time fields so replay cannot silently fall back to
  a different provider or calendar.
- Google Calendar creates verify with `calendar.events.get` against the same
  account, calendar id, and event id before Wall-E can summarize the action as
  verified.
- Gmail sends and replies verify with `gmail.messages.get`. Replies keep the
  original message id separately from the sent reply id so thread evidence is
  not confused with source evidence.
- Final summary guards treat `sent` or `created` without `verified` evidence as
  incomplete. Shell stdout, AppleScript UIDs, or model prose do not count as
  external-action completion evidence.

## Coding-Agent Completion Contract

Wall-E coding sessions use the same evidence rule. If an action-oriented coding
prompt caused file edits, the agent must run a relevant verification tool before
ending with a success summary. Accepted evidence includes tests, lint, build,
typecheck, Playwright/browser screenshots for UI work, or `git diff --check`
when no project-specific verifier exists. If verification is impossible, the
agent must say so with tool-backed evidence instead of claiming success.

## Tests

Focused regressions:

- `claude-task-manager/tests/walle-permission-policy.test.js`
- `claude-task-manager/tests/walle-external-actions.test.js`
- `wall-e/tests/external-action-controller.test.js`
- `wall-e/tests/external-action-gateway.test.js`
- `wall-e/tests/local-tools-gws-live-files.test.js`
- `wall-e/tests/coding-orchestrator.test.js`
- `wall-e/tests/coding-stream-processor.test.js`
- `wall-e/tests/execution-trace.test.js`
- `wall-e/tests/chat.test.js` with `stages a draft email` and
  `validated same-turn email dispatch`

For realistic prompt validation, run the Wall-E chat loop with a disposable data
directory and a mock provider that attempts `mail_send`. The expected tool result
is `decision: "stage_preview"` and `sent: false` for draft-only prompts. For a
current-turn approval prompt, the expected tool result is a verified executor
result, not a blocked confirmation envelope.
