mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(policy): honor state dir for exec approvals evidence
This commit is contained in:
+9
-7
@@ -415,12 +415,14 @@ allowlist such as `["all"]`.
|
||||
|
||||
#### Exec approvals
|
||||
|
||||
Exec approvals policy observes the runtime `~/.openclaw/exec-approvals.json`
|
||||
file artifact. Actual posture rules such as `execApprovals.defaults.*` or
|
||||
`execApprovals.agents.*` require readable artifact evidence; a missing or
|
||||
invalid artifact is reported as unobservable evidence instead of becoming a
|
||||
best-effort pass against synthetic runtime defaults. Once the artifact is
|
||||
readable, omitted approval fields inherit runtime defaults: missing
|
||||
Exec approvals policy observes the active runtime `exec-approvals.json`
|
||||
artifact. By default this is `~/.openclaw/exec-approvals.json`; when
|
||||
`OPENCLAW_STATE_DIR` is set, Policy reads
|
||||
`$OPENCLAW_STATE_DIR/exec-approvals.json`. Actual posture rules such as
|
||||
`execApprovals.defaults.*` or `execApprovals.agents.*` require readable artifact
|
||||
evidence; a missing or invalid artifact is reported as unobservable evidence
|
||||
instead of becoming a best-effort pass against synthetic runtime defaults. Once
|
||||
the artifact is readable, omitted approval fields inherit runtime defaults: missing
|
||||
`defaults.security` is `full`, and missing agent security inherits that
|
||||
default. Evidence includes `defaults`, `agents.*`, and
|
||||
`agents.*.allowlist[].pattern` plus optional `argPattern`, effective
|
||||
@@ -429,7 +431,7 @@ path/token, `commandText`, `lastUsedCommand`, resolved paths, or timestamps.
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
| ------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `execApprovals.requireFile` | Runtime `~/.openclaw/exec-approvals.json` path | Set to `true` to require the approvals artifact to exist and parse. |
|
||||
| `execApprovals.requireFile` | Active runtime `exec-approvals.json` path | Set to `true` to require the approvals artifact to exist and parse. |
|
||||
| `execApprovals.defaults.allowSecurity` | `defaults.security`, defaulting to `full` | Allow only approved default approval security modes. |
|
||||
| `execApprovals.agents.allowSecurity` | `agents.*.security`, inheriting defaults | Allow only approved per-agent effective approval security modes. |
|
||||
| `execApprovals.agents.allowAutoAllowSkills` | `defaults.autoAllowSkills` and `agents.*.autoAllowSkills`, inheriting runtime defaults | Set to `false` to require strict manual allowlists without implicit skill CLI approval. |
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
|
||||
let workspaceDir: string;
|
||||
let originalOpenClawHome: string | undefined;
|
||||
let originalOpenClawStateDir: string | undefined;
|
||||
|
||||
function cfgWithPolicy(settings: Record<string, unknown> = {}): OpenClawConfig {
|
||||
return {
|
||||
@@ -106,13 +107,23 @@ describe("registerPolicyDoctorChecks", () => {
|
||||
clearHealthChecksForTest();
|
||||
resetPolicyDoctorChecksForTest();
|
||||
originalOpenClawHome = process.env.OPENCLAW_HOME;
|
||||
originalOpenClawStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
workspaceDir = await fs.mkdtemp(join(tmpdir(), "policy-doctor-"));
|
||||
process.env.OPENCLAW_HOME = workspaceDir;
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
await fs.mkdir(join(workspaceDir, ".openclaw"), { recursive: true });
|
||||
await fs.symlink(
|
||||
"../exec-approvals.json",
|
||||
join(workspaceDir, ".openclaw", "exec-approvals.json"),
|
||||
);
|
||||
try {
|
||||
await fs.symlink(
|
||||
"../exec-approvals.json",
|
||||
join(workspaceDir, ".openclaw", "exec-approvals.json"),
|
||||
);
|
||||
} catch (err) {
|
||||
if (typeof err !== "object" || err === null || !("code" in err) || err.code !== "EPERM") {
|
||||
throw err;
|
||||
}
|
||||
await fs.rm(join(workspaceDir, ".openclaw"), { recursive: true, force: true });
|
||||
await fs.symlink(workspaceDir, join(workspaceDir, ".openclaw"), "junction");
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -121,6 +132,11 @@ describe("registerPolicyDoctorChecks", () => {
|
||||
} else {
|
||||
process.env.OPENCLAW_HOME = originalOpenClawHome;
|
||||
}
|
||||
if (originalOpenClawStateDir === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = originalOpenClawStateDir;
|
||||
}
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
clearHealthChecksForTest();
|
||||
resetPolicyDoctorChecksForTest();
|
||||
@@ -8492,6 +8508,40 @@ describe("registerPolicyDoctorChecks", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_STATE_DIR for the exec approvals artifact path", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
const stateDir = join(workspaceDir, "state");
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({ execApprovals: { defaults: { allowSecurity: ["deny"] } } }),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({ version: 1, defaults: { security: "deny" } }),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(stateDir, "exec-approvals.json"),
|
||||
JSON.stringify({ version: 1, defaults: { security: "full" } }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-default-security-unapproved",
|
||||
ocPath: "oc://exec-approvals.json/defaults",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects unsupported exec approval allowlist requirement keys", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
|
||||
@@ -7030,14 +7030,13 @@ async function readPolicyFile(
|
||||
async function readExecApprovalsFile(
|
||||
ctx: HealthCheckContext,
|
||||
): Promise<{ raw: string; path: string; displayName: string; ocDocName: string } | null> {
|
||||
const displayName = execApprovalsDisplayName();
|
||||
const path = resolvePolicyArtifactPath(ctx, canonicalExecApprovalsPath());
|
||||
const artifact = execApprovalsArtifactLocation(ctx);
|
||||
try {
|
||||
const fs = await loadFsPromisesModule();
|
||||
return {
|
||||
raw: await fs.readFile(path, "utf-8"),
|
||||
path,
|
||||
displayName,
|
||||
raw: await fs.readFile(artifact.path, "utf-8"),
|
||||
path: artifact.path,
|
||||
displayName: artifact.displayName,
|
||||
ocDocName: "exec-approvals.json",
|
||||
};
|
||||
} catch (err) {
|
||||
@@ -7078,16 +7077,20 @@ function resolvePolicyArtifactHomeDir(): string | undefined {
|
||||
const explicitHome = normalizedEnvValue(process.env.OPENCLAW_HOME);
|
||||
if (explicitHome !== undefined) {
|
||||
if (explicitHome === "~" || explicitHome.startsWith("~/") || explicitHome.startsWith("~\\")) {
|
||||
const fallbackHome = resolveOsPolicyHomeDir();
|
||||
return fallbackHome === undefined
|
||||
? undefined
|
||||
: resolve(explicitHome.replace(/^~(?=$|[\\/])/, fallbackHome));
|
||||
return resolvePolicyHomeRelativePath(explicitHome);
|
||||
}
|
||||
return resolve(explicitHome);
|
||||
}
|
||||
return resolveOsPolicyHomeDir();
|
||||
}
|
||||
|
||||
function resolvePolicyHomeRelativePath(value: string): string {
|
||||
const fallbackHome = resolveOsPolicyHomeDir();
|
||||
return fallbackHome === undefined
|
||||
? resolve(value)
|
||||
: resolve(value.replace(/^~(?=$|[\\/])/, fallbackHome));
|
||||
}
|
||||
|
||||
function resolveOsPolicyHomeDir(): string | undefined {
|
||||
return (
|
||||
normalizedEnvValue(process.env.HOME) ??
|
||||
@@ -7434,8 +7437,31 @@ function canonicalExecApprovalsPath(): string {
|
||||
return "~/.openclaw/exec-approvals.json";
|
||||
}
|
||||
|
||||
function execApprovalsArtifactLocation(ctx: HealthCheckContext): {
|
||||
readonly path: string;
|
||||
readonly displayName: string;
|
||||
} {
|
||||
const stateDir = normalizedEnvValue(process.env.OPENCLAW_STATE_DIR);
|
||||
if (stateDir !== undefined) {
|
||||
const path = resolve(resolvePolicyStateDir(stateDir), "exec-approvals.json");
|
||||
return { path, displayName: path };
|
||||
}
|
||||
return {
|
||||
path: resolvePolicyArtifactPath(ctx, canonicalExecApprovalsPath()),
|
||||
displayName: canonicalExecApprovalsPath(),
|
||||
};
|
||||
}
|
||||
|
||||
function execApprovalsDisplayName(): string {
|
||||
return canonicalExecApprovalsPath();
|
||||
const stateDir = normalizedEnvValue(process.env.OPENCLAW_STATE_DIR);
|
||||
if (stateDir === undefined) {
|
||||
return canonicalExecApprovalsPath();
|
||||
}
|
||||
return resolve(resolvePolicyStateDir(stateDir), "exec-approvals.json");
|
||||
}
|
||||
|
||||
function resolvePolicyStateDir(stateDir: string): string {
|
||||
return stateDir.startsWith("~") ? resolvePolicyHomeRelativePath(stateDir) : resolve(stateDir);
|
||||
}
|
||||
|
||||
function policyPathSetting(ctx: HealthCheckContext): string {
|
||||
|
||||
Reference in New Issue
Block a user