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:
Gio Della-Libera
2026-06-15 17:30:48 -07:00
committed by GitHub
parent 01acb34bdb
commit 55263b3dfa
7 changed files with 2514 additions and 22 deletions
+92 -10
View File
@@ -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. |
+114
View File
@@ -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
+57 -9
View File
@@ -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(
+121 -1
View File
@@ -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",
}),
]);
});
});
+303
View File
@@ -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))