mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
feat(policy): cover exec approvals artifact (#90003)
Add exec approvals artifact evidence to Policy.
- add the execApprovals policy namespace and check IDs for required artifact presence, default/per-agent security posture, autoAllowSkills, and allowlist drift
- read the active exec-approvals.json artifact only when execApprovals policy rules are configured, honoring OPENCLAW_STATE_DIR before the default ~/.openclaw path
- emit redacted posture evidence and stable oc:// references without socket tokens, command text, resolved paths, timestamps, or approval-session details
- document the public policy surface and add focused scanner, doctor, conformance, and CLI coverage
Validation:
- GitHub Actions for head b82eefe492 are green, including Real behavior proof.
- ClawSweeper re-review completed for the same head with proof: sufficient and status: ready for maintainer look.
- Maintainer artifact-boundary acceptance is recorded in the PR discussion and body.
Co-authored-by: Gio Della-Libera <235387111+giodl73-repo@users.noreply.github.com>
This commit is contained in:
+92
-10
@@ -54,7 +54,8 @@ doctor can report the missing artifact.
|
||||
Policy is authored, not generated from the user's current settings. A minimal
|
||||
policy for channels, MCP servers, model providers, network posture, ingress/channel access, Gateway
|
||||
exposure, agent workspace posture, configured sandbox runtime posture, OpenClaw
|
||||
data-handling posture, config secret provider/auth profile posture, and tool metadata looks like this:
|
||||
data-handling posture, config secret provider/auth profile posture, exec approval
|
||||
file posture, and tool metadata looks like this:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -145,6 +146,15 @@ data-handling posture, config secret provider/auth profile posture, and tool met
|
||||
"allowModes": ["api_key", "token"],
|
||||
},
|
||||
},
|
||||
"execApprovals": {
|
||||
"requireFile": true,
|
||||
"defaults": { "allowSecurity": ["deny"] },
|
||||
"agents": {
|
||||
"allowSecurity": ["deny", "allowlist"],
|
||||
"allowAutoAllowSkills": false,
|
||||
"allowlist": { "expected": ["deploy", "status"] },
|
||||
},
|
||||
},
|
||||
"tools": {
|
||||
"requireMetadata": ["risk", "sensitivity", "owner"],
|
||||
"profiles": {
|
||||
@@ -187,9 +197,11 @@ and `group:runtime` covers shell/process tools. Tool posture policy observes
|
||||
`tools.profile`, `tools.allow`, `tools.alsoAllow`, `tools.deny`,
|
||||
`tools.fs.workspaceOnly`, `tools.exec.security`, `tools.exec.ask`,
|
||||
`tools.exec.host`, `tools.elevated.enabled`, and the same per-agent
|
||||
`agents.list[].tools.*` overrides. It does not read runtime/operator approval
|
||||
state such as exec-approvals.json, and it does not enforce tool calls at
|
||||
runtime. Secret evidence records
|
||||
`agents.list[].tools.*` overrides. Exec approval policy reads the named
|
||||
`exec-approvals.json` product artifact only when an `execApprovals` rule is
|
||||
present; evidence records defaults, per-agent posture, and allowlist patterns
|
||||
without socket tokens or last-used command text. Policy does not enforce tool
|
||||
calls at runtime. Secret evidence records
|
||||
provider/source posture and SecretRef metadata, never raw secret values. Policy
|
||||
does not read or attest per-agent credential stores such as `auth-profiles.json`;
|
||||
those stores remain owned by the existing auth and credential flows.
|
||||
@@ -218,8 +230,8 @@ its own finding against the same observed config.
|
||||
|
||||
Use `scopes.<scopeName>` when one set of agents or channels needs stricter
|
||||
policy than the top-level baseline. Agent-scoped sections use `agentIds`, which
|
||||
supports `tools.*`, `agents.workspace.*`, `sandbox.*`, and
|
||||
`dataHandling.memory.*`. Channel-scoped
|
||||
supports `tools.*`, `agents.workspace.*`, `sandbox.*`, `dataHandling.memory.*`,
|
||||
and `execApprovals.*`. Channel-scoped
|
||||
ingress uses `channelIds`, which supports `ingress.channels.*`. Unsupported
|
||||
sections are rejected instead of being ignored. If an `agentIds` entry is not
|
||||
present in `agents.list[]`, OpenClaw evaluates the scoped rule against inherited
|
||||
@@ -304,10 +316,10 @@ groups where those fields cannot be observed.
|
||||
Top-level `ingress.session.requireDmScope` remains global because
|
||||
`session.dmScope` is not channel-attributable evidence.
|
||||
|
||||
| Selector | Supported sections | Use when |
|
||||
| ------------ | ----------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| `agentIds` | `tools`, `agents.workspace`, `sandbox`, and `dataHandling.memory` | One or more runtime agents need stricter rules. |
|
||||
| `channelIds` | `ingress.channels` | One or more channels need stricter ingress rules. |
|
||||
| Selector | Supported sections | Use when |
|
||||
| ------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| `agentIds` | `tools`, `agents.workspace`, `sandbox`, `dataHandling.memory`, and `execApprovals` | One or more runtime agents need stricter rules. |
|
||||
| `channelIds` | `ingress.channels` | One or more channels need stricter ingress rules. |
|
||||
|
||||
Every scope present in `policy.jsonc` must be valid and enforceable.
|
||||
|
||||
@@ -401,6 +413,69 @@ allowlist such as `["all"]`.
|
||||
| `secrets.denySources` | Secret provider sources and SecretRef sources | Deny sources such as `exec`, `file`, or another configured source name. |
|
||||
| `secrets.allowInsecureProviders` | Insecure secret-provider posture flags | Set to `false` to reject providers that opt into insecure posture. |
|
||||
|
||||
#### Exec approvals
|
||||
|
||||
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
|
||||
`autoAllowSkills` posture, and entry source. It does not include socket
|
||||
path/token, `commandText`, `lastUsedCommand`, resolved paths, or timestamps.
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
| ------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
| `execApprovals.agents.allowlist.expected` | Aggregate `agents.*.allowlist[]` pattern and optional argPattern entries | Require the approvals allowlist to match the reviewed pattern set. |
|
||||
|
||||
For example, require the approvals artifact, deny permissive defaults, and
|
||||
allow only reviewed exec approval posture for selected agents:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"execApprovals": {
|
||||
"requireFile": true,
|
||||
"defaults": {
|
||||
// Security modes: "deny", "allowlist", or "full".
|
||||
// This default permits only the locked-down deny posture.
|
||||
"allowSecurity": ["deny"],
|
||||
},
|
||||
},
|
||||
"scopes": {
|
||||
"restricted-shell": {
|
||||
"agentIds": ["family-agent", "groups-agent"],
|
||||
"execApprovals": {
|
||||
"agents": {
|
||||
// Selected agents may use reviewed allowlist posture, but not "full".
|
||||
"allowSecurity": ["allowlist"],
|
||||
// false means skill CLIs must appear in the reviewed allowlist instead of
|
||||
// being implicitly approved by autoAllowSkills.
|
||||
"allowAutoAllowSkills": false,
|
||||
"allowlist": {
|
||||
"expected": [
|
||||
// Simple entry: exact reviewed executable pattern with no argPattern.
|
||||
"travel-hub",
|
||||
// Constrained entry: pattern plus reviewed argument regex.
|
||||
{ "pattern": "calendar-cli", "argPattern": "^sync\\b" },
|
||||
"/bin/date",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Auth profiles
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
@@ -769,6 +844,13 @@ Policy currently verifies:
|
||||
| `policy/secrets-insecure-provider` | A secret provider opts into insecure posture when policy denies it. |
|
||||
| `policy/auth-profile-invalid-metadata` | A config auth profile is missing valid provider or mode metadata. |
|
||||
| `policy/auth-profile-unapproved-mode` | A config auth profile mode is outside the policy allowlist. |
|
||||
| `policy/exec-approvals-missing` | Policy requires `exec-approvals.json`, but the artifact is missing. |
|
||||
| `policy/exec-approvals-invalid` | The configured exec approvals artifact cannot be parsed. |
|
||||
| `policy/exec-approvals-default-security-unapproved` | Exec approval defaults use a security mode outside the policy allowlist. |
|
||||
| `policy/exec-approvals-agent-security-unapproved` | A per-agent effective exec approval security mode is outside the allowlist. |
|
||||
| `policy/exec-approvals-auto-allow-skills-enabled` | An exec approval agent implicitly auto-allows skill CLIs when policy denies it. |
|
||||
| `policy/exec-approvals-allowlist-missing` | The approvals allowlist is missing a pattern required by policy. |
|
||||
| `policy/exec-approvals-allowlist-unexpected` | The approvals allowlist includes a pattern not expected by policy. |
|
||||
| `policy/tools-missing-risk-level` | A governed tool declaration is missing risk metadata. |
|
||||
| `policy/tools-unknown-risk-level` | A governed tool declaration uses an unknown risk value. |
|
||||
| `policy/tools-missing-sensitivity-token` | A governed tool declaration is missing sensitivity metadata. |
|
||||
|
||||
@@ -514,6 +514,82 @@ describe("policy commands", () => {
|
||||
expect(parsed.rulesChecked).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it("accepts exec approval allowlist conformance entries with argPattern", async () => {
|
||||
const policy = {
|
||||
execApprovals: {
|
||||
agents: {
|
||||
allowAutoAllowSkills: false,
|
||||
allowlist: {
|
||||
expected: ["status", { pattern: "calendar-cli", argPattern: "^sync\\b" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "baseline.policy.jsonc"),
|
||||
JSON.stringify(policy),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(join(workspaceDir, "policy.jsonc"), JSON.stringify(policy), "utf-8");
|
||||
|
||||
const { exitCode, parsed } = await runPolicyCompareJson({
|
||||
baseline: "baseline.policy.jsonc",
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(parsed).toMatchObject({
|
||||
ok: true,
|
||||
findings: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects unsupported exec approval allowlist requirement keys in policy compare", async () => {
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "baseline.policy.jsonc"),
|
||||
JSON.stringify({
|
||||
execApprovals: {
|
||||
agents: {
|
||||
allowlist: {
|
||||
expected: [{ pattern: "deploy", argpattern: "^--prod$" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
execApprovals: {
|
||||
agents: {
|
||||
allowlist: {
|
||||
expected: [{ pattern: "deploy", argPattern: "^--prod$" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { exitCode, parsed } = await runPolicyCompareJson({
|
||||
baseline: "baseline.policy.jsonc",
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(parsed).toMatchObject({
|
||||
ok: false,
|
||||
rulesChecked: 0,
|
||||
});
|
||||
expect(parsed.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/policy-conformance-invalid",
|
||||
target: "oc://baseline.policy.jsonc/execApprovals/agents/allowlist/expected/#0",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports missing and weaker policy file conformance rules", async () => {
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "baseline.policy.jsonc"),
|
||||
@@ -940,6 +1016,44 @@ describe("policy commands", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepts stricter later scoped candidate overlays during policy compare", async () => {
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "baseline.policy.jsonc"),
|
||||
JSON.stringify({
|
||||
scopes: {
|
||||
release: {
|
||||
agentIds: ["main"],
|
||||
tools: { exec: { allowHosts: ["sandbox"] } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
scopes: {
|
||||
team: {
|
||||
agentIds: ["main"],
|
||||
tools: { exec: { allowHosts: ["sandbox", "node"] } },
|
||||
},
|
||||
lockdown: {
|
||||
agentIds: ["main"],
|
||||
tools: { exec: { allowHosts: ["sandbox"] } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { exitCode, parsed } = await runPolicyCompareJson({
|
||||
baseline: "baseline.policy.jsonc",
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(parsed.findings).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects duplicate scoped candidates when any matching scoped value is weaker", async () => {
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "baseline.policy.jsonc"),
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
} from "./register.js";
|
||||
|
||||
let workspaceDir: string;
|
||||
let originalOpenClawHome: string | undefined;
|
||||
let originalOpenClawStateDir: string | undefined;
|
||||
|
||||
function cfgWithPolicy(settings: Record<string, unknown> = {}): OpenClawConfig {
|
||||
return {
|
||||
@@ -104,10 +106,37 @@ describe("registerPolicyDoctorChecks", () => {
|
||||
beforeEach(async () => {
|
||||
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 });
|
||||
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 () => {
|
||||
if (originalOpenClawHome === undefined) {
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
} 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();
|
||||
@@ -249,6 +278,23 @@ describe("registerPolicyDoctorChecks", () => {
|
||||
strictness: "requires-true",
|
||||
selectors: ["agentIds"],
|
||||
},
|
||||
{
|
||||
path: "execApprovals.agents.allowSecurity",
|
||||
strictness: "allowlist-subset",
|
||||
emptyList: "disabled",
|
||||
selectors: ["agentIds"],
|
||||
},
|
||||
{
|
||||
path: "execApprovals.agents.allowAutoAllowSkills",
|
||||
strictness: "requires-false",
|
||||
selectors: ["agentIds"],
|
||||
},
|
||||
{
|
||||
path: "execApprovals.agents.allowlist.expected",
|
||||
strictness: "exact-list",
|
||||
emptyList: "meaningful",
|
||||
selectors: ["agentIds"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -564,6 +610,13 @@ describe("registerPolicyDoctorChecks", () => {
|
||||
"policy/secrets-insecure-provider",
|
||||
"policy/auth-profile-invalid-metadata",
|
||||
"policy/auth-profile-unapproved-mode",
|
||||
"policy/exec-approvals-missing",
|
||||
"policy/exec-approvals-invalid",
|
||||
"policy/exec-approvals-default-security-unapproved",
|
||||
"policy/exec-approvals-agent-security-unapproved",
|
||||
"policy/exec-approvals-auto-allow-skills-enabled",
|
||||
"policy/exec-approvals-allowlist-missing",
|
||||
"policy/exec-approvals-allowlist-unexpected",
|
||||
"policy/tools-missing-risk-level",
|
||||
"policy/tools-unknown-risk-level",
|
||||
"policy/tools-missing-sensitivity-token",
|
||||
@@ -7805,6 +7858,768 @@ describe("registerPolicyDoctorChecks", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports exec approvals file conformance findings", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
execApprovals: {
|
||||
requireFile: true,
|
||||
defaults: { allowSecurity: ["deny"] },
|
||||
agents: {
|
||||
allowSecurity: ["allowlist"],
|
||||
allowlist: { expected: ["deploy", "doctor"] },
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
socket: { path: "/tmp/openclaw.sock", token: "secret-token" },
|
||||
defaults: { security: "full" },
|
||||
agents: {
|
||||
sebby: {
|
||||
security: "full",
|
||||
allowlist: [{ pattern: "deploy", commandText: "deploy --prod" }],
|
||||
},
|
||||
buddy: {
|
||||
security: "allowlist",
|
||||
allowlist: [{ pattern: "status" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-default-security-unapproved",
|
||||
ocPath: "oc://exec-approvals.json/defaults",
|
||||
requirement: "oc://policy.jsonc/execApprovals/defaults/allowSecurity",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-agent-security-unapproved",
|
||||
ocPath: "oc://exec-approvals.json/agents/sebby",
|
||||
requirement: "oc://policy.jsonc/execApprovals/agents/allowSecurity",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-allowlist-missing",
|
||||
target: "oc://exec-approvals.json",
|
||||
requirement: "oc://policy.jsonc/execApprovals/agents/allowlist/expected",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-allowlist-unexpected",
|
||||
ocPath: "oc://exec-approvals.json/agents/buddy/allowlist/#0",
|
||||
requirement: "oc://policy.jsonc/execApprovals/agents/allowlist/expected",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(JSON.stringify(result.findings)).not.toContain("secret-token");
|
||||
expect(JSON.stringify(result.findings)).not.toContain("deploy --prod");
|
||||
});
|
||||
|
||||
it("compares exec approval allowlist entries with argPattern", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
execApprovals: {
|
||||
agents: {
|
||||
allowlist: { expected: [{ pattern: "deploy", argPattern: "^--prod$" }] },
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
agents: { main: { allowlist: [{ pattern: "deploy" }] } },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-allowlist-missing",
|
||||
message:
|
||||
"exec approvals allowlist is missing expected pattern 'deploy argPattern=^--prod$'.",
|
||||
target: "oc://exec-approvals.json",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-allowlist-unexpected",
|
||||
message: "exec approvals allowlist has unexpected pattern 'deploy'.",
|
||||
ocPath: "oc://exec-approvals.json/agents/main/allowlist/#0",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("checks inherited default security for global exec approval agent rules", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({ execApprovals: { agents: { allowSecurity: ["allowlist"] } } }),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({ version: 1, defaults: { security: "full" } }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-agent-security-unapproved",
|
||||
ocPath: "oc://exec-approvals.json/defaults",
|
||||
requirement: "oc://policy.jsonc/execApprovals/agents/allowSecurity",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports inherited autoAllowSkills when policy requires manual exec allowlists", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({ execApprovals: { agents: { allowAutoAllowSkills: false } } }),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({ version: 1, defaults: { autoAllowSkills: true } }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-auto-allow-skills-enabled",
|
||||
ocPath: "oc://exec-approvals.json/defaults",
|
||||
requirement: "oc://policy.jsonc/execApprovals/agents/allowAutoAllowSkills",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses wildcard security for global exec approval agents that only add allowlist entries", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({ execApprovals: { agents: { allowSecurity: ["deny"] } } }),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
defaults: { security: "full" },
|
||||
agents: {
|
||||
"*": { security: "deny" },
|
||||
main: { allowlist: [{ pattern: "status" }] },
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([]);
|
||||
});
|
||||
|
||||
it("checks default-inherited global exec approval agents when explicit agents exist", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({ execApprovals: { agents: { allowSecurity: ["allowlist"] } } }),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
defaults: { security: "full" },
|
||||
agents: { main: { security: "allowlist" } },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-agent-security-unapproved",
|
||||
ocPath: "oc://exec-approvals.json/defaults",
|
||||
requirement: "oc://policy.jsonc/execApprovals/agents/allowSecurity",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("applies scoped exec approvals only to selected agents", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
scopes: {
|
||||
restricted: {
|
||||
agentIds: ["sebby"],
|
||||
execApprovals: {
|
||||
agents: {
|
||||
allowSecurity: ["allowlist"],
|
||||
allowlist: { expected: ["deploy", "doctor"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
defaults: { security: "deny" },
|
||||
agents: {
|
||||
sebby: {
|
||||
security: "full",
|
||||
allowlist: [{ pattern: "deploy" }, { pattern: "status" }],
|
||||
},
|
||||
buddy: {
|
||||
security: "full",
|
||||
allowlist: [{ pattern: "unrelated" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-agent-security-unapproved",
|
||||
ocPath: "oc://exec-approvals.json/agents/sebby",
|
||||
requirement: "oc://policy.jsonc/scopes/restricted/execApprovals/agents/allowSecurity",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-allowlist-missing",
|
||||
requirement:
|
||||
"oc://policy.jsonc/scopes/restricted/execApprovals/agents/allowlist/expected",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-allowlist-unexpected",
|
||||
ocPath: "oc://exec-approvals.json/agents/sebby/allowlist/#1",
|
||||
requirement:
|
||||
"oc://policy.jsonc/scopes/restricted/execApprovals/agents/allowlist/expected",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(result.findings).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ ocPath: expect.stringContaining("agents/buddy") }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not inherit wildcard security when exact agent security is malformed", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
scopes: {
|
||||
restricted: {
|
||||
agentIds: ["sebby"],
|
||||
execApprovals: { agents: { allowSecurity: ["deny"] } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
defaults: { security: "deny" },
|
||||
agents: {
|
||||
"*": { security: "full" },
|
||||
sebby: { security: "bogus" },
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses runtime defaults for malformed exec approval mode fields", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({ execApprovals: { defaults: { allowSecurity: ["full"] } } }),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({ version: 1, defaults: { security: "bogus" } }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([]);
|
||||
});
|
||||
|
||||
it("requires exec approvals artifacts for scoped exec approval rules", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
scopes: {
|
||||
restricted: {
|
||||
agentIds: ["sebby", "buddy"],
|
||||
execApprovals: {
|
||||
agents: { allowSecurity: ["allowlist"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-missing",
|
||||
target: "oc://exec-approvals.json",
|
||||
requirement: "oc://policy.jsonc/scopes/restricted/execApprovals",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects invalid exec approvals artifacts for scoped exec approval rules", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
scopes: {
|
||||
restricted: {
|
||||
agentIds: ["sebby", "buddy"],
|
||||
execApprovals: {
|
||||
agents: { allowSecurity: ["allowlist"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(join(workspaceDir, "exec-approvals.json"), "{", "utf-8");
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-invalid",
|
||||
target: "oc://exec-approvals.json",
|
||||
requirement: "oc://policy.jsonc/scopes/restricted/execApprovals",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not require exec approvals artifacts for requireFile false alone", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({ execApprovals: { requireFile: false } }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([]);
|
||||
});
|
||||
|
||||
it("applies wildcard exec approvals to scoped agents", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
scopes: {
|
||||
restricted: {
|
||||
agentIds: ["sebby"],
|
||||
execApprovals: {
|
||||
agents: {
|
||||
allowSecurity: ["allowlist"],
|
||||
allowlist: { expected: ["deploy"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
defaults: { security: "deny" },
|
||||
agents: {
|
||||
"*": {
|
||||
security: "full",
|
||||
allowlist: [{ pattern: "status" }],
|
||||
},
|
||||
sebby: {
|
||||
allowlist: [{ pattern: "deploy" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-agent-security-unapproved",
|
||||
ocPath: 'oc://exec-approvals.json/agents/"*"',
|
||||
requirement: "oc://policy.jsonc/scopes/restricted/execApprovals/agents/allowSecurity",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-allowlist-unexpected",
|
||||
ocPath: 'oc://exec-approvals.json/agents/"*"/allowlist/#0',
|
||||
requirement:
|
||||
"oc://policy.jsonc/scopes/restricted/execApprovals/agents/allowlist/expected",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("applies wildcard autoAllowSkills posture to scoped exec approvals", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
scopes: {
|
||||
restricted: {
|
||||
agentIds: ["sebby"],
|
||||
execApprovals: {
|
||||
agents: { allowAutoAllowSkills: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
agents: {
|
||||
"*": { autoAllowSkills: true },
|
||||
buddy: { autoAllowSkills: true },
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-auto-allow-skills-enabled",
|
||||
ocPath: 'oc://exec-approvals.json/agents/"*"',
|
||||
requirement:
|
||||
"oc://policy.jsonc/scopes/restricted/execApprovals/agents/allowAutoAllowSkills",
|
||||
}),
|
||||
]);
|
||||
expect(result.findings).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ ocPath: expect.stringContaining("agents/buddy") }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("applies inherited default autoAllowSkills posture to scoped exec approvals", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
scopes: {
|
||||
restricted: {
|
||||
agentIds: ["sebby"],
|
||||
execApprovals: {
|
||||
agents: { allowAutoAllowSkills: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
defaults: { autoAllowSkills: true },
|
||||
agents: {
|
||||
sebby: { allowlist: [{ pattern: "deploy" }] },
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-auto-allow-skills-enabled",
|
||||
ocPath: "oc://exec-approvals.json/defaults",
|
||||
requirement:
|
||||
"oc://policy.jsonc/scopes/restricted/execApprovals/agents/allowAutoAllowSkills",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("evaluates legacy default exec approvals for scoped main policies", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
scopes: {
|
||||
restricted: {
|
||||
agentIds: ["main"],
|
||||
execApprovals: {
|
||||
agents: {
|
||||
allowSecurity: ["deny"],
|
||||
allowlist: { expected: ["legacy", "doctor"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
defaults: { security: "deny" },
|
||||
agents: {
|
||||
default: {
|
||||
security: "allowlist",
|
||||
allowlist: ["legacy", { pattern: "doctor" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-agent-security-unapproved",
|
||||
ocPath: "oc://exec-approvals.json/agents/default",
|
||||
target: "oc://exec-approvals.json/agents/default",
|
||||
requirement: "oc://policy.jsonc/scopes/restricted/execApprovals/agents/allowSecurity",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_HOME for the default exec approvals artifact path", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
const openclawHome = join(workspaceDir, "home");
|
||||
const approvalsDir = join(openclawHome, ".openclaw");
|
||||
const previousOpenClawHome = process.env.OPENCLAW_HOME;
|
||||
await fs.mkdir(approvalsDir, { 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(approvalsDir, "exec-approvals.json"),
|
||||
JSON.stringify({ version: 1, defaults: { security: "full" } }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
process.env.OPENCLAW_HOME = openclawHome;
|
||||
try {
|
||||
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",
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (previousOpenClawHome === undefined) {
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
} else {
|
||||
process.env.OPENCLAW_HOME = previousOpenClawHome;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
execApprovals: {
|
||||
agents: {
|
||||
allowlist: {
|
||||
expected: [{ pattern: "deploy", argpattern: "^--prod$" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/policy-jsonc-invalid",
|
||||
target: "oc://policy.jsonc/execApprovals/agents/allowlist/expected/#0",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("targets the missing exec approvals artifact when required", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({ execApprovals: { requireFile: true } }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-missing",
|
||||
target: "oc://exec-approvals.json",
|
||||
requirement: "oc://policy.jsonc/execApprovals/requireFile",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects required versionless exec approvals artifacts", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "policy.jsonc"),
|
||||
JSON.stringify({
|
||||
execApprovals: { requireFile: true, defaults: { allowSecurity: ["deny"] } },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(workspaceDir, "exec-approvals.json"),
|
||||
JSON.stringify({ defaults: { security: "deny" } }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
registerPolicyDoctorChecks();
|
||||
const result = await runDoctorLintChecks(ctx(configPath, cfgWithPolicy()));
|
||||
|
||||
expect(result.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "policy/exec-approvals-invalid",
|
||||
requirement: "oc://policy.jsonc/execApprovals",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports malformed secrets policy values before applying secrets checks", async () => {
|
||||
const configPath = join(workspaceDir, "openclaw.jsonc");
|
||||
await fs.writeFile(configPath, "{}", "utf-8");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -355,19 +355,49 @@ function policyRuleValueIsValid(metadata: PolicyRuleMetadata, value: unknown): b
|
||||
case "string":
|
||||
return typeof value === "string" && policyStringIsAllowed(metadata, value);
|
||||
case "string-list":
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.every(
|
||||
(entry) =>
|
||||
typeof entry === "string" &&
|
||||
entry.trim() !== "" &&
|
||||
policyStringIsAllowed(metadata, entry),
|
||||
)
|
||||
if (!Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
if (isExecApprovalAllowlistExpectedRule(metadata)) {
|
||||
return value.every(isExecApprovalAllowlistRequirement);
|
||||
}
|
||||
return value.every(
|
||||
(entry) =>
|
||||
typeof entry === "string" &&
|
||||
entry.trim() !== "" &&
|
||||
policyStringIsAllowed(metadata, entry),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isExecApprovalAllowlistExpectedRule(metadata: PolicyRuleMetadata): boolean {
|
||||
return metadata.policyPath.join(".") === "execApprovals.agents.allowlist.expected";
|
||||
}
|
||||
|
||||
function unsupportedPolicyKey(
|
||||
value: Record<string, unknown>,
|
||||
supported: readonly string[],
|
||||
): string | undefined {
|
||||
return Object.keys(value).find((key) => !supported.includes(key));
|
||||
}
|
||||
|
||||
function isExecApprovalAllowlistRequirement(value: unknown): boolean {
|
||||
if (typeof value === "string") {
|
||||
return value.trim() !== "";
|
||||
}
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
if (unsupportedPolicyKey(value, ["argPattern", "pattern"]) !== undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof value.pattern !== "string" || value.pattern.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
return value.argPattern === undefined || typeof value.argPattern === "string";
|
||||
}
|
||||
|
||||
function policyStringIsAllowed(metadata: PolicyRuleMetadata, value: string): boolean {
|
||||
const normalized = metadata.caseSensitive === true ? value.trim() : value.trim().toLowerCase();
|
||||
if (normalized === "") {
|
||||
@@ -506,7 +536,25 @@ function collectScopedPolicyRuleClaims(document: PolicyDocument): readonly Polic
|
||||
}
|
||||
}
|
||||
}
|
||||
return claims;
|
||||
return coalesceScopedPolicyRuleClaims(claims);
|
||||
}
|
||||
|
||||
function coalesceScopedPolicyRuleClaims(
|
||||
claims: readonly PolicyRuleClaim[],
|
||||
): readonly PolicyRuleClaim[] {
|
||||
const byKey = new Map<string, PolicyRuleClaim>();
|
||||
for (const claim of claims) {
|
||||
const previous = byKey.get(claim.key);
|
||||
if (
|
||||
previous !== undefined &&
|
||||
isPolicyValueAtLeastAsStrict(previous.metadata, claim.value, previous.value)
|
||||
) {
|
||||
byKey.set(claim.key, claim);
|
||||
continue;
|
||||
}
|
||||
byKey.set(claim.key, previous ?? claim);
|
||||
}
|
||||
return [...byKey.values()];
|
||||
}
|
||||
|
||||
function normalizeSelectorValues(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Policy tests cover policy state plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { scanPolicyChannels, scanPolicyTools } from "./policy-state.js";
|
||||
import { scanPolicyChannels, scanPolicyExecApprovals, scanPolicyTools } from "./policy-state.js";
|
||||
|
||||
describe("scanPolicyChannels", () => {
|
||||
it("ignores reserved channel config namespaces", () => {
|
||||
@@ -84,3 +84,123 @@ describe("scanPolicyTools", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scanPolicyExecApprovals", () => {
|
||||
it("scans redacted exec approvals posture and allowlist metadata", () => {
|
||||
const evidence = scanPolicyExecApprovals(
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
socket: { path: "/tmp/openclaw.sock", token: "secret-token" },
|
||||
defaults: { security: "full", ask: "off", askFallback: "full", autoAllowSkills: true },
|
||||
agents: {
|
||||
sebby: {
|
||||
security: "allowlist",
|
||||
ask: "on-miss",
|
||||
allowlist: [
|
||||
{
|
||||
pattern: "deploy",
|
||||
argPattern: "^--prod$",
|
||||
source: "allow-always",
|
||||
commandText: "deploy --prod",
|
||||
lastUsedCommand: "deploy --prod",
|
||||
},
|
||||
{
|
||||
pattern: "inspect",
|
||||
source: "free-form text that must not leak",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(evidence).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "defaults",
|
||||
kind: "defaults",
|
||||
security: "full",
|
||||
autoAllowSkills: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "agent:sebby",
|
||||
kind: "agent",
|
||||
agentId: "sebby",
|
||||
security: "allowlist",
|
||||
ask: "on-miss",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "agent:sebby:allowlist:0",
|
||||
kind: "allowlist",
|
||||
agentId: "sebby",
|
||||
pattern: "deploy",
|
||||
argPattern: "^--prod$",
|
||||
entrySource: "allow-always",
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
entrySource: "free-form text that must not leak",
|
||||
}),
|
||||
]);
|
||||
expect(JSON.stringify(evidence)).not.toContain("secret-token");
|
||||
expect(JSON.stringify(evidence)).not.toContain("deploy --prod");
|
||||
expect(JSON.stringify(evidence)).not.toContain("free-form text that must not leak");
|
||||
});
|
||||
|
||||
it("omits malformed exec approval mode fields", () => {
|
||||
expect(
|
||||
scanPolicyExecApprovals(
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
defaults: { security: "bogus", ask: "bad", askFallback: "nope" },
|
||||
agents: {
|
||||
sebby: { security: "bogus", ask: "bad", askFallback: "nope" },
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
expect.not.objectContaining({ security: expect.any(String) }),
|
||||
expect.not.objectContaining({ security: expect.any(String) }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("normalizes legacy default agents and string allowlist entries", () => {
|
||||
expect(
|
||||
scanPolicyExecApprovals(
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
agents: {
|
||||
default: {
|
||||
security: "allowlist",
|
||||
allowlist: ["legacy", { pattern: "doctor" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "defaults",
|
||||
kind: "defaults",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "agent:main",
|
||||
kind: "agent",
|
||||
agentId: "main",
|
||||
security: "allowlist",
|
||||
source: "oc://exec-approvals.json/agents/default",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "agent:main:allowlist:0",
|
||||
kind: "allowlist",
|
||||
agentId: "main",
|
||||
pattern: "legacy",
|
||||
source: "oc://exec-approvals.json/agents/default/allowlist/#0",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "agent:main:allowlist:1",
|
||||
kind: "allowlist",
|
||||
agentId: "main",
|
||||
pattern: "doctor",
|
||||
source: "oc://exec-approvals.json/agents/default/allowlist/#1",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { POLICY_TOOL_GROUPS } from "./tool-policy-conformance.js";
|
||||
|
||||
// Mirrors the sandbox browser config default without importing core internals into the policy plugin.
|
||||
const DEFAULT_POLICY_SANDBOX_BROWSER_NETWORK = "openclaw-sandbox-browser";
|
||||
const DEFAULT_EXEC_APPROVAL_AGENT_ID = "main";
|
||||
const ALLOWLIST_DEFAULT_INGRESS_GROUP_POLICY_CHANNELS = new Set([
|
||||
"googlechat",
|
||||
"irc",
|
||||
@@ -53,6 +54,7 @@ export type PolicyEvidence = {
|
||||
readonly dataHandling?: readonly PolicyDataHandlingEvidence[];
|
||||
readonly secrets?: readonly PolicySecretEvidence[];
|
||||
readonly authProfiles?: readonly PolicyAuthProfileEvidence[];
|
||||
readonly execApprovals?: readonly PolicyExecApprovalEvidence[];
|
||||
};
|
||||
|
||||
export type PolicyChannelEvidence = {
|
||||
@@ -209,6 +211,21 @@ export type PolicyAuthProfileEvidence = {
|
||||
readonly mode?: string;
|
||||
};
|
||||
|
||||
export type PolicyExecApprovalEvidence = {
|
||||
readonly id: string;
|
||||
readonly kind: "agent" | "allowlist" | "defaults";
|
||||
readonly source: string;
|
||||
readonly agentId?: string;
|
||||
readonly security?: string;
|
||||
readonly securityConfigured?: boolean;
|
||||
readonly ask?: string;
|
||||
readonly askFallback?: string;
|
||||
readonly autoAllowSkills?: boolean;
|
||||
readonly pattern?: string;
|
||||
readonly argPattern?: string;
|
||||
readonly entrySource?: string;
|
||||
};
|
||||
|
||||
export type PolicyDataHandlingEvidence = {
|
||||
readonly id: string;
|
||||
readonly kind:
|
||||
@@ -302,6 +319,8 @@ export function collectPolicyEvidence(
|
||||
readonly includeSandboxPosture?: boolean;
|
||||
readonly includeSecrets?: boolean;
|
||||
readonly includeAuthProfiles?: boolean;
|
||||
readonly execApprovalsRaw?: string | null;
|
||||
readonly includeExecApprovals?: boolean;
|
||||
},
|
||||
): PolicyEvidence;
|
||||
export function collectPolicyEvidence(
|
||||
@@ -316,6 +335,8 @@ export function collectPolicyEvidence(
|
||||
readonly includeSandboxPosture?: boolean;
|
||||
readonly includeSecrets?: boolean;
|
||||
readonly includeAuthProfiles?: boolean;
|
||||
readonly execApprovalsRaw?: string | null;
|
||||
readonly includeExecApprovals?: boolean;
|
||||
},
|
||||
): Promise<PolicyEvidence>;
|
||||
export function collectPolicyEvidence(
|
||||
@@ -330,6 +351,8 @@ export function collectPolicyEvidence(
|
||||
readonly includeSandboxPosture?: boolean;
|
||||
readonly includeSecrets?: boolean;
|
||||
readonly includeAuthProfiles?: boolean;
|
||||
readonly execApprovalsRaw?: string | null;
|
||||
readonly includeExecApprovals?: boolean;
|
||||
} = {},
|
||||
): PolicyEvidence | Promise<PolicyEvidence> {
|
||||
const evidence = {
|
||||
@@ -352,6 +375,14 @@ export function collectPolicyEvidence(
|
||||
: { sandboxPosture: scanPolicySandboxPosture(cfg) }),
|
||||
...(options.includeSecrets === false ? {} : { secrets: scanPolicySecrets(cfg) }),
|
||||
...(options.includeAuthProfiles === false ? {} : { authProfiles: scanPolicyAuthProfiles(cfg) }),
|
||||
...(options.includeExecApprovals === false || options.execApprovalsRaw === undefined
|
||||
? {}
|
||||
: {
|
||||
execApprovals:
|
||||
options.execApprovalsRaw === null
|
||||
? []
|
||||
: scanPolicyExecApprovals(options.execApprovalsRaw),
|
||||
}),
|
||||
};
|
||||
if (options.toolsRaw === undefined) {
|
||||
return evidence;
|
||||
@@ -359,6 +390,278 @@ export function collectPolicyEvidence(
|
||||
return scanPolicyTools(options.toolsRaw).then((tools) => ({ ...evidence, tools }));
|
||||
}
|
||||
|
||||
export function scanPolicyExecApprovals(raw: string): readonly PolicyExecApprovalEvidence[] {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
if (!isRecord(parsed) || parsed.version !== 1) {
|
||||
return [];
|
||||
}
|
||||
const evidence: PolicyExecApprovalEvidence[] = [];
|
||||
const defaults = isRecord(parsed.defaults) ? parsed.defaults : {};
|
||||
evidence.push(
|
||||
execApprovalPostureEvidence(
|
||||
"defaults",
|
||||
"defaults",
|
||||
defaults,
|
||||
"oc://exec-approvals.json/defaults",
|
||||
),
|
||||
);
|
||||
|
||||
for (const agent of normalizedExecApprovalAgents(parsed.agents)) {
|
||||
const agentSource = `oc://exec-approvals.json/agents/${ocPathSegment(agent.sourceAgentId)}`;
|
||||
evidence.push(
|
||||
execApprovalPostureEvidence(
|
||||
`agent:${agent.agentId}`,
|
||||
"agent",
|
||||
agent.value,
|
||||
agentSource,
|
||||
agent.agentId,
|
||||
),
|
||||
);
|
||||
for (const [index, entry] of agent.allowlistEntries.entries()) {
|
||||
const allowlistSource = `oc://exec-approvals.json/agents/${ocPathSegment(
|
||||
entry.sourceAgentId,
|
||||
)}/allowlist/#${entry.index}`;
|
||||
evidence.push({
|
||||
id: `agent:${agent.agentId}:allowlist:${index}`,
|
||||
kind: "allowlist",
|
||||
source: allowlistSource,
|
||||
agentId: agent.agentId,
|
||||
pattern: entry.pattern,
|
||||
...(entry.argPattern === undefined ? {} : { argPattern: entry.argPattern }),
|
||||
...(entry.entrySource === undefined ? {} : { entrySource: entry.entrySource }),
|
||||
});
|
||||
}
|
||||
}
|
||||
return evidence;
|
||||
}
|
||||
|
||||
function execApprovalPostureEvidence(
|
||||
id: string,
|
||||
kind: "agent" | "defaults",
|
||||
value: Record<string, unknown>,
|
||||
source: string,
|
||||
agentId?: string,
|
||||
): PolicyExecApprovalEvidence {
|
||||
const security = readExecApprovalSecurity(value.security);
|
||||
const ask = readExecApprovalAsk(value.ask);
|
||||
const askFallback = readExecApprovalSecurity(value.askFallback);
|
||||
const autoAllowSkills = readBoolean(value.autoAllowSkills);
|
||||
return {
|
||||
id,
|
||||
kind,
|
||||
source,
|
||||
...(agentId === undefined ? {} : { agentId }),
|
||||
...(value.security == null ? {} : { securityConfigured: true }),
|
||||
...(security === undefined ? {} : { security }),
|
||||
...(ask === undefined ? {} : { ask }),
|
||||
...(askFallback === undefined ? {} : { askFallback }),
|
||||
...(autoAllowSkills === undefined ? {} : { autoAllowSkills }),
|
||||
};
|
||||
}
|
||||
|
||||
function readExecApprovalSecurity(value: unknown): string | undefined {
|
||||
const normalized = readString(value);
|
||||
return normalized === "deny" || normalized === "allowlist" || normalized === "full"
|
||||
? normalized
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function readExecApprovalAsk(value: unknown): string | undefined {
|
||||
const normalized = readString(value);
|
||||
return normalized === "off" || normalized === "on-miss" || normalized === "always"
|
||||
? normalized
|
||||
: undefined;
|
||||
}
|
||||
|
||||
type NormalizedExecApprovalAllowlistEntry = ReturnType<
|
||||
typeof execApprovalAllowlistEntries
|
||||
>[number] & {
|
||||
readonly sourceAgentId: string;
|
||||
};
|
||||
|
||||
type NormalizedExecApprovalAgent = {
|
||||
readonly agentId: string;
|
||||
readonly sourceAgentId: string;
|
||||
readonly value: Record<string, unknown>;
|
||||
readonly allowlistEntries: readonly NormalizedExecApprovalAllowlistEntry[];
|
||||
};
|
||||
|
||||
function normalizedExecApprovalAgents(rawAgents: unknown): readonly NormalizedExecApprovalAgent[] {
|
||||
if (!isRecord(rawAgents)) {
|
||||
return [];
|
||||
}
|
||||
const agents = Object.entries(rawAgents).filter(
|
||||
(entry): entry is [string, Record<string, unknown>] => isRecord(entry[1]),
|
||||
);
|
||||
const legacyDefault = agents.find(([agentId]) => agentId === "default")?.[1];
|
||||
const normalized = agents
|
||||
.filter(([agentId]) => agentId !== "default")
|
||||
.map(([agentId, value]): NormalizedExecApprovalAgent => {
|
||||
if (agentId === DEFAULT_EXEC_APPROVAL_AGENT_ID && legacyDefault !== undefined) {
|
||||
return {
|
||||
agentId,
|
||||
sourceAgentId: agentId,
|
||||
value: mergeLegacyExecApprovalAgent(value, legacyDefault),
|
||||
allowlistEntries: mergedExecApprovalAllowlistEntries(
|
||||
value.allowlist,
|
||||
legacyDefault.allowlist,
|
||||
),
|
||||
};
|
||||
}
|
||||
return execApprovalAgentFromParts(agentId, agentId, value);
|
||||
});
|
||||
if (
|
||||
legacyDefault !== undefined &&
|
||||
!agents.some(([agentId]) => agentId === DEFAULT_EXEC_APPROVAL_AGENT_ID)
|
||||
) {
|
||||
normalized.push(
|
||||
execApprovalAgentFromParts(DEFAULT_EXEC_APPROVAL_AGENT_ID, "default", legacyDefault),
|
||||
);
|
||||
}
|
||||
return normalized.toSorted((a, b) => a.agentId.localeCompare(b.agentId));
|
||||
}
|
||||
|
||||
function execApprovalAgentFromParts(
|
||||
agentId: string,
|
||||
sourceAgentId: string,
|
||||
value: Record<string, unknown>,
|
||||
): NormalizedExecApprovalAgent {
|
||||
const allowlistEntries = execApprovalAllowlistEntries(value.allowlist).map(
|
||||
(entry): NormalizedExecApprovalAllowlistEntry => ({
|
||||
index: entry.index,
|
||||
pattern: entry.pattern,
|
||||
argPattern: entry.argPattern,
|
||||
entrySource: entry.entrySource,
|
||||
sourceAgentId,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
agentId,
|
||||
sourceAgentId,
|
||||
value,
|
||||
allowlistEntries,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeLegacyExecApprovalAgent(
|
||||
current: Record<string, unknown>,
|
||||
legacy: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
...legacy,
|
||||
...current,
|
||||
security: current.security ?? legacy.security,
|
||||
ask: current.ask ?? legacy.ask,
|
||||
askFallback: current.askFallback ?? legacy.askFallback,
|
||||
autoAllowSkills: current.autoAllowSkills ?? legacy.autoAllowSkills,
|
||||
allowlist: mergedExecApprovalAllowlist(current.allowlist, legacy.allowlist),
|
||||
};
|
||||
}
|
||||
|
||||
function mergedExecApprovalAllowlist(
|
||||
current: unknown,
|
||||
legacy: unknown,
|
||||
): readonly unknown[] | undefined {
|
||||
const entries = mergedExecApprovalAllowlistEntries(current, legacy).map((entry) => {
|
||||
const allowlistEntry: Record<string, unknown> = { pattern: entry.pattern };
|
||||
if (entry.argPattern !== undefined) {
|
||||
allowlistEntry.argPattern = entry.argPattern;
|
||||
}
|
||||
if (entry.entrySource !== undefined) {
|
||||
allowlistEntry.source = entry.entrySource;
|
||||
}
|
||||
return allowlistEntry;
|
||||
});
|
||||
return entries.length === 0 ? undefined : entries;
|
||||
}
|
||||
|
||||
function mergedExecApprovalAllowlistEntries(
|
||||
current: unknown,
|
||||
legacy: unknown,
|
||||
): readonly NormalizedExecApprovalAllowlistEntry[] {
|
||||
const entries: NormalizedExecApprovalAllowlistEntry[] = [];
|
||||
const seen = new Set<string>();
|
||||
const appendEntries = (sourceEntries: readonly NormalizedExecApprovalAllowlistEntry[]) => {
|
||||
for (const sourceEntry of sourceEntries) {
|
||||
const key = `${sourceEntry.pattern.toLowerCase()}\x00${sourceEntry.argPattern ?? ""}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
entries.push(sourceEntry);
|
||||
}
|
||||
};
|
||||
appendEntries(withExecApprovalAllowlistSource(current, DEFAULT_EXEC_APPROVAL_AGENT_ID));
|
||||
appendEntries(withExecApprovalAllowlistSource(legacy, "default"));
|
||||
return entries;
|
||||
}
|
||||
|
||||
function withExecApprovalAllowlistSource(
|
||||
value: unknown,
|
||||
sourceAgentId: string,
|
||||
): readonly NormalizedExecApprovalAllowlistEntry[] {
|
||||
return execApprovalAllowlistEntries(value).map(
|
||||
(entry): NormalizedExecApprovalAllowlistEntry => ({
|
||||
index: entry.index,
|
||||
pattern: entry.pattern,
|
||||
argPattern: entry.argPattern,
|
||||
entrySource: entry.entrySource,
|
||||
sourceAgentId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function readExecApprovalAllowlistEntrySource(value: unknown): "allow-always" | undefined {
|
||||
return readString(value) === "allow-always" ? "allow-always" : undefined;
|
||||
}
|
||||
|
||||
function execApprovalAllowlistEntries(value: unknown): readonly {
|
||||
readonly index: number;
|
||||
readonly pattern: string;
|
||||
readonly argPattern?: string;
|
||||
readonly entrySource?: string;
|
||||
}[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const entries: {
|
||||
readonly index: number;
|
||||
readonly pattern: string;
|
||||
readonly argPattern?: string;
|
||||
readonly entrySource?: string;
|
||||
}[] = [];
|
||||
for (const [index, entry] of value.entries()) {
|
||||
if (typeof entry === "string") {
|
||||
const pattern = entry.trim();
|
||||
if (pattern !== "") {
|
||||
entries.push({ index, pattern });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!isRecord(entry)) {
|
||||
continue;
|
||||
}
|
||||
const pattern = readString(entry.pattern);
|
||||
if (pattern === undefined) {
|
||||
continue;
|
||||
}
|
||||
const argPattern = readString(entry.argPattern);
|
||||
const entrySource = readExecApprovalAllowlistEntrySource(entry.source);
|
||||
entries.push({
|
||||
index,
|
||||
pattern,
|
||||
...(argPattern === undefined ? {} : { argPattern }),
|
||||
...(entrySource === undefined ? {} : { entrySource }),
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function scanPolicyChannels(cfg: Record<string, unknown>): readonly PolicyChannelEvidence[] {
|
||||
return Object.entries(configuredChannels(cfg))
|
||||
.filter(([id]) => !RESERVED_CHANNEL_CONFIG_KEYS.has(id))
|
||||
|
||||
Reference in New Issue
Block a user