feat(exec): add Trust Windows for time-bounded exec approvals#46657
feat(exec): add Trust Windows for time-bounded exec approvals#46657keylimesoda wants to merge 12 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR introduces Trust Windows — time-bounded, in-memory exec permission grants that temporarily set Key findings from this round:
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/cli/exec-approvals-cli.ts
Line: 604-608
Comment:
**Dead "No active trust window" error path in `untrust` catch block**
This error branch can no longer be triggered. Prior to this PR the server returned a "No active trust window" error when `untrust` was called with no active window; the CLI swallowed that particular error and treated it as success. The updated server handler now passes `allowMissing: true` to `revokeTrustWindow`, so the gateway always responds with `ok: true` (never an error) when the window is simply absent. The string-match check is dead code that could mislead future maintainers into thinking this error path is still live.
```suggestion
} catch (err) {
defaultRuntime.error(formatCliError(err));
defaultRuntime.exit(1);
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/cli/exec-approvals-cli.ts
Line: 629-656
Comment:
**`trust-status` CLI command missing agent session guard**
`trust` has both a TTY check and an `OPENCLAW_SESSION_KEY`/`OPENCLAW_AGENT_ID` env-var check. `untrust` has the env-var check. `trust-status` has neither. An agent process that has access to the CLI binary (and whose env vars are not set) can run `openclaw approvals trust-status` from within a session and learn whether a trust window is active for its agent ID, because the server's `isTrustGrantCallerAllowed` will see a legitimate CLI connection and allow it through.
While knowing the trust status doesn't directly enable further escalation (the agent's exec calls are already permitted or denied by the approval layer), it is inconsistent with the "no agent self-escalation" design and lets an agent adapt its behavior based on observed trust state. Consider adding at minimum the env-var guard:
```typescript
.action(async (opts: TrustStatusOpts) => {
try {
if (process.env.OPENCLAW_SESSION_KEY || process.env.OPENCLAW_AGENT_ID) {
exitWithError(
"Trust status queries are blocked from agent sessions. Use a direct terminal session.",
);
}
// ... rest of handler
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/infra/trust-audit.test.ts
Line: 22-35
Comment:
**Temp directory not cleaned up in `afterEach`**
`homeDir` is populated by `makeTempDir()` in `beforeEach` but `afterEach` only restores the `OPENCLAW_HOME` env var — it never removes the temp directory itself. Compare with the existing cleanup pattern used in `exec-approvals.test.ts`:
```typescript
} finally {
process.env.OPENCLAW_HOME = previousHome;
fs.rmSync(homeDir, { recursive: true, force: true });
}
```
Each test run leaks a temp directory. Add `fs.rmSync(homeDir, { recursive: true, force: true })` to the `afterEach` hook:
```suggestion
afterEach(() => {
process.env.OPENCLAW_HOME = previousHome;
if (homeDir) {
fs.rmSync(homeDir, { recursive: true, force: true });
}
});
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 06f4c7c |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 76243c7966
ℹ️ 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".
76243c7 to
1e56d52
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e56d52403
ℹ️ 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".
1e56d52 to
3fc693c
Compare
Apply server-side trust grant caller guard, derive grantedBy from authenticated gateway connection metadata, and harden trust audit writes so I/O errors never alter execution results or mask original failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add server-side caller validation for exec.approvals.trust, derive grantedBy from connection metadata, and wrap trust audit writes to prevent masking command outcomes or original errors; include targeted tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3fc693c to
fc51b81
Compare
|
@greptile-apps please re-review latest commit fc51b81 (security feedback addressed). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d44d877891
ℹ️ 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".
Treat expired windows as inactive during revoke, remove dead revoke parameters, and return null summaries when no audited commands exist. Add targeted tests for expired revoke and empty-summary behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
d44d877 to
86043a7
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 86043a7748
ℹ️ 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 (!isTrustWindowActive(trustWindow, now)) { | ||
| return { ok: false, error: `No active trust window for agent "${validated.agentId}"` }; | ||
| } |
There was a problem hiding this comment.
Let untrust handle expired trust windows
This early return makes revokeTrustWindow fail as soon as a window has naturally expired, so exec.approvals.untrust never reaches its audit cleanup path (cleanupTrustAudit(...) in src/gateway/server-methods/exec-approvals.ts). In practice, if an operator runs untrust after expiry, the CLI just reports “No active trust window” and the trust audit file cannot be removed via the supported untrust workflow, while expired cache entries remain until restart; expired windows should still be treated as revocable for cleanup purposes.
Useful? React with 👍 / 👎.
|
@greptile-apps please re-review latest commit 86043a7 (expired-window revoke + dead params + null empty-summary follow-ups addressed). |
Ensure revokeTrustWindow removes active trust before attempting summary generation so unreadable audit files cannot block de-escalation. Add regression test for directory-at-audit-path failure mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c6de4655a9
ℹ️ 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".
c6de465 to
7b16d1a
Compare
|
@greptile-apps please re-review latest commit 7b16d1a (revoke remains safe even if audit summary generation fails). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b16d1aa8d
ℹ️ 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".
Require trusted trust/untrust callers to be CLI-identified clients with authenticated device identity, and add the same caller gate to exec.approvals.untrust. Add regression tests for cli-mode spoof without device identity and non-CLI untrust rejection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@greptile-apps please re-review latest commit a7c386a (trust/untrust caller gate now requires CLI identity + authenticated device identity). |
Validate approvals trust --minutes as a strict decimal integer string before numeric conversion so values like 10foo, 1.5, and 0x20 are rejected instead of coerced. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
6d01dc0 to
fe2c92a
Compare
Gate exec.approvals.trust.status behind the same trusted interactive CLI caller checks used by trust/untrust to prevent non-CLI clients from probing trust window metadata. Add regression test for non-CLI rejection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@greptile-apps please re-review latest commit aa1629d (trust.status now restricted to trusted interactive CLI callers). |
Expose trustWindowActive from resolveExecApprovalsFromFile and consume it in resolveExecHostApprovalContext instead of re-reading trust cache. This removes dual independent trust-state checks while preserving authoritative host enforcement behavior. Update typed test fixtures accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
46ffdf4 to
99dda03
Compare
|
@greptile-apps please re-review latest commit 99dda03 (trust state now sourced once from resolveExecApprovalsFromFile; host context consumes trustWindowActive instead of a second trust-cache read). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 99dda037d9
ℹ️ 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".
Allow untrust to clear expired/missing trust-window state for cleanup workflows while keeping revoke strict by default. Add CLI agent-session guard to approvals untrust and extend trust/untrust regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@greptile-apps please re-review latest commit a694f17 (untrust now handles expired/missing windows for cleanup, plus untrust CLI now has the same agent-session guard as trust). |
Permit trusted CLI trust/untrust/status calls without device identity when the caller is loopback and authenticated via token/password, matching callGateway local auth behavior. Preserve stricter checks for non-loopback/no-auth callers and add regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4b2980f to
dd6ba0e
Compare
|
@greptile-apps please re-review latest commit dd6ba0e (trust caller gate now allows localhost CLI token/password auth without device identity, while still rejecting non-loopback no-device callers). |
| const message = formatCliError(err); | ||
| if (message.includes("No active trust window")) { | ||
| defaultRuntime.log(message); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Dead "No active trust window" error path in untrust catch block
This error branch can no longer be triggered. Prior to this PR the server returned a "No active trust window" error when untrust was called with no active window; the CLI swallowed that particular error and treated it as success. The updated server handler now passes allowMissing: true to revokeTrustWindow, so the gateway always responds with ok: true (never an error) when the window is simply absent. The string-match check is dead code that could mislead future maintainers into thinking this error path is still live.
| const message = formatCliError(err); | |
| if (message.includes("No active trust window")) { | |
| defaultRuntime.log(message); | |
| return; | |
| } | |
| } catch (err) { | |
| defaultRuntime.error(formatCliError(err)); | |
| defaultRuntime.exit(1); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cli/exec-approvals-cli.ts
Line: 604-608
Comment:
**Dead "No active trust window" error path in `untrust` catch block**
This error branch can no longer be triggered. Prior to this PR the server returned a "No active trust window" error when `untrust` was called with no active window; the CLI swallowed that particular error and treated it as success. The updated server handler now passes `allowMissing: true` to `revokeTrustWindow`, so the gateway always responds with `ok: true` (never an error) when the window is simply absent. The string-match check is dead code that could mislead future maintainers into thinking this error path is still live.
```suggestion
} catch (err) {
defaultRuntime.error(formatCliError(err));
defaultRuntime.exit(1);
}
```
How can I resolve this? If you propose a fix, please make it concise.| expiresAt: number; | ||
| grantedAt: number; | ||
| grantedBy?: string; | ||
| remainingMs?: number; | ||
| }; | ||
| }; | ||
|
|
||
| if (opts.json) { | ||
| defaultRuntime.log(JSON.stringify(result)); | ||
| return; | ||
| } | ||
|
|
||
| if (!result.trustWindow) { | ||
| defaultRuntime.log(`Trust status: inactive (agent "${result.agentId}")`); | ||
| return; | ||
| } | ||
| const remainingMs = | ||
| typeof result.trustWindow.remainingMs === "number" | ||
| ? result.trustWindow.remainingMs | ||
| : Math.max(0, result.trustWindow.expiresAt - Date.now()); | ||
| const remainingMinutes = Math.ceil(remainingMs / 60_000); | ||
| defaultRuntime.log(`Trust status: active (agent "${result.agentId}")`); | ||
| defaultRuntime.log(`Remaining: ${remainingMinutes}m`); | ||
| } catch (err) { | ||
| defaultRuntime.error(formatCliError(err)); | ||
| defaultRuntime.exit(1); | ||
| } | ||
| }); |
There was a problem hiding this comment.
trust-status CLI command missing agent session guard
trust has both a TTY check and an OPENCLAW_SESSION_KEY/OPENCLAW_AGENT_ID env-var check. untrust has the env-var check. trust-status has neither. An agent process that has access to the CLI binary (and whose env vars are not set) can run openclaw approvals trust-status from within a session and learn whether a trust window is active for its agent ID, because the server's isTrustGrantCallerAllowed will see a legitimate CLI connection and allow it through.
While knowing the trust status doesn't directly enable further escalation (the agent's exec calls are already permitted or denied by the approval layer), it is inconsistent with the "no agent self-escalation" design and lets an agent adapt its behavior based on observed trust state. Consider adding at minimum the env-var guard:
.action(async (opts: TrustStatusOpts) => {
try {
if (process.env.OPENCLAW_SESSION_KEY || process.env.OPENCLAW_AGENT_ID) {
exitWithError(
"Trust status queries are blocked from agent sessions. Use a direct terminal session.",
);
}
// ... rest of handlerPrompt To Fix With AI
This is a comment left during a code review.
Path: src/cli/exec-approvals-cli.ts
Line: 629-656
Comment:
**`trust-status` CLI command missing agent session guard**
`trust` has both a TTY check and an `OPENCLAW_SESSION_KEY`/`OPENCLAW_AGENT_ID` env-var check. `untrust` has the env-var check. `trust-status` has neither. An agent process that has access to the CLI binary (and whose env vars are not set) can run `openclaw approvals trust-status` from within a session and learn whether a trust window is active for its agent ID, because the server's `isTrustGrantCallerAllowed` will see a legitimate CLI connection and allow it through.
While knowing the trust status doesn't directly enable further escalation (the agent's exec calls are already permitted or denied by the approval layer), it is inconsistent with the "no agent self-escalation" design and lets an agent adapt its behavior based on observed trust state. Consider adding at minimum the env-var guard:
```typescript
.action(async (opts: TrustStatusOpts) => {
try {
if (process.env.OPENCLAW_SESSION_KEY || process.env.OPENCLAW_AGENT_ID) {
exitWithError(
"Trust status queries are blocked from agent sessions. Use a direct terminal session.",
);
}
// ... rest of handler
```
How can I resolve this? If you propose a fix, please make it concise.| }); | ||
|
|
||
| afterEach(() => { | ||
| process.env.OPENCLAW_HOME = previousHome; | ||
| }); | ||
|
|
||
| it("writes and loads audit entries", () => { | ||
| const entry = appendTrustAuditEntry({ | ||
| agentId: "main", | ||
| command: "echo hello", | ||
| exitCode: 0, | ||
| durationMs: 25, | ||
| now: 1_000, | ||
| }); |
There was a problem hiding this comment.
Temp directory not cleaned up in afterEach
homeDir is populated by makeTempDir() in beforeEach but afterEach only restores the OPENCLAW_HOME env var — it never removes the temp directory itself. Compare with the existing cleanup pattern used in exec-approvals.test.ts:
} finally {
process.env.OPENCLAW_HOME = previousHome;
fs.rmSync(homeDir, { recursive: true, force: true });
}Each test run leaks a temp directory. Add fs.rmSync(homeDir, { recursive: true, force: true }) to the afterEach hook:
| }); | |
| afterEach(() => { | |
| process.env.OPENCLAW_HOME = previousHome; | |
| }); | |
| it("writes and loads audit entries", () => { | |
| const entry = appendTrustAuditEntry({ | |
| agentId: "main", | |
| command: "echo hello", | |
| exitCode: 0, | |
| durationMs: 25, | |
| now: 1_000, | |
| }); | |
| afterEach(() => { | |
| process.env.OPENCLAW_HOME = previousHome; | |
| if (homeDir) { | |
| fs.rmSync(homeDir, { recursive: true, force: true }); | |
| } | |
| }); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/trust-audit.test.ts
Line: 22-35
Comment:
**Temp directory not cleaned up in `afterEach`**
`homeDir` is populated by `makeTempDir()` in `beforeEach` but `afterEach` only restores the `OPENCLAW_HOME` env var — it never removes the temp directory itself. Compare with the existing cleanup pattern used in `exec-approvals.test.ts`:
```typescript
} finally {
process.env.OPENCLAW_HOME = previousHome;
fs.rmSync(homeDir, { recursive: true, force: true });
}
```
Each test run leaks a temp directory. Add `fs.rmSync(homeDir, { recursive: true, force: true })` to the `afterEach` hook:
```suggestion
afterEach(() => {
process.env.OPENCLAW_HOME = previousHome;
if (homeDir) {
fs.rmSync(homeDir, { recursive: true, force: true });
}
});
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 06f4c7c60d
ℹ️ 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 (host === "gateway" && trustWindowActiveAtStart) { | ||
| try { | ||
| appendTrustAuditEntry({ | ||
| agentId, |
There was a problem hiding this comment.
Audit trusted commands in gateway approval-followup path
This trust-audit append is only reached for the synchronous run.promise path, so commands that execute via processGatewayAllowlist(...).pendingResult are never logged. That path is still reachable during an active trust window when requiresAsk is forced (for example by obfuscation detection), which means successful trusted-window executions can be omitted from the trust audit and revoke summary.
Useful? React with 👍 / 👎.
| if (trustWindowActiveAtStart) { | ||
| try { | ||
| appendTrustAuditEntry({ | ||
| agentId: runAgentId, |
There was a problem hiding this comment.
Audit trusted commands in node approval-followup path
In executeNodeHostCommand, trust auditing is only appended in the non-approval branch here; when requiresAsk is true (which can still happen under trust, e.g. obfuscation-triggered approvals), execution continues through the async followup flow and never reaches this write. The result is missing trust-audit entries for commands that did run during the trust window.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38a2aa701c
ℹ️ 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 hostAsk = trustWindowActive | ||
| ? "off" | ||
| : approvals.agent.ask === "off" |
There was a problem hiding this comment.
Keep node trust runs from dropping the approval signal
When a trust window is active this branch forces hostAsk to "off", which makes executeNodeHostCommand skip the gateway approval flow and call node.invoke with approved=false; on node-hosts using the default allowlist/on-miss policy, evaluateSystemRunPolicy then rejects the run as SYSTEM_RUN_DENIED: approval required. I checked this path in src/agents/bash-tools.exec-host-node.ts (direct buildInvokeParams(false, null) call) and src/node-host/exec-policy.ts, so trusted node commands can still fail even though trust is reported active.
Useful? React with 👍 / 👎.
|
Closing this to clean up the commit history and address the automated feedback in a more cohesive way. The incremental fix-on-fix commits made this hard to review. I'll link the replacement PR here shortly. |
|
Replacement PR: #46956. Clean commit history, addressed P1 safety issues and automated review feedback from this iteration. |
Summary
Add Trust Windows — time-bounded sessions that temporarily set exec to permissive mode (security=full, ask=off), then automatically expire. This strikes a usability middle ground between constant approval prompts and permanent broad allowlists.
What's included
Core infra (\src/infra/exec-approvals.ts)
evokeTrustWindow\ / \getTrustWindow\ / \isTrustWindowActive\ APIs
esolveExecApprovalsFromFile\ defaults resolution
Exec host enforcement (\�ash-tools.exec-host-shared.ts, \�ash-tools.exec.ts, \�ash-tools.exec-host-node.ts)
Trust audit module (\src/infra/trust-audit.ts) — NEW
Gateway RPC surface (\src/gateway/server-methods/exec-approvals.ts)
Protocol schemas (\src/gateway/protocol/schema/exec-approvals.ts)
CLI commands (\src/cli/exec-approvals-cli.ts)
Tests (23 new/extended)
Design decisions
esolveExecApprovalsFromFile\ path, so behavior is identical across gateway and node exec hosts.
Validation
Files changed (16 files, +994 / -22)