openclaw/scripts/e2e/lib/muse-runtime
Ayaan Zaidi ff7c37bed6
fix: let write-only runs receive their native approval verdict (#186461)
## Problem

A native harness (Muse, Codex, ACPX) asks for approval in two calls: `plugin.approval.request`, then `plugin.approval.waitDecision`. When the run's operator authority has only `operator.write` (for example, a turn from an operator device paired without `operator.approvals`, or a subagent that keeps that authority), the Gateway host bridge accepts the request but rejects the wait:

```
Muse Code: Muse approval failed (handlerThrew): missing scope: operator.approvals
```

The approval reaches reviewers, but the run cannot learn the verdict. It fails, and the approval is closed as denied. This was seen on a live Gateway: the approval row was created from a subagent session, and the next call failed.

Core plugin hooks avoid this because in-process they use one single-phase request. The harness host capability always uses two phases, so its wait had no write-scope path.

## Fix

The host bridge now covers the wait for the run's own approval, with the same limits as the request:

- `waitForApproval` carries the same approval owner and agent runtime identity as `requestApproval`.
- The in-process authority, method authorization and approval request authority accept `plugin.approval.waitDecision` from a synthetic owner-bound client with write scope. All three use one predicate, `isHostPluginApprovalCall`.
- Visibility does not change. The wait client gets the runtime connection id of the run that made the request, so a different run still gets "approval expired or not found".
- The bridge still never grants reviewer scope or `plugin.approval.resolve`.

## User impact

Native harness approvals now work on write-only operator runs: the reviewer's Allow or Deny reaches the native runtime. Runs with `operator.approvals` or admin scope are not affected.

## Evidence

- **Regression test:** `src/gateway/server-plugin-approval-request.test.ts` adds "lets a retained write-only run await its own approval verdict". It uses the real plugin approval handlers and in-process dispatch. On `main` it fails with `missing scope: operator.approvals`. With the fix it passes, and a different run's wait still gets `approval expired or not found`.
- **Muse Docker e2e (`scripts/e2e/lib/muse-runtime/run.sh deterministic`):** new step: a Gateway client with only `operator.read` and `operator.write` runs a Muse turn that asks for approval, and an approvals client allows it.
  - Without the fix, the Gateway logs `Muse Code: Muse approval failed (handlerThrew): missing scope: operator.approvals` and the turn fails.
  - With the fix, `PASS write-only operator run receives its native approval verdict`, and Muse gets the `server-allow-id` choice.
  - Unrelated red on unmodified `main`: this lane already fails two older assertions (`initialize` count `2 !== 1`, and `refusal must not start or contact a Muse process`). For these runs only, I skipped the stale `initialize` count locally; that skip is not committed. Unmodified `main` then fails on the same refusal assertion. This PR does not change either one.
- **Revocation during the wait:**
  - Regression test: the wait is parked in the Gateway handler, the run's operator authority is revoked, and a reviewer then allows. The wait rejects with `run authority revoked` and returns no decision.
  - Muse Docker e2e, observed after everything settled: the write-only run asks for approval. `chat.abort` ends the run while its wait is pending, and the harness waits for the run to end. The reviewer then submits Allow, and the harness waits for that call to finish before checking Muse. Observed: the run ended with `{"status":"error","stopReason":"rpc"}`, the Gateway accepted the late `approval.resolve`, and Muse received no `server-allow-id` choice for that approval.
- **Telegram Test Server:** an owner DM and a group turn with fake Muse `fixture:approval` both created the approval on `main`. Turns with no operator authority were never affected; the bug needs a write-only operator authority.
- **Focused suites:** host capability, in-process gateway, plugin approval handlers and agent-runtime tests, native hook relay waits, in-process dispatch authorization, gateway tool, exec approval agent-runtime, Muse harness and Codex approval bridge all pass. `node scripts/check-changed.mjs` passes. Its dead-export scan fails only on files this PR does not touch, which already fail on `main`.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-10-28 11:31:06 +05:30
..
auth.mjs
consent.mjs fix: native runtime asks for approval after setup consent (#185920) 2026-10-27 09:34:46 -04:00
deterministic.mjs fix: let write-only runs receive their native approval verdict (#186461) 2026-10-28 11:31:06 +05:30
gateway.mjs fix: let write-only runs receive their native approval verdict (#186461) 2026-10-28 11:31:06 +05:30
live.mjs
run.sh
setup.sh