Skip to content

Commit 6dbd5bd

Browse files
authored
Policy: add model, network, and MCP conformance checks (#80783)
* feat(policy): add model network and mcp conformance checks * fix(policy): validate conformance rule shapes * fix(policy): quote dynamic evidence paths * fix(policy): scan per-agent model maps * fix(policy): normalize model provider conformance
1 parent 2bb00f6 commit 6dbd5bd

7 files changed

Lines changed: 1607 additions & 33 deletions

File tree

docs/cli/policy.md

Lines changed: 121 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ report drift through `doctor --lint`. The final conformance signal is a clean
1717
`doctor --lint` run; policy contributes findings to that shared lint surface
1818
instead of creating a separate health gate.
1919

20-
Policy currently manages configured channels and governed tool declarations.
21-
For example, IT or a workspace operator can record that Telegram is not an
22-
approved channel provider, require governed tools to carry risk and sensitivity
23-
metadata, then use `doctor --lint` as the shared conformance gate.
20+
Policy currently manages configured channels, MCP servers, model providers,
21+
network SSRF posture, and governed tool declarations. For example, IT or a
22+
workspace operator can record that Telegram is not an approved channel
23+
provider, restrict MCP servers and model refs to approved entries, require
24+
private-network fetch/browser access to remain disabled, require governed tools
25+
to carry risk and sensitivity metadata, then use `doctor --lint` as the shared
26+
conformance gate.
2427

2528
Use policy when a workspace needs a durable statement such as "these channels
2629
must not be enabled" or "governed tools must declare approval metadata" and a
@@ -41,7 +44,8 @@ arbitrary plugins. The plugin remains enabled if `policy.jsonc` is missing, so
4144
doctor can report the missing artifact.
4245

4346
Policy is authored, not generated from the user's current settings. A minimal
44-
policy for channels and tool metadata looks like this:
47+
policy for channels, MCP servers, model providers, network posture, and tool
48+
metadata looks like this:
4549

4650
```jsonc
4751
{
@@ -54,6 +58,23 @@ policy for channels and tool metadata looks like this:
5458
},
5559
],
5660
},
61+
"mcp": {
62+
"servers": {
63+
"allow": ["docs"],
64+
"deny": ["untrusted"],
65+
},
66+
},
67+
"models": {
68+
"providers": {
69+
"allow": ["openai", "anthropic"],
70+
"deny": ["openrouter"],
71+
},
72+
},
73+
"network": {
74+
"privateNetwork": {
75+
"allow": false,
76+
},
77+
},
5778
"tools": {
5879
"requireMetadata": ["risk", "sensitivity", "owner"],
5980
},
@@ -62,8 +83,9 @@ policy for channels and tool metadata looks like this:
6283

6384
The rules are the authority. A category block is only a namespace; checks run
6485
when a concrete rule is present. OpenClaw reads current `channels.*` settings
65-
and `TOOLS.md` declarations as evidence, then reports observed state that does
66-
not conform.
86+
`mcp.servers.*`, `models.providers.*`, selected agent model refs, network SSRF
87+
settings, and `TOOLS.md` declarations as evidence, then reports observed state
88+
that does not conform.
6789

6890
Run policy-only checks during authoring:
6991

@@ -167,6 +189,35 @@ Example JSON output:
167189
"enabled": false
168190
}
169191
],
192+
"mcpServers": [
193+
{
194+
"id": "docs",
195+
"transport": "stdio",
196+
"source": "oc://openclaw.config/mcp/servers/docs",
197+
"command": "npx"
198+
}
199+
],
200+
"modelProviders": [
201+
{
202+
"id": "openai",
203+
"source": "oc://openclaw.config/models/providers/openai"
204+
}
205+
],
206+
"modelRefs": [
207+
{
208+
"ref": "openai/gpt-5.5",
209+
"provider": "openai",
210+
"model": "gpt-5.5",
211+
"source": "oc://openclaw.config/agents/defaults/model"
212+
}
213+
],
214+
"network": [
215+
{
216+
"id": "browser-private-network",
217+
"source": "oc://openclaw.config/browser/ssrfPolicy/dangerouslyAllowPrivateNetwork",
218+
"value": false
219+
}
220+
],
170221
"tools": [
171222
{
172223
"id": "deploy",
@@ -178,7 +229,7 @@ Example JSON output:
178229
}
179230
]
180231
},
181-
"checksRun": 6,
232+
"checksRun": 15,
182233
"checksSkipped": 0,
183234
"findings": []
184235
}
@@ -226,18 +277,23 @@ choose a different interval.
226277

227278
Policy currently verifies:
228279

229-
| Check id | Finding |
230-
| ---------------------------------------- | ------------------------------------------------------------------- |
231-
| `policy/policy-jsonc-missing` | Policy is enabled but `policy.jsonc` is missing. |
232-
| `policy/policy-jsonc-invalid` | Policy cannot be parsed or has malformed rules. |
233-
| `policy/policy-hash-mismatch` | Policy does not match configured `expectedHash`. |
234-
| `policy/attestation-hash-mismatch` | Current policy evidence no longer matches the accepted attestation. |
235-
| `policy/channels-denied-provider` | An enabled channel matches a channel deny rule. |
236-
| `policy/tools-missing-owner` | A governed tool declaration is missing owner metadata. |
237-
| `policy/tools-missing-risk-level` | A governed tool declaration is missing risk metadata. |
238-
| `policy/tools-missing-sensitivity-token` | A governed tool declaration is missing sensitivity metadata. |
239-
| `policy/tools-unknown-risk-level` | A governed tool declaration uses an unknown risk value. |
240-
| `policy/tools-unknown-sensitivity-token` | A governed tool declaration uses an unknown sensitivity value. |
280+
| Check id | Finding |
281+
| ---------------------------------------- | --------------------------------------------------------------------- |
282+
| `policy/policy-jsonc-missing` | Policy is enabled but `policy.jsonc` is missing. |
283+
| `policy/policy-jsonc-invalid` | Policy cannot be parsed or contains malformed rule entries. |
284+
| `policy/policy-hash-mismatch` | Policy does not match configured `expectedHash`. |
285+
| `policy/attestation-hash-mismatch` | Current policy evidence no longer matches the accepted attestation. |
286+
| `policy/channels-denied-provider` | An enabled channel matches a channel deny rule. |
287+
| `policy/mcp-denied-server` | A configured MCP server is denied by policy. |
288+
| `policy/mcp-unapproved-server` | A configured MCP server is outside the allowlist. |
289+
| `policy/models-denied-provider` | A configured model provider or model ref uses a denied provider. |
290+
| `policy/models-unapproved-provider` | A configured model provider or model ref is outside the allowlist. |
291+
| `policy/network-private-access-enabled` | A private-network SSRF escape hatch is enabled when policy denies it. |
292+
| `policy/tools-missing-risk-level` | A governed tool declaration is missing risk metadata. |
293+
| `policy/tools-unknown-risk-level` | A governed tool declaration uses an unknown risk value. |
294+
| `policy/tools-missing-sensitivity-token` | A governed tool declaration is missing sensitivity metadata. |
295+
| `policy/tools-missing-owner` | A governed tool declaration is missing owner metadata. |
296+
| `policy/tools-unknown-sensitivity-token` | A governed tool declaration uses an unknown sensitivity value. |
241297

242298
Policy findings can include both `target` and `requirement`. `target` is the
243299
observed workspace thing that does not conform. `requirement` is the authored
@@ -277,6 +333,51 @@ Example tool finding:
277333
}
278334
```
279335

336+
Example MCP finding:
337+
338+
```json
339+
{
340+
"checkId": "policy/mcp-unapproved-server",
341+
"severity": "error",
342+
"message": "MCP server 'remote' is not in the policy allowlist.",
343+
"source": "policy",
344+
"path": "openclaw config",
345+
"ocPath": "oc://openclaw.config/mcp/servers/remote",
346+
"target": "oc://openclaw.config/mcp/servers/remote",
347+
"requirement": "oc://policy.jsonc/mcp/servers/allow"
348+
}
349+
```
350+
351+
Example model-provider finding:
352+
353+
```json
354+
{
355+
"checkId": "policy/models-unapproved-provider",
356+
"severity": "error",
357+
"message": "Model ref 'anthropic/claude-sonnet-4.7' uses unapproved provider 'anthropic'.",
358+
"source": "policy",
359+
"path": "openclaw config",
360+
"ocPath": "oc://openclaw.config/agents/defaults/model/fallbacks/#0",
361+
"target": "oc://openclaw.config/agents/defaults/model/fallbacks/#0",
362+
"requirement": "oc://policy.jsonc/models/providers/allow"
363+
}
364+
```
365+
366+
Example network finding:
367+
368+
```json
369+
{
370+
"checkId": "policy/network-private-access-enabled",
371+
"severity": "error",
372+
"message": "Network setting 'browser-private-network' allows private-network access.",
373+
"source": "policy",
374+
"path": "openclaw config",
375+
"ocPath": "oc://openclaw.config/browser/ssrfPolicy/dangerouslyAllowPrivateNetwork",
376+
"target": "oc://openclaw.config/browser/ssrfPolicy/dangerouslyAllowPrivateNetwork",
377+
"requirement": "oc://policy.jsonc/network/privateNetwork/allow"
378+
}
379+
```
380+
280381
## Repair
281382

282383
`doctor --lint` and `policy check` are read-only.

docs/plugins/reference/policy.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
2-
summary: "Adds policy-backed doctor checks for workspace conformance."
2+
summary: "Policy-backed doctor checks for workspace conformance."
33
read_when:
44
- You are installing, configuring, or auditing the policy plugin
55
title: "Policy plugin"
66
---
77

88
# Policy plugin
99

10-
Adds policy-backed doctor checks for workspace conformance.
10+
Policy-backed doctor checks for workspace conformance.
1111

1212
## Distribution
1313

@@ -16,8 +16,22 @@ Adds policy-backed doctor checks for workspace conformance.
1616

1717
## Surface
1818

19-
plugin
19+
plugin; CLI command: [`openclaw policy`](/cli/policy)
20+
21+
## Behavior
22+
23+
The Policy plugin contributes doctor health checks for policy-managed OpenClaw
24+
settings and governed workspace declarations. Policy currently covers channel
25+
conformance, governed tool metadata, MCP server posture, model-provider posture,
26+
and private-network access posture.
27+
28+
Policy stores authored requirements in `policy.jsonc`, observes existing
29+
OpenClaw settings and workspace declarations as evidence, and reports drift
30+
through `openclaw policy check` and `openclaw doctor --lint`. A clean policy
31+
check emits policy, evidence, findings, and attestation hashes that operators
32+
can record for audit.
2033

2134
## Related docs
2235

23-
- [policy](/cli/policy)
36+
- [Policy CLI](/cli/policy)
37+
- [Doctor lint mode](/cli/doctor#lint-mode)

extensions/policy/src/cli.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,13 @@ describe("policy commands", () => {
7272

7373
expect(exitCode).toBe(0);
7474
const policyHash = policyDocumentHash(policy);
75-
const evidence = { channels: [] };
75+
const evidence = {
76+
channels: [],
77+
mcpServers: [],
78+
modelProviders: [],
79+
modelRefs: [],
80+
network: [],
81+
};
7682
const workspaceHash = policyWorkspaceHash(evidence);
7783
const findingsHash = policyFindingsHash([]);
7884
expect(typeof parsed.attestation.checkedAt).toBe("string");
@@ -101,6 +107,44 @@ describe("policy commands", () => {
101107
});
102108
});
103109

110+
it("reports policy findings in policy check output", async () => {
111+
await fs.writeFile(
112+
join(workspaceDir, "policy.jsonc"),
113+
JSON.stringify({
114+
channels: {
115+
denyRules: [{ id: "no-telegram", when: { provider: "telegram" } }],
116+
},
117+
}),
118+
"utf-8",
119+
);
120+
const output: string[] = [];
121+
122+
const exitCode = await policyCheckCommand(
123+
{ cwd: workspaceDir, json: true },
124+
{
125+
writeStdout(value) {
126+
output.push(value);
127+
},
128+
error(value) {
129+
output.push(value);
130+
},
131+
},
132+
);
133+
134+
expect(exitCode).toBe(0);
135+
expect(JSON.parse(output.at(-1) ?? "{}")).toMatchObject({
136+
ok: true,
137+
evidence: {
138+
channels: [],
139+
mcpServers: [],
140+
modelProviders: [],
141+
modelRefs: [],
142+
network: [],
143+
},
144+
findings: [],
145+
});
146+
});
147+
104148
it("reports malformed policy rules in policy check output", async () => {
105149
await fs.writeFile(
106150
join(workspaceDir, "policy.jsonc"),

0 commit comments

Comments
 (0)