Skip to content

feat(exec): add session-based trust windows for exec approvals#39126

Closed
keylimesoda wants to merge 10 commits intoopenclaw:mainfrom
keylimesoda:feat/trust-windows-v2
Closed

feat(exec): add session-based trust windows for exec approvals#39126
keylimesoda wants to merge 10 commits intoopenclaw:mainfrom
keylimesoda:feat/trust-windows-v2

Conversation

@keylimesoda
Copy link
Copy Markdown

Summary

Grant time-bounded unrestricted exec access from CLI or Discord:

openclaw approvals trust --minutes 15

Or from Discord: /trust 15

Auto-reverts to normal security policy on expiry. Replaces the need to permanently set security: off.

What changed

  • TrustWindow type and in-memory resolvergetTrustWindow() is a pure read, no side effects. In-memory Map is sole authority.
  • CLI trust/untrust commands — TTY-gated to prevent agent self-escalation
  • Gateway RPC endpointsexec.approvals.trust / exec.approvals.untrust (operator.admin scope)
  • Discord /trust and /untrust — Owner-gated slash commands
  • JSONL audit trail — Every command during trust period is logged; summary on expiry
  • Trust status line in message footer
  • Anti-stacking — Cannot extend an active window; must revoke first
  • Max duration cap — 60m default, 480m absolute (even with --force)

Security model

Escalation vector Mitigation
Agent calls RPC directly operator.admin scope required
Agent runs CLI TTY gate (isTTY check)
Agent sends /trust in Discord ownerAllowFrom check
Stale window persists In-memory only; restart clears all

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 overrides hostSecurity/hostAsk after the shared resolution call.

Testing

  • 78 tests passing (trust window resolver, CLI trust/untrust, trust-audit JSONL)
  • npx tsc --noEmit clean
  • 26 files changed, +1838/-22 lines

Files

Area Files
Core exec-approvals.ts, trust-audit.ts
Exec hosts bash-tools.exec.ts, exec-host-gateway.ts, exec-host-node.ts
CLI exec-approvals-cli.ts
Gateway RPC server-methods/exec-approvals.ts, protocol/
Discord monitor/trust-command.ts, monitor/provider.ts
UI agent-runner-utils.ts, tui/
Swift GatewayModels.swift (macOS + shared)
Tests 3 test files, 78 tests

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.
@openclaw-barnacle openclaw-barnacle Bot added channel: discord Channel integration: discord app: macos App: macos app: web-ui App: web-ui gateway Gateway runtime cli CLI command changes agents Agent runtime and tooling size: XL labels Mar 7, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 7, 2026

Greptile Summary

This PR implements session-based trust windows for exec approvals, allowing operators to temporarily grant unrestricted exec access via CLI (openclaw approvals trust --minutes 15) or Discord (/trust 15). The core security model — in-memory Map, TTY gate for CLI, owner-gated Discord commands, anti-stacking, and per-session expiry — is well-designed and consistent with the existing resolveExecHostApprovalContext pattern.

Key changes:

  • src/infra/exec-approvals.ts: New grantTrustWindow / revokeTrustWindow / getTrustWindow functions backed by an in-memory Map; trust window override injected into resolveExecApprovalsFromFile as a pure read with no disk writes
  • src/infra/trust-audit.ts: New JSONL audit trail written at ~/.openclaw/trust-audit-{agentId}.jsonl with 0o600 permissions, redaction, and truncation
  • src/discord/monitor/trust-command.ts: New /trust and /untrust Discord slash commands with isOwnerAuthorized guard; however, the /trust option description advertises "max: 480" but the RPC call omits the force: true parameter, so values over 60m will silently fail with an error message
  • src/gateway/server-methods-list.ts: exec.approvals.trust and exec.approvals.untrust were added, but exec.approvals.trust.status is missing from the advertised methods list
  • src/gateway/server-methods/exec-approvals.ts: Error response shapes are inconsistent — exec.approvals.trust uses respond(false, ...) (causing client-side rejection) while exec.approvals.untrust uses respond(true, { ok: false }) (application-level error), leaving dead code in the CLI error handling path

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 trust.status method in the features list, and the inconsistent error handling between trust and untrust handlers.

Confidence Score: 3/5

  • Core security logic is sound, but three functional inconsistencies need fixing: Discord max duration discrepancy, missing method in advertised features, and inconsistent error handling patterns.
  • The PR's core trust window implementation is well-designed with solid security posture (in-memory only, TTY gates, owner checks, anti-stacking). The 78-test suite covers trust resolution, CLI, and audit trails. However, three issues reduce confidence: (1) Discord /trust command advertises 480m max but enforces 60m due to missing force parameter — a user-facing functional discrepancy; (2) exec.approvals.trust.status is missing from the advertised methods list, breaking client-side API discovery; (3) Trust and untrust handlers use different error response patterns, leaving dead code in CLI and test mocks mismatched to actual behavior. These are fixable issues but should be resolved before merge.
  • src/discord/monitor/trust-command.ts (max duration discrepancy), src/gateway/server-methods-list.ts (missing method), src/gateway/server-methods/exec-approvals.ts (inconsistent error handling)

Last reviewed commit: cd3e26f

Comment thread src/discord/monitor/trust-command.ts Outdated
Comment on lines +111 to +151
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 };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Suggested change
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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +29 to +30
"exec.approvals.trust",
"exec.approvals.untrust",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct — exec.approvals.trust.status should be listed for feature discovery. Will add it in the next commit.

Status: Will fix in next commit.

