fix(exec): resolve remote approval regressions#58792
Conversation
There was a problem hiding this comment.
💡 Codex Review
openclaw/src/agents/bash-tools.exec-host-node.ts
Lines 416 to 419 in 94ea2e9
When the no-route cron branch is taken, the code validates askFallback inline but then falls through to the normal invoke path with buildInvokeParams(false, null). That drops the approval outcome, so node-side policy evaluation still sees the call as unapproved and can deny with approval-required (for example ask="always" with askFallback="full"). This means the new headless fallback logic does not actually unblock node-host cron executions.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (match) { | ||
| matches.push(match); | ||
| } | ||
| segmentAllowlistEntries.push(match ?? null); |
There was a problem hiding this comment.
Mark policy-blocked segments as non-durable approvals
The new durable-trust signal is built from segmentAllowlistEntries, but entries are only appended in the non-blocked path. A policy-blocked segment returns early without adding a null marker, so a partially evaluated multi-segment command can be misclassified as fully allow-always durable if earlier segments matched. That can suppress ask:"always" prompts unexpectedly in commands that include blocked segments.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR fixes a cluster of remote-exec approval regressions introduced in the 2026.3.31 release by targeting the shared policy/runtime seams rather than adding more per-channel workarounds. The changes span three broad themes:
Supporting fixes include aligning the Slack and Discord monitor handlers to use their respective canonical resolver functions, fixing Key observations:
Confidence Score: 5/5Safe to merge; all remaining findings are P2 style/accuracy issues with no runtime correctness impact on the primary fixed paths. The PR is well-structured, carries thorough regression tests for every addressed scenario (policy inheritance, durable trust, cron inline resolution, allowlist plan fallback, Discord/Slack inferred approvers), and the core logic changes are consistent across gateway and node-host paths. The single P2 finding is a false-positive in the doctor diagnostic output — it does not affect exec execution or approval decisions at runtime. src/commands/doctor-security.ts — the built-in security default in resolveHostExecPolicy should match the runtime fallback. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/commands/doctor-security.ts
Line: 88-91
Comment:
**Doctor built-in security default disagrees with runtime fallback**
`resolveHostExecPolicy` falls back to `"deny"` when `exec-approvals.json` has no `defaults.security`, but the runtime in `bash-tools.exec.ts` falls back to `"allowlist"` for non-sandbox hosts:
```ts
// runtime (bash-tools.exec.ts)
const configuredSecurity =
defaults?.security ??
approvalDefaults?.security ??
(host === "sandbox" ? "deny" : "allowlist"); // ← "allowlist" for gateway/node
```
Because of this mismatch, if `exec-approvals.json` exists but has no `defaults` block and `tools.exec.security` is `"allowlist"`, the doctor computes `host.security = "deny"` (rank 0), decides `allowlist` (rank 1) is "broader", and emits the warning:
> *"Effective host exec stays security="deny" because the stricter side wins."*
The statement is factually incorrect — the runtime effective security is `"allowlist"` in this scenario, not `"deny"`, so the user is given a misleading hint about what will actually execute.
Fix: use the same non-sandbox fallback as the runtime:
```suggestion
const security = agentEntry?.security ?? defaults?.security ?? "allowlist";
const ask = agentEntry?.ask ?? defaults?.ask ?? "on-miss";
```
If the sandbox case matters here too, a `host` parameter could be threaded in, but `"allowlist"` as the fallback matches the dominant remote-exec path this PR targets.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Merge branch 'main' into fix/exec-approv..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a44ec5e815
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const agentEntry = | ||
| params.agentId && params.approvals.agents && params.approvals.agents[params.agentId] | ||
| ? params.approvals.agents[params.agentId] | ||
| : undefined; | ||
| const defaults = params.approvals.defaults; |
There was a problem hiding this comment.
Account for wildcard host policy in doctor conflict checks
resolveHostExecPolicy only reads agents[agentId] and defaults, but effective runtime policy also merges agents["*"] (see resolveExecApprovalsFromFile). As a result, openclaw doctor can misreport tools.exec/per-agent conflict warnings for configurations that rely on wildcard host policy, either missing real conflicts or flagging incorrect host values.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c09d914eb9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🔒 Aisle Security AnalysisWe found 4 potential security issue(s) in this PR:
1. 🟠 Durable exec approval can bypass allowlist enforcement and shell-wrapper protections after an 'allow-always' on unanalyzable commands
DescriptionDurable approval ( In
This creates an authorization bypass where a user (or automation) can permanently “bless” a command that the allowlist analyzer cannot safely understand, and then execute it later without prompts and without enforced allowlist planning (including the fail-closed shell-wrapper block). Vulnerable flow:
Vulnerable code: if (phase.policy.approvalDecision === "allow-always" && phase.inlineEvalHit === null) {
const patterns = resolveAllowAlwaysPatterns({ segments: phase.segments, ... });
...
if (patterns.length === 0) {
addDurableCommandApproval(phase.approvals.file, phase.agentId, phase.commandText);
}
}And later: if (params.security === "allowlist" && (!analysisOk || !allowlistSatisfied) && !approvedByAsk) {
if (params.durableApprovalSatisfied) {
return { allowed: true, ... };
}
return { allowed: false, eventReason: "allowlist-miss", ... };
}RecommendationDo not grant (or honor) durable approvals unless the command was successfully analyzed and deemed safe under the allowlist model. Recommended changes:
if (
phase.security === "allowlist" &&
phase.policy.approvalDecision === "allow-always" &&
phase.policy.analysisOk &&
phase.policy.allowlistSatisfied &&
phase.inlineEvalHit === null &&
!phase.shellPayload // if present, require explicit approval each time or persist per-inner-exe patterns only
) {
const patterns = resolveAllowAlwaysPatterns({ ... });
if (patterns.length > 0) {
for (const pattern of patterns) {
addAllowlistEntry(file, agentId, pattern, { source: "allow-always" });
}
} else {
// Consider NOT creating durable approvals here; instead require explicit approval on each run.
}
}
if (shellWrapperBlocked) {
// never bypass with durable approval
}
2. 🟠 Headless cron trigger can auto-approve exec when no approval route is available (askFallback=full)
Description
Flow:
Impact:
Vulnerable logic: if (!params.decision) {
if (params.askFallback === "full") {
return { approvedByAsk: true, deniedReason: null, timedOut: true };
}
}combined with: return (
trigger === "cron" &&
unavailableReason === "no-approval-route" &&
preResolvedDecision === null
);RecommendationTreat Suggested approach:
Example fix (deny when no approval route): export function resolveBaseExecApprovalDecision(params: {
decision: string | null;
askFallback: ResolvedExecApprovals["agent"]["askFallback"];
obfuscationDetected: boolean;
unavailableReason?: ExecApprovalUnavailableReason | null;
}) {
if (params.decision === null && params.unavailableReason === "no-approval-route") {
return { approvedByAsk: false, deniedReason: "no-approval-route", timedOut: true };
}
// existing logic...
}Additionally:
3. 🟠 Allowlist mode executes raw command on Windows when enforced command plan unavailable (PATH/CWD hijack risk)
DescriptionIn allowlist security mode, The new logic introduces a path where execution proceeds without an enforced command:
This weakens allowlist guarantees: the allowlist evaluation may have been satisfied based on a particular resolved executable (or intended resolution), but the actual raw execution on Windows can select a different executable due to Windows search order (current working directory before PATH, PATHEXT, etc.) or other resolution differences. This enables a practical allowlist bypass such as placing a malicious Vulnerable flow:
RecommendationFail closed in allowlist mode when an enforced execution plan cannot be built, especially on Windows. Options:
Example fail-closed change (conceptual): if (security === "allowlist" && allowlistSatisfied && !enforcedCommand) {
throw new Error(`exec denied: allowlist execution plan unavailable (${allowlistPlanUnavailableReason})`);
}If a headless/cron flow must proceed, require an explicit durable allowlist entry that binds to an absolute path (not just command text) or require 4. 🟡 Durable exec approvals rely on 64-bit truncated SHA-256, enabling feasible collision-based approval bypass
Description
Because only 64 bits of the SHA-256 digest are used, an attacker who can influence the command text and can get a user to approve one command could potentially create a different command with the same 64-bit prefix (birthday-style collision) and reuse the durable approval to bypass further approval prompts. Key points:
Vulnerable code: function buildDurableCommandApprovalPattern(commandText: string): string {
const digest = crypto.createHash("sha256").update(commandText).digest("hex").slice(0, 16);
return `=command:${digest}`;
}RecommendationIncrease the approval identifier length so collisions are computationally infeasible, or use a keyed construction. Safer options (choose one):
function buildDurableCommandApprovalPattern(commandText: string): string {
const digest = crypto.createHash("sha256").update(commandText).digest("hex");
// or: .slice(0, 32) for 128-bit if size is a concern
return `=command:${digest}`;
}
function buildDurableCommandApprovalPattern(commandText: string, secret: string): string {
const digest = crypto.createHmac("sha256", secret).update(commandText).digest("hex");
return `=command:${digest}`;
}If backwards-compatibility is needed, consider supporting legacy 64-bit patterns only temporarily and migrating to longer tokens on next write. Analyzed PR: #58792 at commit Last updated on: 2026-04-01T08:36:49Z |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b1f32e9c13
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| ); | ||
| } | ||
| if (hostAsk === "on-miss" && hostSecurity === "allowlist" && analysisOk) { | ||
| if ((hostAsk === "always" || hostSecurity === "allowlist") && analysisOk) { |
There was a problem hiding this comment.
Load node approvals even when allowlist analysis fails
The new durable-trust path is currently unreachable for commands whose local allowlist analysis fails, because the exec.approvals.node.get fetch is gated behind analysisOk. This means exact-command allow-always entries (which hasDurableExecApproval is designed to match even when analysis is not OK) are never checked in that case, so valid shell commands that fail static analysis (for example line-continuation forms) still re-prompt or deny on every run instead of reusing durable approval.
Useful? React with 👍 / 👎.
This comment was marked as spam.
This comment was marked as spam.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2ba00325f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| security: hostSecurity, | ||
| analysisOk, | ||
| allowlistSatisfied, | ||
| durableApprovalSatisfied, |
There was a problem hiding this comment.
Keep approval prompt when durable trust can't satisfy allowlist
Including durableApprovalSatisfied in requiresExecApproval here can suppress the approval flow for allowlist-miss commands that only have exact-command durable entries (for example commands that previously got allow-always with no persistable pattern). On the next run, requiresAsk becomes false, but the later allowlist guard still throws exec denied: allowlist miss, so the command is hard-denied instead of re-prompting or honoring the durable decision; this regression is specific to gateway allowlist mode with analysis/allowlist misses.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f77f81069
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| explicit: resolveSlackAccount(params).config.execApprovals?.approvers, | ||
| normalizeApprover: normalizeSlackApproverId, |
There was a problem hiding this comment.
Preserve inferred Slack approvers when explicit list is absent
This change removes allowFrom/dm.allowFrom/defaultTo from Slack approver resolution, so getSlackExecApprovalApprovers now returns an empty list whenever execApprovals.approvers is unset. Existing Slack setups that relied on inferred owner IDs will be treated as having no approvers, which disables native exec-approval handling and pushes requests into fallback/denial paths instead of reaching the intended approver.
Useful? React with 👍 / 👎.
| explicit: | ||
| params.configOverride?.approvers ?? | ||
| resolveDiscordAccount(params).config.execApprovals?.approvers, | ||
| normalizeApprover: (value) => normalizeDiscordApproverId(String(value)), |
There was a problem hiding this comment.
Keep Discord owner-based approver inference
The Discord resolver now passes only explicit approvers into resolveApprovalApprovers, dropping inference from allowFrom, legacy dm.allowFrom, and direct-message defaultTo. That regresses previously working configs where execApprovals.approvers was intentionally omitted, causing approver resolution to become empty and preventing Discord exec approvals from being delivered or accepted.
Useful? React with 👍 / 👎.
Fixes #58710
Fixes #58722
Fixes #58739
Fixes #58752
Fixes #58662
Fixes #46573
Fixes #58769
Addresses #58715
Addresses #58616
Addresses #58691
Addresses #49266
Addresses #43279
Addresses #46846
Addresses #46848
Addresses #42574
Summary
This PR fixes the 2026.3.31 exec-approval regression cluster at the shared policy/runtime seams instead of stacking more per-channel workarounds on top.
Root causes addressed:
src/agents/bash-tools.exec.tsinheritedaskfromexec-approvals.jsonbut notsecurity, sosecurity:"full" + ask:"off"inexec-approvals.jsoncould still collapse back toallowlistand fail remote exec.allowFrom/defaultTo.allow-alwayspersisted only as anallowlist-mode implementation detail, soask:"always"kept re-prompting and remote channels effectively treatedallow-alwayslikeallow-once.allow-alwaysapprovals could still degrade back into repeated prompts.What changed
securityandaskdefaults fromexec-approvals.jsonwhen tool policy is unsetopenclaw doctorwarn whentools.execis broader than~/.openclaw/exec-approvals.json, so stricter host-policy conflicts become explicit instead of silently clampingallow-alwaysas durable user-approved trust and honor it on future gateway and node-host exec callsask:"always", so the durable-trust fix does not weaken explicit policyallow-alwaysapprovals actually stick without requiring a restartVerification
pnpm test -- src/agents/bash-tools.exec.approval-id.test.ts src/plugin-sdk/approval-delivery-helpers.test.ts extensions/slack/src/monitor/exec-approvals.test.ts extensions/discord/src/monitor/exec-approvals.test.ts extensions/slack/src/exec-approvals.test.ts extensions/discord/src/exec-approvals.test.tspnpm test -- src/node-host/invoke-system-run.test.ts src/infra/exec-approvals-allow-always.test.ts src/agents/bash-tools.exec.approval-id.test.ts src/infra/exec-approvals-policy.test.ts src/node-host/exec-policy.test.tspnpm checkpnpm buildNotes
full/offpolicy fromexec-approvals.jsonnow actually takes effect on remote exec, unsupported allowlist-plan cases fall back to explicit approval instead of a hard dead end, and durable exact-command trust covers repeated shell-wrapper reruns that cannot safely persist an executable allowlist entry.