fix(exec): allow symlinked OPENCLAW_STATE_DIR via terminal stat#96037
fix(exec): allow symlinked OPENCLAW_STATE_DIR via terminal stat#96037t2wei wants to merge 1 commit into
Conversation
`ensureDir` validated the approvals directory with `fs.lstatSync`, then
rejected any path where `isSymbolicLink()` was true. This breaks on
common deployment topologies where the state directory itself is a
top-level symlink (e.g. `/opt/openclaw` -> `/mnt/efs/openclaw` for
EFS-backed hosts on AWS).
The companion `assertNoExecApprovalsSymlinkParents` check walks the
segments BETWEEN the trusted root and the terminal target — when
`OPENCLAW_STATE_DIR` equals `OPENCLAW_HOME` (the trusted root), no
segments are walked and the terminal symlink slips past that walk
without inspection. The follow-up lstat check is therefore the only
gate, and it rejects the entire deployment.
Switch to `fs.statSync` so a terminal symlink that resolves to a real
directory is accepted while:
- intermediate path symlinks are still rejected by the parents check
- non-directory terminals (regular files, FIFOs, etc.) are still
rejected via `dirStat.isDirectory()`
Symptom before fix:
```text
[tools] exec failed: Refusing to use unsafe exec approvals directory:
/opt/openclaw raw_params={"workdir":"...","timeout":20}
```
Every exec tool call from any agent failed with the same error
regardless of `workdir`, because `OPENCLAW_STATE_DIR` resolution
happens server-side before the workdir is consulted.
Adds a regression test that mirrors the deployment shape: `HOME` and
`STATE_DIR` both pointing at a symlink whose target is a real directory.
|
Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 5:34 PM ET / 21:34 UTC. Summary PR surface: Source +8, Tests +24. Total +32 across 2 files. Reproducibility: yes. from source inspection. Current main resolves Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Support symlinked state-dir deployments through a narrowly validated terminal-symlink policy that preserves parent, destination-file, hardlink, and migration safeguards, with redacted real exec proof before merge. Do we have a high-confidence way to reproduce the issue? Yes, from source inspection. Current main resolves Is this the best way to solve the issue? No, not as submitted. Switching to Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 44ec7580e2f0. Label changesLabel justifications:
Evidence reviewedPR surface: Source +8, Tests +24. Total +32 across 2 files. View PR surface stats
Security concerns:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
This pull request has been automatically marked as stale due to inactivity. |
|
Closing due to inactivity. |
What Problem This Solves
src/infra/exec-approvals.ts'sensureDirvalidated the approvals directory withfs.lstatSyncand rejected any path whereisSymbolicLink()was true:```ts
const dirStat = fs.lstatSync(dir);
if (!dirStat.isDirectory() || dirStat.isSymbolicLink()) {
throw new Error(`Refusing to use unsafe exec approvals directory: ${dir}`);
}
```
This breaks on common deployment topologies where the state directory itself is a top-level symlink — e.g. `/opt/openclaw` → `/mnt/efs/openclaw` for EFS-backed hosts on AWS, or any deployment that wants a stable canonical path while the actual storage is mounted under a different name.
Why The Companion Check Misses It
The function calls `assertNoExecApprovalsSymlinkParents(dir, resolveRequiredHomeDir())` first, which walks segments BETWEEN the trusted root and the terminal target. When `OPENCLAW_STATE_DIR` equals `OPENCLAW_HOME` (a common deployment shape where the symlink is the canonical state root), there are zero segments to walk, so the terminal symlink slips past that check entirely. The follow-up `lstatSync` check is then the only gate — and it rejects the entire deployment.
User Impact
Before the fix, every `exec` tool call from every agent failed with the same error, regardless of the `workdir` parameter the agent passed:
```text
[tools] exec failed: Refusing to use unsafe exec approvals directory: /opt/openclaw
raw_params={"workdir":"/opt/openclaw/workspace/docs/tmp/job-1","timeout":20}
```
The error message is misleading because the rejection happens during approvals-state resolution (server-side) before `workdir` is even consulted. Agents that retried with different `workdir` values still failed identically.
Fix
Switch the terminal directory check from `fs.lstatSync` to `fs.statSync` (follows symlinks). A terminal symlink whose target is a real directory now passes, while:
Behavior is unchanged for the no-symlink path: `statSync` returns the same stat as `lstatSync` when the target is a normal directory.
Evidence
Test plan
🤖 AI-assisted PR — diagnosis + patch produced with Claude Code; verified against production deploy that exhibited the bug.