Comment on lines +249 to +253
if (!result.ok) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, result.error));
return;
}
respond(true, { ok: true, expiresAt: result.expiresAt, agentId: result.agentId }, undefined);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +514 to +515
for (const guildId of Object.keys(guildEntries)) {
const trustCtx = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/infra/exec-approvals.ts Outdated
grantedBy?: string;
force?: boolean;
}): GrantTrustResult {
const agentId = params.agentId?.trim() || "main";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@keylimesoda
Copy link
Copy Markdown
Author

keylimesoda commented Mar 7, 2026

Note: This is not MacOS, but is OS agnostic, despite the odd tagging.

keylimesoda and others added 2 commits March 7, 2026 12:15
- 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
keylimesoda pushed a commit to keylimesoda/openclaw that referenced this pull request Mar 7, 2026
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
@keylimesoda
Copy link
Copy Markdown
Author

Review Feedback Addressed

Pushed two follow-up commits addressing all review feedback:

Commit 2af4c7f — Greptile feedback (3 issues)

  1. Discord /trust max duration discrepancy — Option description now accurately reflects the 60m limit enforced without force (was advertising 480m)
  2. Missing exec.approvals.trust.status in features list — Added to server-methods-list.ts for client-side API discovery
  3. Inconsistent error handling — Standardized untrust handler to use respond(false, ...) matching trust pattern. Updated CLI + Discord command handlers and tests.

Commit d79678b — Codex feedback (2 issues)

  1. Agent ID normalizationgrantTrustWindow, revokeTrustWindow, and getTrustWindow now lowercase agentId to match runtime normalization, preventing cache misses with mixed-case input
  2. Guild config key filtering — Non-Snowflake keys (slugs, wildcards like *) are now filtered out before creating guild-scoped slash commands, preventing deploy failures

All 103 trust-window related tests pass (exec-approvals: 48, CLI: 8, safe-bins: 47).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/infra/trust-audit.ts Outdated
Comment on lines +140 to +143
const { entries } = loadTrustAudit({ agentId: params.agentId });
const total = entries.length;
const failed = entries.filter(
(entry) => typeof entry.code === "number" && entry.code !== 0,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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)
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +39 to +43
return allowListMatches(
ownerAllowList,
{ id: sender.id, name: sender.name, tag: sender.tag },
{ allowNameMatching: isDangerousNameMatchingEnabled(discordConfig) },
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +513 to +514
if (nativeEnabled && guildEntries) {
for (const guildId of Object.keys(guildEntries)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/bash-tools.exec.ts Outdated
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +212 to +213
const trustWindow = getTrustWindow(agentId);
respond(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +64 to +68
url: gatewayUrl,
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
clientDisplayName: "Discord Trust Command",
mode: GATEWAY_CLIENT_MODES.BACKEND,
scopes: ["operator.admin"],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +598 to +601
})) as { ok: boolean; agentId?: string; summary?: string };

defaultRuntime.log(`🔒 Trust window revoked for agent "${agentKey}".`);
defaultRuntime.log(` Normal approval policy restored.`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/infra/trust-audit.ts
Comment on lines +148 to +149
if (typeof params.endedAt === "number" && e.ts > params.endedAt) {
return false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +48 to +52
return allowListMatches(
ownerAllowList,
{ id: sender.id, name: sender.name, tag: sender.tag },
{ allowNameMatching: isDangerousNameMatchingEnabled(discordConfig) },
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +251 to +253
await interaction.reply({
content: `🔒 **Trust window revoked** for agent "${agentId}".${summaryBlock}${auditNote}`,
ephemeral: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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)
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/tui/tui.ts
Comment on lines +766 to +767
const result = await client.getTrustStatus({ agentId: currentAgentId });
trustWindow = (result.trustWindow as TrustWindow | undefined) ?? null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 23, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 26, 2026

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 details

Best 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 src/discord paths, and the trust RPC helper bypasses the normal Gateway credential resolution path.

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.

  • [high] Trust authorization permits mutable-name matching — src/discord/monitor/trust-command.ts:48
    The Discord trust command allows allowListMatches with dangerous name/tag matching enabled, so non-ID allowlist entries can authorize unrestricted exec elevation. This needs stable owner IDs only.
    Confidence: 0.9
  • [medium] Discord trust RPC bypasses normal Gateway auth resolution — src/discord/monitor/trust-command.ts:59
    The Discord command creates GatewayClient directly instead of using the established Gateway call path that resolves credentials, TLS, device identity, method support, and timeouts, making the admin trust RPC unreliable in authenticated deployments.
    Confidence: 0.91

What I checked:

Likely related people:

  • steipete: Recent history shows broad maintenance across exec approvals, Gateway method surfaces, and Discord startup paths touched by the superseding work. (role: recent maintainer; confidence: high; commits: 3f7e6eebc2, 9a100d520d, 61718d2da5; files: src/infra/exec-approvals.ts, src/gateway/server-methods/exec-approvals.ts, extensions/discord/src/monitor/provider.ts)
  • gumadeiras: Recent merged refactors centralized native approval lifecycle, effective policy reporting, and approval not-found handling near the exec approval behavior this feature changes. (role: approval lifecycle maintainer; confidence: medium; commits: d78512b09d, 0ef9383487, ba735d0158; files: src/agents/bash-tools.exec-host-shared.ts, src/auto-reply/reply/commands-approve.ts, src/infra/exec-approvals.ts)
  • scoootscooob: Recent commits moved Discord implementation into extensions/discord and removed the old src/discord shim tree, which is the key stale path blocker in this PR. (role: Discord extension migration owner; confidence: high; commits: 5682ec37fa, 439c21e078, 9ff57ac479; files: extensions/discord/src/monitor/provider.ts, src/discord/monitor/provider.ts, src/auto-reply/reply/commands-approve.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 605e89468ebf.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: macos App: macos app: web-ui App: web-ui channel: discord Channel integration: discord cli CLI command changes gateway Gateway runtime size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant