* feat(gateway): propagate approvals to ancestor sessions with replay Squash-rebased #103921 segment onto the native-clients tip on current main. Session-scoped approval events publish sanitized pending/terminal transitions to opted-in session audiences, with authoritative pending replay on stream subscribe and durable-expiry reconciliation through the owning manager. SessionApprovalEvent/Replay wire types export from the approvals owner module; Swift models regenerated. (cherry picked from commit 2d1dcf9747044710111d0c730fc46ba6013a165c) (cherry picked from commit ccf88efd56b513d07599ffcee997bf0cddaf9adc) (cherry picked from commit 8eb33f59f00ec9ee4625259b76bd7ae60d2cd480) (cherry picked from commit c297cbc93c8401e9c79c20f242245a194f5dd236) (cherry picked from commit 93d68f28601ff37d863da4db009504993a22533a) (cherry picked from commit d285ccd8b2f212bb57e814107e7709c896e96b7d) (cherry picked from commit 5c536982231402b1b58d1683794ea9948b32d077) (cherry picked from commit 1646e5b6dc4dd20de54e10f110a5909be18d7d50) (cherry picked from commit 35cf1b705b247fb282f4d56ffe74bd2fb238221f) (cherry picked from commit4f8ef3699d) (cherry picked from commit 74fbdcc99d5fff062d67760a51dba4ee9e84479b) (cherry picked from commit 222b285502681d2be14b1819798e9ee5555f431e) (cherry picked from commit b9e744951f676b600e7c5322bfd04b0e99797c07) (cherry picked from commit9e904efde5) (cherry picked from commit678f2384fa) (cherry picked from commit72842e5cf6) (cherry picked from commit be74c25e80e84a1f065053766f23efb84822e811) (cherry picked from commit93ba8c4b09) (cherry picked from commit8f23761372) * feat(gateway): fail-closed plugin and tool approval gates Squash-rebased #103932 segment onto the ancestor-propagation tip on current main. Plugin node.invoke approvals claim a one-shot allow-once decision before handing execution authority to the policy, so observation or retry cannot replay a consumed approval; sibling tool gates bind approval ids to their originating reviewer identity. (cherry picked from commit 122df0d75281f572c012b6484ed5daf085d1d577) (cherry picked from commit f23d8ac240a8dcf2a42c4daaae962263975a0ae8) (cherry picked from commit 8632fb6436a224dac7a9a9ef0216884530de2c24) (cherry picked from commit d9fe2dd5c53665e5732f5f3da5ed1907a686ade8) (cherry picked from commit bb139f2c8aa36ddad70413e1ef79ff491a6f2ecd) (cherry picked from commit 9b3e056b68ea515c7d4baef269289b6670570480) (cherry picked from commit b11e66b59a7af1f3b08712de06864aed64d349e4) (cherry picked from commit a12916ee592d03dfa35e86a2aaede4476a5d4e5d) (cherry picked from commit d699de840fc8346892b9ae244fe3c3e8c4413032) (cherry picked from commit9b7e5a9608) (cherry picked from commit 4b507593f1b2f10b4496984c84a2803b853e5c5e) (cherry picked from commit 56408a186733c4eeb5aa76bd5907cde7b0cd3d5b) (cherry picked from commit a4051d6d8353d39d6fa0b5d69c36e06c3cd3e796) (cherry picked from commit76829805a7) (cherry picked from commita8b493f934) (cherry picked from commitaceb990597) (cherry picked from commit a29b0e75482470c53a9fb4fe3f204e14cf71868f) (cherry picked from commit2e3be08653) (cherry picked from commitc9ae3d7202) * fix: main-gate repairs for the durable-approvals stack - android: capture createdAtMs on the pending exec-approval write at registration; canonical readback after a refresh that already replaced the visible rows was dropping the approval instead of surfacing the still-pending reconciliation message (#104913 merged without this) - android: generous CI timeout ceilings in GatewayExecApprovalRuntimeTest - agents: hermetic model-discovery test via the plugins/provider-runtime boundary (lazy plugin-runtime resolution hangs vitest workers since #104770) - cli: usage-cost settle-budget test asserts the budget bound on every call instead of pinning the poll count (fast hosts fit a second poll in 50ms) - protocol coverage: allowlist session.approval for ios/android (native review rides exec.approval push/nudge delivery) - docs map, native i18n inventory, plugin-sdk api baseline regenerated
9.5 KiB
summary, title, sidebarTitle, read_when
| summary | title | sidebarTitle | read_when | |||
|---|---|---|---|---|---|---|
| Ask users to approve plugin tool calls and plugin-owned permission prompts | Plugin permission requests | Permission requests |
|
Plugin permission requests let plugin code pause a tool call or plugin-owned
operation until a user approves or denies it. They use the Gateway
plugin.approval.* flow and the same approval UI surfaces that handle chat
approval buttons and /approve commands.
Use plugin permission requests for plugin/app permissions. They do not replace host exec approvals, optional tool allowlists, or Codex's native permission review.
Choose the right gate
Pick the gate that matches the decision point you need:
| Gate | Use it when | What it controls |
|---|---|---|
| Optional tools | A tool should not be visible to the model until the user opts in. | Tool exposure through tools.allow. |
| Plugin permission requests | A plugin hook or plugin-owned operation must ask before one action runs. | Runtime approval through plugin.approval.*. |
| Exec approvals | A host command or shell-like tool needs operator approval. | Host exec policy and durable exec allowlists. |
| Codex native permission requests | Codex asks before native shell, file, MCP, or app-server actions. | Codex app-server or native hook approval handling, routed through plugin approvals when OpenClaw owns the prompt. |
| MCP approval elicitations | A Codex MCP server requests approval for a tool call. | MCP approval responses bridged through OpenClaw plugin approvals. |
Optional tools are a discovery-time gate. Plugin permission requests are a per-call gate. Use both when a sensitive tool should require explicit opt-in before the model can see it and approval before the action runs.
Request approval before a tool call
Most plugin-authored prompts should start in a before_tool_call hook. The hook
runs after the model selects a tool and before OpenClaw executes it:
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "deploy-policy",
name: "Deploy Policy",
register(api) {
api.on("before_tool_call", async (event) => {
if (event.toolName !== "deploy_service") {
return;
}
const environment =
typeof event.params.environment === "string" ? event.params.environment : "unknown";
return {
requireApproval: {
title: "Deploy service",
description: `Deploy service to ${environment}.`,
severity: environment === "production" ? "critical" : "warning",
allowedDecisions:
environment === "production"
? ["allow-once", "deny"]
: ["allow-once", "allow-always", "deny"],
timeoutMs: 120_000,
onResolution(decision) {
console.log(`deploy approval resolved: ${decision}`);
},
},
};
});
},
});
Write prompt text for the person who will approve the action:
- Keep
titleshort and action-focused; the Gateway caps it at 80 characters. - Keep
descriptionspecific and bounded; the Gateway caps it at 512 characters. - Include the action, target, and risk. Do not include secrets, tokens, or private payloads that should not appear in chat approval surfaces.
severitydefaults to"warning"when omitted. Use"critical"only for actions where the wrong decision could cause production damage or data loss.allowedDecisionsdefaults to["allow-once", "allow-always", "deny"]when omitted. Pass["allow-once", "deny"]when persistent trust is unsafe for that action.timeoutMsdefaults to 120000 (2 minutes) and is capped at 600000 (10 minutes) regardless of the requested value.
Decision behavior
OpenClaw creates a pending approval with a plugin: ID, delivers it to the
available approval surfaces, and waits for a decision.
| Decision | Result |
|---|---|
allow-once |
The current call continues. |
allow-always |
The current call continues and the decision is passed to the plugin. |
deny |
The call is blocked with a denied tool result. |
| Timeout | The call is blocked. |
| Cancellation | The call is blocked when the run is aborted. |
| No approval route | The call is blocked because no connected approval surface can resolve it. |
Only the exact allow-once and allow-always decisions permitted by the
request allow execution. Unknown, malformed, mismatched, missing, and timed-out
decisions fail closed. The legacy timeoutBehavior field remains accepted for
plugin compatibility but is deprecated and ignored; do not set it in new hooks.
allow-always is only durable when the requesting plugin or runtime implements
that persistence. For ordinary before_tool_call.requireApproval hooks,
OpenClaw treats allow-once and allow-always as approval decisions for the
current call and passes the resolved value to onResolution. If your plugin
offers allow-always, document and implement exactly what future calls it
trusts.
If the hook also returns params, OpenClaw applies those parameter changes only
after the approval succeeds. A lower-priority hook can still block after a
higher-priority hook requested approval.
allowedDecisions limits the buttons and commands shown to the user. The
Gateway rejects a resolve attempt for any decision the request did not offer.
Route approval prompts
Approval prompts can resolve in local UI surfaces or in chat channels that
support approval handling. To forward plugin approval prompts to explicit chat
targets, configure approvals.plugin:
{
approvals: {
plugin: {
enabled: true,
mode: "targets",
agentFilter: ["main"],
targets: [{ channel: "slack", to: "U12345678" }],
},
},
}
approvals.plugin is independent from approvals.exec. Enabling exec approval
forwarding does not route plugin approval prompts, and enabling plugin approval
forwarding does not change host exec policy.
When a prompt includes manual approval text, resolve it with one of the offered decisions:
/approve <id> allow-once
/approve <id> allow-always
/approve <id> deny
See Advanced exec approvals for the full forwarding model, same-chat approval behavior, native channel delivery, and channel-specific approver rules.
Codex native permissions
Codex native permission prompts can also travel through plugin approvals, but they have different ownership than plugin-authored hooks.
- Codex app-server approval requests route through OpenClaw after Codex review.
- The native hook
permission_requestrelay can ask throughplugin.approval.requestwhen that relay is enabled. - MCP tool approval elicitations route through plugin approvals when Codex marks
_meta.codex_approval_kindas"mcp_tool_call".
See Codex harness runtime for the Codex-specific behavior and fallback rules.
Troubleshooting
The tool says plugin approvals are unavailable. No approval UI or configured
approval route accepted the request. Connect an approval-capable client, use a
channel that supports same-chat /approve, or configure approvals.plugin.
allow-always appears but the next call prompts again. The generic plugin
approval flow does not automatically persist trust for arbitrary hooks. Persist
plugin-owned trust in your plugin after onResolution("allow-always"), or
offer only allow-once and deny.
/approve rejects the decision. The request restricted
allowedDecisions. Use one of the decisions printed in the prompt.
A Discord, Matrix, Slack, or Telegram prompt routes differently from exec
approvals. Plugin approvals and exec approvals use separate config and may use
different authorization checks. Verify approvals.plugin and the channel's
plugin approval support instead of only checking approvals.exec.