# Controlled Action Middleware

Human approval is transport-agnostic in this repo, but the protected operation itself should also stay outside the guest.

The preferred shape is:

1. a sandboxed agent requests a controlled action
2. middleware presents that action to an operator
3. the operator approves or denies it
4. host-side code executes the protected action deterministically if approved
5. host-side code writes status and machine-readable outputs back for agents to read

That keeps approval and execution reusable across Telegram and future transports.

## Core CLI

These are the stable commands the middleware layer targets:

```bash
./rho request status <request-id> --shared-root <path>
./rho request show <request-id> --shared-root <path>
./rho request pending --shared-root <path>
./rho request pending --shared-root <path> --latest

./rho approve <request-id> --shared-root <path> --decision approve
./rho approve <request-id> --shared-root <path> --decision deny
./rho approve --latest --shared-root <path> --decision approve
./rho approve --latest --shared-root <path> --decision deny
```

Those commands own request-level approval state:

- request inspection
- pending-request lookup
- approval manifest writes
- requester notification messages

Middleware should not write approval manifests directly.

## Controlled Actions

The protected operations themselves should not be re-run through an LLM after approval.

Instead, use a host-controlled action record with typed fields such as:

- `action_id`
- `request_id`
- `action_type`
- `input_path`
- `output_path`
- optional typed paths such as `script_path`, `manifest_path`, and `notification_path`

Current action types:

- `run_real_data`
- `release_results`

The host validates the action again outside the sandbox, asks the operator, and if approved executes the action directly.

## Adapter Shape

A middleware adapter can be anything that turns human intent into request approval and controlled action execution:

- Telegram bot
- Slack bot
- HTTP webhook
- desktop UI
- local TUI
- deterministic fixture policy

As long as it ends by calling `rho` where appropriate and only triggers deterministic host handlers for protected actions, the transport remains replaceable.

The host relay in [rho-controlled-action-relay.py](/Users/madhavajay/dev/rho/main/scripts/rho-controlled-action-relay.py) exposes that adapter choice directly:

```bash
python3 scripts/rho-controlled-action-relay.py relay \
  --middleware auto|telegram|fixture \
  ...
```

Current adapters:

- `auto`
  - local deterministic approval through `./rho approve ...`
- `telegram`
  - sends operator-facing text through `rho-telegram` and waits for `/approve` or `/deny`
- `fixture`
  - reads deterministic yes/no decisions from a YAML policy file

Example fixture policy:

```yaml
version: 1
default_decision: approved
actions:
  run_real_data: approved
  release_results: denied
```

Matching precedence is:

1. `action_id`
2. `request_id`
3. `action_type`
4. `default_decision`

## Agent Tool Bridge

For sandboxed Pi agents, the preferred bridge is:

1. the guest calls a strict custom tool such as `request_controlled_action`
2. that tool writes a validated intent file into a mounted outbox directory
3. a host-side relay validates the outbox file again outside the guest
4. the relay calls transport middleware such as Telegram or direct `rho approve`
5. if approved, the relay executes the protected action deterministically on the host
6. the relay writes a status file into a mounted inbox directory
7. the guest can read that status through a second tool such as `get_controlled_action_status`

This keeps:

- model planning inside the sandbox
- transport credentials outside the sandbox
- deterministic protected execution outside the sandbox
- the host as the policy enforcement point

The validation should be strict on both sides:

- typed tool parameters inside Pi
- host-side validation of request ids, agent names, enum actions, and guest path prefixes before any middleware call or host execution

Prompt-facing schema/example files can be mounted read-only into the guest so the model can inspect the expected request shape without being able to modify the canonical validation source.

## Telegram Adapter

[rho-telegram](/Users/madhavajay/dev/rho/main/rho-telegram) is the first adapter.

It adds:

```bash
./rho-telegram send-approval-request <profile> \
  --shared-root <path> \
  --request-id <request-id>

./rho-telegram approval-listen <profile> \
  --shared-root <path> \
  --timeout-seconds 90 \
  --once
```

It supports:

- inline `Approve`, `Deny`, and `Show Details` buttons
- `/approve <request-id>`
- `/deny <request-id>`
- `/show <request-id>`
- `/approve_last`
- `/deny_last`

Internally it still delegates request approval to `./rho approve ...`.

For controlled actions, Telegram is only the human interaction layer. The protected action itself is still executed by host-side relay code after approval.
