Skip to content

Commit b826cfa

Browse files
committed
fix(exec): respect OPENCLAW_STATE_DIR for exec approvals
1 parent 5e197ac commit b826cfa

6 files changed

Lines changed: 76 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212

1313
### Fixes
1414

15+
- Exec approvals: honor explicit `OPENCLAW_STATE_DIR` for default JSON and socket paths while keeping the legacy `~/.openclaw` fallback when unset, so custom state roots report and store approvals consistently. Fixes #62917; carries forward #65736. Thanks @oinoom.
1516
- CLI/plugins: use plugin metadata snapshots for install slot selection and add opt-in plugin lifecycle timing traces, so plugin install avoids runtime-loading the plugin registry for metadata-only decisions. Thanks @shakkernerd.
1617
- fix(plugins): restrict bundled plugin dir resolution to trusted package roots. (#73275) Thanks @pgondhi987.
1718
- fix(security): prevent workspace PATH injection via service env and trash helpers. (#73264) Thanks @pgondhi987.

docs/tools/exec-approvals.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ Approvals live in a local JSON file on the execution host:
7474
~/.openclaw/exec-approvals.json
7575
```
7676

77+
If `OPENCLAW_STATE_DIR` is set explicitly, the default approvals JSON file
78+
and socket move under that state directory instead.
79+
7780
Example schema:
7881

7982
```json

src/config/paths.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ export function resolveNewStateDir(homedir: () => string = resolveDefaultHomeDir
5252
return newStateDir(homedir);
5353
}
5454

55+
export function resolveExplicitStateDir(
56+
env: NodeJS.ProcessEnv = process.env,
57+
homedir: () => string = envHomedir(env),
58+
): string | null {
59+
const override = env.OPENCLAW_STATE_DIR?.trim();
60+
if (!override) {
61+
return null;
62+
}
63+
return resolveUserPath(override, env, () => resolveRequiredHomeDir(env, homedir));
64+
}
65+
5566
/**
5667
* State directory for mutable data (sessions, logs, caches).
5768
* Can be overridden via OPENCLAW_STATE_DIR.
@@ -62,9 +73,9 @@ export function resolveStateDir(
6273
homedir: () => string = envHomedir(env),
6374
): string {
6475
const effectiveHomedir = () => resolveRequiredHomeDir(env, homedir);
65-
const override = env.OPENCLAW_STATE_DIR?.trim();
76+
const override = resolveExplicitStateDir(env, homedir);
6677
if (override) {
67-
return resolveUserPath(override, env, effectiveHomedir);
78+
return override;
6879
}
6980
const newDir = newStateDir(effectiveHomedir);
7081
if (env.OPENCLAW_TEST_FAST === "1") {

src/infra/exec-approvals-effective.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
33
import {
44
DEFAULT_EXEC_APPROVAL_ASK_FALLBACK,
55
resolveExecApprovalAllowedDecisions,
6-
resolveExecApprovalsPath,
6+
resolveExecApprovalsDefaultHostPath,
77
type ExecApprovalDecision,
88
maxAsk,
99
minSecurity,
@@ -16,7 +16,6 @@ import {
1616

1717
const DEFAULT_REQUESTED_SECURITY: ExecSecurity = "full";
1818
const DEFAULT_REQUESTED_ASK: ExecAsk = "off";
19-
const DEFAULT_HOST_PATH = "~/.openclaw/exec-approvals.json";
2019
const REQUESTED_DEFAULT_LABEL = {
2120
security: DEFAULT_REQUESTED_SECURITY,
2221
ask: DEFAULT_REQUESTED_ASK,
@@ -149,10 +148,6 @@ function resolveAskNote(params: {
149148
return "more aggressive ask wins";
150149
}
151150

152-
function resolveDefaultExecApprovalsHostPath(): string {
153-
return process.env.OPENCLAW_STATE_DIR?.trim() ? resolveExecApprovalsPath() : DEFAULT_HOST_PATH;
154-
}
155-
156151
export function collectExecPolicyScopeSnapshots(params: {
157152
cfg: OpenClawConfig;
158153
approvals: ExecApprovalsFile;
@@ -239,7 +234,7 @@ export function resolveExecPolicyScopeSnapshot(params: {
239234
ask: requestedAsk.value,
240235
},
241236
});
242-
const hostPath = params.hostPath ?? resolveDefaultExecApprovalsHostPath();
237+
const hostPath = params.hostPath ?? resolveExecApprovalsDefaultHostPath();
243238
const effectiveSecurity = minSecurity(requestedSecurity.value, resolved.agent.security);
244239
const effectiveAsk = maxAsk(requestedAsk.value, resolved.agent.ask);
245240
const effectiveAskFallback = minSecurity(effectiveSecurity, resolved.agent.askFallback);

src/infra/exec-approvals-store.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ vi.mock("./jsonl-socket.js", () => ({
1212
import type { ExecApprovalsFile } from "./exec-approvals.js";
1313

1414
type ExecApprovalsModule = typeof import("./exec-approvals.js");
15+
type ExecApprovalsEffectiveModule = typeof import("./exec-approvals-effective.js");
1516

1617
let addAllowlistEntry: ExecApprovalsModule["addAllowlistEntry"];
1718
let addDurableCommandApproval: ExecApprovalsModule["addDurableCommandApproval"];
@@ -26,6 +27,7 @@ let requestExecApprovalViaSocket: ExecApprovalsModule["requestExecApprovalViaSoc
2627
let resolveExecApprovalsPath: ExecApprovalsModule["resolveExecApprovalsPath"];
2728
let resolveExecApprovalsSocketPath: ExecApprovalsModule["resolveExecApprovalsSocketPath"];
2829
let saveExecApprovals: ExecApprovalsModule["saveExecApprovals"];
30+
let resolveExecPolicyScopeSummary: ExecApprovalsEffectiveModule["resolveExecPolicyScopeSummary"];
2931

3032
const tempDirs: string[] = [];
3133
const originalOpenClawHome = process.env.OPENCLAW_HOME;
@@ -47,6 +49,7 @@ beforeAll(async () => {
4749
resolveExecApprovalsSocketPath,
4850
saveExecApprovals,
4951
} = await import("./exec-approvals.js"));
52+
({ resolveExecPolicyScopeSummary } = await import("./exec-approvals-effective.js"));
5053
});
5154

5255
beforeEach(() => {
@@ -122,6 +125,45 @@ describe("exec approvals store helpers", () => {
122125
);
123126
});
124127

128+
it("reports explicit OPENCLAW_STATE_DIR exec approvals path in host policy sources", () => {
129+
const dir = createHomeDir();
130+
const stateDir = path.join(dir, "state-root");
131+
process.env.OPENCLAW_STATE_DIR = stateDir;
132+
133+
const summary = resolveExecPolicyScopeSummary({
134+
approvals: {
135+
version: 1,
136+
defaults: {
137+
security: "allowlist",
138+
},
139+
},
140+
configPath: "tools.exec",
141+
scopeLabel: "tools.exec",
142+
});
143+
144+
expect(summary.security.hostSource).toBe(
145+
`${path.join(stateDir, "exec-approvals.json")} defaults.security`,
146+
);
147+
});
148+
149+
it("keeps legacy fallback out of default exec approvals host policy sources", () => {
150+
const dir = createHomeDir();
151+
fs.mkdirSync(path.join(dir, ".clawdbot"), { recursive: true });
152+
153+
const summary = resolveExecPolicyScopeSummary({
154+
approvals: {
155+
version: 1,
156+
defaults: {
157+
security: "allowlist",
158+
},
159+
},
160+
configPath: "tools.exec",
161+
scopeLabel: "tools.exec",
162+
});
163+
164+
expect(summary.security.hostSource).toBe("~/.openclaw/exec-approvals.json defaults.security");
165+
});
166+
125167
it("merges socket defaults from normalized, current, and built-in fallback", () => {
126168
const normalized = normalizeExecApprovals({
127169
version: 1,

src/infra/exec-approvals.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import crypto from "node:crypto";
22
import fs from "node:fs";
33
import path from "node:path";
4-
import { resolveStateDir } from "../config/paths.js";
4+
import { resolveExplicitStateDir } from "../config/paths.js";
55
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
66
import {
77
normalizeLowercaseStringOrEmpty,
@@ -184,15 +184,25 @@ function hashExecApprovalsRaw(raw: string | null): string {
184184
}
185185

186186
export function resolveExecApprovalsPath(): string {
187-
if (process.env.OPENCLAW_STATE_DIR?.trim()) {
188-
return path.join(resolveStateDir(), DEFAULT_FILE_FILENAME);
187+
const stateDir = resolveExplicitStateDir();
188+
if (stateDir) {
189+
return path.join(stateDir, DEFAULT_FILE_FILENAME);
189190
}
190191
return expandHomePrefix(DEFAULT_FILE);
191192
}
192193

194+
export function resolveExecApprovalsDefaultHostPath(): string {
195+
const stateDir = resolveExplicitStateDir();
196+
if (stateDir) {
197+
return path.join(stateDir, DEFAULT_FILE_FILENAME);
198+
}
199+
return DEFAULT_FILE;
200+
}
201+
193202
export function resolveExecApprovalsSocketPath(): string {
194-
if (process.env.OPENCLAW_STATE_DIR?.trim()) {
195-
return path.join(resolveStateDir(), DEFAULT_SOCKET_FILENAME);
203+
const stateDir = resolveExplicitStateDir();
204+
if (stateDir) {
205+
return path.join(stateDir, DEFAULT_SOCKET_FILENAME);
196206
}
197207
return expandHomePrefix(DEFAULT_SOCKET);
198208
}

0 commit comments

Comments
 (0)