feat(exec): add session-based trust windows for exec approvals#39126
feat(exec): add session-based trust windows for exec approvals#39126keylimesoda wants to merge 10 commits intoopenclaw:mainfrom
Conversation
Grant time-bounded unrestricted exec access with: openclaw approvals trust --minutes 15 Rebased onto current main (v2026.3.3+). Original PR openclaw#30038 was auto-closed without merge. Changes adapted to new resolveExecHostApprovalContext() pattern. - TrustWindow type and resolver override (pure read, no side effects) - CLI trust/untrust commands with TTY enforcement (agent self-escalation prevention) - Gateway RPC endpoints (exec.approvals.trust / exec.approvals.untrust) - Discord /trust and /untrust slash commands (owner-gated) - Interactive confirmation prompt and max duration cap (60m default, 480m absolute) - JSONL audit trail of commands during trust period - Trust status line in message footer - 78 tests passing (7 resolver + 5 CLI + trust-audit suite) Replaces the need to permanently set security: off.
Greptile SummaryThis PR implements session-based trust windows for exec approvals, allowing operators to temporarily grant unrestricted exec access via CLI ( Key changes:
The security model and core trust window logic are sound and well-tested (78 tests). However, there are three issues worth addressing before merge: the Discord command's functional discrepancy (advertised vs. enforced max), the missing Confidence Score: 3/5
Last reviewed commit: cd3e26f |
| description: "Duration in minutes (default: 60, max: 480)", | ||
| type: ApplicationCommandOptionType.Number, | ||
| required: false, | ||
| }, | ||
| { | ||
| name: "agent", | ||
| description: "Agent id (default: main)", | ||
| type: ApplicationCommandOptionType.String, | ||
| required: false, | ||
| }, | ||
| ]; | ||
|
|
||
| private ctx: TrustCommandContext; | ||
|
|
||
| constructor(ctx: TrustCommandContext) { | ||
| super(); | ||
| this.ephemeral = ctx.ephemeralDefault; | ||
| if (ctx.guildId) { | ||
| this.guildId = ctx.guildId; | ||
| } | ||
| this.ctx = ctx; | ||
| } | ||
|
|
||
| async run(interaction: CommandInteraction) { | ||
| if (!isOwnerAuthorized(interaction, this.ctx.discordConfig)) { | ||
| await interaction.reply({ | ||
| content: "⛔ You are not authorized to manage trust windows.", | ||
| ephemeral: true, | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| const minutes = interaction.options.getNumber("minutes") ?? DEFAULT_MAX_TRUST_MINUTES; | ||
| const agentId = interaction.options.getString("agent")?.trim() || "main"; | ||
|
|
||
| try { | ||
| const result = (await callGatewayRpc(this.ctx.cfg, "exec.approvals.trust", { | ||
| agentId, | ||
| minutes, | ||
| grantedBy: `discord:${interaction.user?.id ?? "unknown"}`, | ||
| })) as { ok: boolean; agentId: string; expiresAt?: number; message?: string }; |
There was a problem hiding this comment.
Discord /trust advertises 480m max but enforces 60m
The option description says "max: 480" but the callGatewayRpc call doesn't pass force: true. When users try /trust minutes:120 or any value over 60, grantTrustWindow rejects it with: Duration exceeds default cap (60m). Use force to allow up to 480m. — contradicting the advertised max.
To fix, either:
- Update the description to match the actual enforced max (60m), or
- Add a
forceoption to the Discord command and pass it through:
| description: "Duration in minutes (default: 60, max: 480)", | |
| type: ApplicationCommandOptionType.Number, | |
| required: false, | |
| }, | |
| { | |
| name: "agent", | |
| description: "Agent id (default: main)", | |
| type: ApplicationCommandOptionType.String, | |
| required: false, | |
| }, | |
| ]; | |
| private ctx: TrustCommandContext; | |
| constructor(ctx: TrustCommandContext) { | |
| super(); | |
| this.ephemeral = ctx.ephemeralDefault; | |
| if (ctx.guildId) { | |
| this.guildId = ctx.guildId; | |
| } | |
| this.ctx = ctx; | |
| } | |
| async run(interaction: CommandInteraction) { | |
| if (!isOwnerAuthorized(interaction, this.ctx.discordConfig)) { | |
| await interaction.reply({ | |
| content: "⛔ You are not authorized to manage trust windows.", | |
| ephemeral: true, | |
| }); | |
| return; | |
| } | |
| const minutes = interaction.options.getNumber("minutes") ?? DEFAULT_MAX_TRUST_MINUTES; | |
| const agentId = interaction.options.getString("agent")?.trim() || "main"; | |
| try { | |
| const result = (await callGatewayRpc(this.ctx.cfg, "exec.approvals.trust", { | |
| agentId, | |
| minutes, | |
| grantedBy: `discord:${interaction.user?.id ?? "unknown"}`, | |
| })) as { ok: boolean; agentId: string; expiresAt?: number; message?: string }; | |
| options: CommandOptions = [ | |
| { | |
| name: "minutes", | |
| description: "Duration in minutes (default: 60, max: 480 with force)", | |
| type: ApplicationCommandOptionType.Number, | |
| required: false, | |
| }, | |
| { | |
| name: "force", | |
| description: "Allow exceeding default 60m cap (up to 480m)", | |
| type: ApplicationCommandOptionType.Boolean, | |
| required: false, | |
| }, | |
| { | |
| name: "agent", | |
| description: "Agent id (default: main)", | |
| type: ApplicationCommandOptionType.String, | |
| required: false, | |
| }, | |
| ]; |
And in run():
const force = interaction.options.getBoolean("force") ?? false;
const result = (await callGatewayRpc(this.ctx.cfg, "exec.approvals.trust", {
agentId,
minutes,
force,
grantedBy: `discord:${interaction.user?.id ?? "unknown"}`,
})) ...Prompt To Fix With AI
This is a comment left during a code review.
Path: src/discord/monitor/trust-command.ts
Line: 111-151
Comment:
**Discord `/trust` advertises 480m max but enforces 60m**
The option description says "max: 480" but the `callGatewayRpc` call doesn't pass `force: true`. When users try `/trust minutes:120` or any value over 60, `grantTrustWindow` rejects it with: `Duration exceeds default cap (60m). Use force to allow up to 480m.` — contradicting the advertised max.
To fix, either:
1. Update the description to match the actual enforced max (60m), or
2. Add a `force` option to the Discord command and pass it through:
```suggestion
options: CommandOptions = [
{
name: "minutes",
description: "Duration in minutes (default: 60, max: 480 with force)",
type: ApplicationCommandOptionType.Number,
required: false,
},
{
name: "force",
description: "Allow exceeding default 60m cap (up to 480m)",
type: ApplicationCommandOptionType.Boolean,
required: false,
},
{
name: "agent",
description: "Agent id (default: main)",
type: ApplicationCommandOptionType.String,
required: false,
},
];
```
And in `run()`:
```ts
const force = interaction.options.getBoolean("force") ?? false;
const result = (await callGatewayRpc(this.ctx.cfg, "exec.approvals.trust", {
agentId,
minutes,
force,
grantedBy: `discord:${interaction.user?.id ?? "unknown"}`,
})) ...
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Good catch. Adding a force boolean option to the Discord command and threading it through the gateway RPC call. Description will be updated to (default: 60, max: 480 with force).
Status: Will fix in next commit.
| "exec.approvals.trust", | ||
| "exec.approvals.untrust", |
There was a problem hiding this comment.
exec.approvals.trust.status not advertised in features list
The method is fully implemented in execApprovalsHandlers and called by the TUI, but it's missing from BASE_METHODS. While the gateway router still handles requests to it, clients cannot discover it via the hello-ok features payload.
| "exec.approvals.trust", | |
| "exec.approvals.untrust", | |
| "exec.approvals.trust", | |
| "exec.approvals.untrust", | |
| "exec.approvals.trust.status", |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-methods-list.ts
Line: 29-30
Comment:
**`exec.approvals.trust.status` not advertised in features list**
The method is fully implemented in `execApprovalsHandlers` and called by the TUI, but it's missing from `BASE_METHODS`. While the gateway router still handles requests to it, clients cannot discover it via the `hello-ok` features payload.
```suggestion
"exec.approvals.trust",
"exec.approvals.untrust",
"exec.approvals.trust.status",
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Correct — exec.approvals.trust.status should be listed for feature discovery. Will add it in the next commit.
Status: Will fix in next commit.
| if (!result.ok) { | ||
| respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, result.error)); | ||
| return; | ||
| } | ||
| respond(true, { ok: true, expiresAt: result.expiresAt, agentId: result.agentId }, undefined); |
There was a problem hiding this comment.
Inconsistent error response between trust and untrust handlers
When grantTrustWindow fails, this handler uses respond(false, ..., errorShape(...)), which causes the gateway client to reject the promise with a thrown Error. The CLI's if (!result?.ok) check (line 824 in exec-approvals-cli.ts) becomes dead code — errors throw instead of resolving to { ok: false }.
By contrast, exec.approvals.untrust (line 267) uses respond(true, { ok: false, ... }), which resolves cleanly and is handled by if (!result?.ok). This asymmetry also means the CLI test mocks (which use mockResolvedValueOnce({ ok: false, ... })) don't match the actual gateway behavior for the trust handler.
Make both consistent by using the application-level error pattern:
| if (!result.ok) { | |
| respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, result.error)); | |
| return; | |
| } | |
| respond(true, { ok: true, expiresAt: result.expiresAt, agentId: result.agentId }, undefined); | |
| if (!result.ok) { | |
| respond(true, { ok: false, agentId: p.agentId?.trim() || "main", message: result.error }, undefined); | |
| return; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-methods/exec-approvals.ts
Line: 249-253
Comment:
**Inconsistent error response between `trust` and `untrust` handlers**
When `grantTrustWindow` fails, this handler uses `respond(false, ..., errorShape(...))`, which causes the gateway client to reject the promise with a thrown `Error`. The CLI's `if (!result?.ok)` check (line 824 in `exec-approvals-cli.ts`) becomes dead code — errors throw instead of resolving to `{ ok: false }`.
By contrast, `exec.approvals.untrust` (line 267) uses `respond(true, { ok: false, ... })`, which resolves cleanly and is handled by `if (!result?.ok)`. This asymmetry also means the CLI test mocks (which use `mockResolvedValueOnce({ ok: false, ... })`) don't match the actual gateway behavior for the `trust` handler.
Make both consistent by using the application-level error pattern:
```suggestion
if (!result.ok) {
respond(true, { ok: false, agentId: p.agentId?.trim() || "main", message: result.error }, undefined);
return;
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Valid issue. The trust handler uses respond(false, ..., errorShape(...)) which throws on the client side, while untrust uses respond(true, { ok: false, ... }) which resolves cleanly. Will switch to the application-level error pattern (respond(true, { ok: false, ... })) for consistency — matches what the CLI expects.
Status: Will fix in next commit.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cd3e26f63e
ℹ️ 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".
| for (const guildId of Object.keys(guildEntries)) { | ||
| const trustCtx = { |
There was a problem hiding this comment.
Filter guild config keys before creating guild-scoped commands
This loop uses raw guildEntries keys as guildId, but Discord guild config keys are not guaranteed to be Snowflake IDs (slug keys are accepted and * defaults are preserved in guild entry resolution). When a slug or * is present, trust/untrust commands are built with invalid guildId values, which can cause slash-command deploy failures and leave the new trust commands unavailable in otherwise valid guild setups.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Already addressed in commit d79678b. The loop now includes a Snowflake regex filter:
if (!/^\\d{17,20}$/.test(guildId)) {
continue;
}This skips slug keys and wildcards. Verified working in production.
Status: ✅ Resolved
| grantedBy?: string; | ||
| force?: boolean; | ||
| }): GrantTrustResult { | ||
| const agentId = params.agentId?.trim() || "main"; |
There was a problem hiding this comment.
Normalize agent IDs before storing trust-window cache keys
Trust windows are keyed by the raw trimmed agentId here, but runtime agent resolution normalizes IDs (including lowercasing) before exec policy checks. If an operator grants trust with mixed-case input (for example --agent MyAgent), exec lookups for myagent miss this cache entry, so the trust window does not actually apply and later revocation with normalized IDs can report no active window.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid concern. Agent IDs should be normalized (lowercased + trimmed) before cache key storage so that --agent MyAgent and runtime lookups for myagent hit the same entry. Will normalize in grantTrustWindow, revokeTrustWindow, getTrustWindow, and the status handler.
Status: Will fix in next commit.
|
Note: This is not MacOS, but is OS agnostic, despite the odd tagging. |
- Fix Discord /trust command: description now accurately reflects the 60m limit enforced without force (was advertising 480m max) - Add exec.approvals.trust.status to server-methods-list.ts for client-side API discovery - Standardize error handling: untrust handler now uses respond(false) pattern matching trust handler, eliminating dead code in CLI and inconsistent error shapes - Update CLI untrust to handle errors via catch (matching trust pattern) - Update tests to use mockRejectedValueOnce for error cases Addresses Greptile review on openclaw#39126
Adds kernel-level filesystem isolation for safeBins-approved commands using
bubblewrap (bwrap) unprivileged user namespaces on Linux.
When tools.exec.nsSandbox.mode is set to 'bwrap', commands that pass
safeBins validation are executed inside a namespace where only approved
binaries, system libraries, and the working directory are visible.
This prevents bypassing the safeBins fence via absolute paths
(e.g. /usr/bin/rm) — the binary simply doesn't exist in the sandbox.
Key design:
- Mounts only safeBins-approved binaries (resolved from trustedSafeBinDirs)
- System libs (/lib, /usr/lib) mounted read-only for dynamic linking
- Working directory mounted read-write
- Shell binaries (sh, bash, env) always available for sh -c execution
- --unshare-all --share-net --die-with-parent for isolation with networking
- Trust windows bypass the sandbox entirely (no bwrap when trusted)
- Docker sandbox takes precedence (bwrap only for host=gateway)
- PTY commands skip bwrap (terminal allocation unreliable in namespaces)
- Extra bind mounts configurable via nsSandbox.extraBinds
Config:
tools.exec.nsSandbox.mode: 'bwrap' | 'none' (default: 'none')
tools.exec.nsSandbox.extraBinds: [{ src, dest?, writable? }]
Composes with trust windows (PR openclaw#39126): trust active = no sandbox;
no trust + safeBins match = bwrap sandbox; no trust + not safeBins = ask.
AI-assisted: Yes (Tommy/Claude Opus 4.6). All code reviewed and understood.
Tests: 18 unit tests covering arg building, mount deduplication,
config normalization, binary resolution. 65/65 exec tests passing.
- Lowercase agentId in grantTrustWindow, revokeTrustWindow, and getTrustWindow to match runtime normalization — prevents cache misses when mixed-case agent IDs are used (e.g., --agent MyAgent) - Filter non-Snowflake guild config keys (slugs, wildcards) before creating guild-scoped trust/untrust slash commands — prevents deploy failures with invalid guildId values Addresses Codex review on openclaw#39126
Review Feedback AddressedPushed two follow-up commits addressing all review feedback: Commit
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d79678be54
ℹ️ 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".
| const { entries } = loadTrustAudit({ agentId: params.agentId }); | ||
| const total = entries.length; | ||
| const failed = entries.filter( | ||
| (entry) => typeof entry.code === "number" && entry.code !== 0, |
There was a problem hiding this comment.
Scope trust audit summaries to the current trust window
summarizeTrustAudit receives startedAt/endedAt from revokeTrustWindow, but it computes totals and command previews from the entire file without filtering by timestamp. If an operator uses --keep (or a previous cleanup fails), the next revoke/expiry summary will include stale commands from earlier windows, so the reported command/failure counts and listed commands are inaccurate for the window being revoked.
Useful? React with 👍 / 👎.
| .action(async (opts: TrustOpts) => { | ||
| try { | ||
| // TTY gate: prevent agent self-escalation | ||
| if (!process.stdin.isTTY) { |
There was a problem hiding this comment.
Strengthen trust escalation guard beyond stdin TTY checks
The new self-escalation guard only checks process.stdin.isTTY, which does not prove a human is present. Automated exec sessions can run commands in PTY mode (the exec tool supports pty=true), causing this check to pass and allowing non-interactive callers to run openclaw approvals trust --yes; this bypasses the intended protection against agent-initiated trust grants.
Useful? React with 👍 / 👎.
- Add min_value/max_value to Discord /trust slash command option (enforces 1-60m client-side) - Strengthen CLI self-escalation guard: check for OPENCLAW_SESSION_KEY/AGENT_ID env vars - Filter trust audit entries by startedAt/endedAt time bounds in summarizeTrustAudit - trust.status already in BASE_METHODS list (verified) - trust/untrust error shapes already consistent (verified)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 28ca7710da
ℹ️ 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".
| return allowListMatches( | ||
| ownerAllowList, | ||
| { id: sender.id, name: sender.name, tag: sender.tag }, | ||
| { allowNameMatching: isDangerousNameMatchingEnabled(discordConfig) }, | ||
| ); |
There was a problem hiding this comment.
Reject wildcard allowlists when authorizing trust commands
This owner check accepts allowFrom: ["*"] as authorized because normalizeDiscordAllowList sets allowAll and allowListMatches returns true for any user, so any member who can run slash commands can grant/revoke trust windows and bypass exec approvals. In deployments that use wildcard allowlists for normal chat access, this is a privilege-escalation path and breaks the intended “owner-gated” security model.
Useful? React with 👍 / 👎.
| if (nativeEnabled && guildEntries) { | ||
| for (const guildId of Object.keys(guildEntries)) { |
There was a problem hiding this comment.
Register trust commands even without explicit guild entries
Trust/untrust commands are only added inside the if (nativeEnabled && guildEntries) block, so accounts with no guilds map (or wildcard-only configs that don’t provide concrete guild keys) never register these commands at all. In those common setups, /trust and /untrust are unavailable despite native commands being enabled.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 324bf7face
ℹ️ 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".
| import path from "node:path"; | ||
| import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core"; | ||
| import { type ExecHost, loadExecApprovals, maxAsk, minSecurity } from "../infra/exec-approvals.js"; | ||
| feat/trust-windows-v2 |
There was a problem hiding this comment.
Remove stray merge text from exec tool module
The top-level expression feat/trust-windows-v2 (and the following main token) is accidental merge text that gets evaluated when this module loads; in runtime paths that transpile without full type-checking, importing src/agents/bash-tools.exec.ts will throw a ReferenceError before any exec logic runs. This breaks all command execution flows that depend on this module.
Useful? React with 👍 / 👎.
| const trustWindow = getTrustWindow(agentId); | ||
| respond( |
There was a problem hiding this comment.
Filter expired trust windows in trust status RPC
exec.approvals.trust.status returns whatever is in getTrustWindow(...) without checking expiresAt, so it can report a window as present/active even after it has already stopped affecting exec policy (the enforcement path only treats it as active while Date.now() < expiresAt). Clients that rely on this RPC can therefore show or act on stale elevated-access state.
Useful? React with 👍 / 👎.
…or consistency
- Reject wildcard allowlists ('*') in trust command authorization
- Add 'force' option to Discord /trust command (allows up to 480m)
- Update max_value to ABSOLUTE_MAX_TRUST_MINUTES (480)
- Make trust/untrust error responses consistent (application-level pattern)
Addresses review comments on PR openclaw#39126:
- greptile: Discord /trust 480m max vs 60m enforcement
- greptile: Inconsistent error response trust vs untrust
- codex: Wildcard allowlist bypass (security fix)
Previously resolved in d79678b:
- codex: Guild key Snowflake filtering
- codex: Agent ID normalization (toLowerCase)
- trust.status in BASE_METHODS
AI-assisted: yes
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59e45ff9db
ℹ️ 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".
| url: gatewayUrl, | ||
| clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, | ||
| clientDisplayName: "Discord Trust Command", | ||
| mode: GATEWAY_CLIENT_MODES.BACKEND, | ||
| scopes: ["operator.admin"], |
There was a problem hiding this comment.
Resolve gateway auth before invoking Discord trust RPCs
callGatewayRpc builds a GatewayClient with only URL and scopes, but never resolves/passes gateway token/password; in token/password auth deployments this backend operator connection is rejected, so /trust and /untrust fail even for authorized owners. The existing Discord exec-approvals path resolves credentials before creating GatewayClient, and this path needs the same auth wiring to work outside insecure/no-auth setups.
Useful? React with 👍 / 👎.
| })) as { ok: boolean; agentId?: string; summary?: string }; | ||
|
|
||
| defaultRuntime.log(`🔒 Trust window revoked for agent "${agentKey}".`); | ||
| defaultRuntime.log(` Normal approval policy restored.`); |
There was a problem hiding this comment.
Check untrust RPC result before reporting revocation
This command prints a successful revocation immediately after the RPC call without checking result.ok. The server method returns { ok: false, message } for cases like no active window or invalid agent, so the CLI can report “Trust window revoked” when nothing changed, which misleads operators about the current approval state.
Useful? React with 👍 / 👎.
| if (typeof params.endedAt === "number" && e.ts > params.endedAt) { | ||
| return false; |
There was a problem hiding this comment.
Count commands that finish after trust-window end
The summary filter excludes any entry with e.ts > endedAt, but trust-audit entries are recorded when command execution completes (the exec hosts append after process completion). Commands started during an active trust window but finishing after expiry/revocation are therefore omitted from the window summary, producing inaccurate command and failure totals.
Useful? React with 👍 / 👎.
Leftover branch name labels from a merge resolution were left as bare lines, causing 'ReferenceError: feat is not defined' and failing all CI checks.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 574233da88
ℹ️ 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".
| return allowListMatches( | ||
| ownerAllowList, | ||
| { id: sender.id, name: sender.name, tag: sender.tag }, | ||
| { allowNameMatching: isDangerousNameMatchingEnabled(discordConfig) }, | ||
| ); |
There was a problem hiding this comment.
Enforce ID-only owner checks for trust commands
isOwnerAuthorized claims trust actions require explicit owner IDs, but it still enables name/tag matching via allowNameMatching. When dangerousNameMatching is turned on and allowFrom contains non-ID entries, another user can match the same normalized name/tag and run /trust or /untrust, which is a privilege-escalation path for this admin operation.
Useful? React with 👍 / 👎.
| await interaction.reply({ | ||
| content: `🔒 **Trust window revoked** for agent "${agentId}".${summaryBlock}${auditNote}`, | ||
| ephemeral: true, |
There was a problem hiding this comment.
Check untrust RPC result before confirming revocation
This path always sends a “trust window revoked” reply even when the RPC returns { ok: false, message } (for example, no active window or invalid agent). Because the handler does not branch on result.ok, operators can get a false-success confirmation while the trust state remains unchanged.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 57a61a47fa
ℹ️ 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".
| params: ExecuteNodeHostCommandParams, | ||
| ): Promise<AgentToolResult<ExecToolDetails>> { | ||
| const { hostSecurity, hostAsk, askFallback } = resolveExecHostApprovalContext({ | ||
| let { hostSecurity, hostAsk, askFallback } = resolveExecHostApprovalContext({ |
There was a problem hiding this comment.
Apply node trust override before deny check
executeNodeHostCommand calls resolveExecHostApprovalContext before evaluating trustWindowActive, but the shared resolver throws immediately when host security resolves to deny (src/agents/bash-tools.exec-host-shared.ts). In node-host setups that intentionally default to security=deny, an active trust window never gets a chance to elevate to full/off, so trusted sessions still fail with exec denied instead of honoring the new trust-window feature.
Useful? React with 👍 / 👎.
…dow deny override - Add missing OpenClawConfig type import in tui.ts (merge artifact) - Check result.ok before confirming untrust success in Discord command - Remove early deny throw from resolveExecHostApprovalContext so callers can evaluate trust windows before rejecting (fixes trust override on node-host with security=deny)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 286bd4c657
ℹ️ 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".
| const result = await client.getTrustStatus({ agentId: currentAgentId }); | ||
| trustWindow = (result.trustWindow as TrustWindow | undefined) ?? null; |
There was a problem hiding this comment.
Ignore stale trust-status responses after agent switches
refreshTrustWindow requests status with the current agentId but then unconditionally writes trustWindow/trustWindowFetchedAt when the promise resolves. If the user switches agents while that request is in flight, the old response overwrites the cache for the new agent, and the 30s throttle suppresses a fresh fetch, so the footer can show the wrong trust state for up to 30 seconds.
Useful? React with 👍 / 👎.
|
This pull request has been automatically marked as stale due to inactivity. |
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. This broad stale PR should close as superseded: current main still lacks trust windows, but the remaining work is now split across narrower open trust-window and slash-command threads, while this branch still targets deleted Discord shim paths and carries security-sensitive review blockers. So I’m closing this here and keeping the remaining discussion on the canonical linked item. Review detailsBest possible solution: Close this broad branch and continue the feature through #46956 for the core trust-window foundation, #47696 for command UX, and #62284 for product scope, porting any useful audit/status details into whichever design maintainers choose. Do we have a high-confidence way to reproduce the issue? Not applicable as a user bug report; this is a feature PR. The blockers are source-reproducible from the PR diff and current main: current main lacks trust windows, the PR targets removed Is this the best way to solve the issue? No. The broad patch is not the best solution as-is because the same remaining work is now split into narrower active threads and this branch still mixes stale Discord paths with security-sensitive trust elevation behavior. Security review: Security review needs attention: The diff adds temporary unrestricted exec authority and still has concrete owner-authorization and Gateway-auth wiring concerns.
What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 605e89468ebf. |
Summary
Grant time-bounded unrestricted exec access from CLI or Discord:
Or from Discord:
/trust 15Auto-reverts to normal security policy on expiry. Replaces the need to permanently set
security: off.What changed
getTrustWindow()is a pure read, no side effects. In-memory Map is sole authority.exec.approvals.trust/exec.approvals.untrust(operator.admin scope)Security model
Rebased from PR #30038
Original PR was auto-closed without merge. This rebase adapts to the current
resolveExecHostApprovalContext()pattern in the exec host modules. All trust window logic now overrideshostSecurity/hostAskafter the shared resolution call.Testing
npx tsc --noEmitcleanFiles
exec-approvals.ts,trust-audit.tsbash-tools.exec.ts,exec-host-gateway.ts,exec-host-node.tsexec-approvals-cli.tsserver-methods/exec-approvals.ts,protocol/monitor/trust-command.ts,monitor/provider.tsagent-runner-utils.ts,tui/GatewayModels.swift(macOS + shared)