mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 19:04:05 +00:00
Policy: add sandbox posture conformance checks (#85572)
Policy: add sandbox posture conformance checks (#85572)
Merged via squash.
Prepared head SHA: 1cf1953d8c
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
This commit is contained in:
@@ -172,21 +172,87 @@ present in `policy.jsonc`. The observed state is existing OpenClaw config or
|
||||
workspace metadata; policy reports drift but does not rewrite runtime behavior
|
||||
unless a repair path is explicitly available and enabled.
|
||||
|
||||
Agent-specific policy overlays keep broad `tools.*` and `agents.workspace`
|
||||
posture global, then let named scope blocks add stricter normal policy sections
|
||||
for explicit `agentIds` under `scopes.<scopeName>`. The initial scoped
|
||||
sections are `tools` and `agents.workspace`; sandbox and ingress can use the
|
||||
same container once their evidence is attributable to an agent. Scoped fields
|
||||
carry strictness metadata such as allowlist subset, denylist superset, required
|
||||
boolean, and exact-list semantics so future policy-file conformance can reuse
|
||||
the same rule inventory instead of guessing. The overlay is additive: global
|
||||
claims still run, and a scoped claim can emit its own finding against the same
|
||||
observed config. See [Agent-scoped policy overlays](/plan/policy-agent-scoped-overlays).
|
||||
Every scope present in `policy.jsonc` must be valid and enforceable. Scopes
|
||||
currently require `agentIds`, and that selector supports only `tools.*` and
|
||||
`agents.workspace.*`. If an `agentIds` entry is not present in `agents.list[]`,
|
||||
the scoped rule is evaluated against the inherited global/default posture for
|
||||
that runtime agent id instead of being skipped.
|
||||
Policy overlays keep broad top-level rules global, then let named scope blocks
|
||||
add stricter normal policy sections for explicit selectors. A scope name is a
|
||||
descriptive bucket only; matching uses the selector values inside the scope.
|
||||
The overlay is additive: global claims still run, and a scoped claim can emit
|
||||
its own finding against the same observed config.
|
||||
|
||||
#### Scoped overlays
|
||||
|
||||
Use `scopes.<scopeName>` when one set of agents needs stricter policy than the
|
||||
top-level baseline. Scopes require the `agentIds` selector, which supports
|
||||
`tools.*`, `agents.workspace.*`, and `sandbox.*`. 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
|
||||
global/default posture for that runtime agent id.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"tools": {
|
||||
"exec": {
|
||||
"allowHosts": ["sandbox", "node"],
|
||||
},
|
||||
},
|
||||
"sandbox": {
|
||||
"requireMode": ["all", "non-main"],
|
||||
},
|
||||
"scopes": {
|
||||
"release-workspace": {
|
||||
"agentIds": ["release-agent", "review-agent"],
|
||||
"agents": {
|
||||
"workspace": {
|
||||
"allowedAccess": ["none", "ro"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"release-lockdown": {
|
||||
"agentIds": ["release-agent"],
|
||||
"tools": {
|
||||
"exec": {
|
||||
"allowHosts": ["sandbox"],
|
||||
"allowSecurity": ["deny", "allowlist"],
|
||||
"requireAsk": ["always"],
|
||||
},
|
||||
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
|
||||
},
|
||||
"sandbox": {
|
||||
"requireMode": ["all"],
|
||||
"allowBackends": ["docker"],
|
||||
},
|
||||
},
|
||||
"shell-sandbox": {
|
||||
"agentIds": ["shell-agent"],
|
||||
"sandbox": {
|
||||
"allowBackends": ["openshell"],
|
||||
"containers": {
|
||||
"requireReadOnlyMounts": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The same agent can appear in multiple scopes when each scope governs different
|
||||
fields, as shown above. A repeated scoped field for the same agent must be
|
||||
equally or more restrictive according to policy metadata; weaker duplicate
|
||||
claims are rejected. Strictness metadata treats allow-lists as subsets,
|
||||
deny-lists as supersets, and required booleans as fixed requirements.
|
||||
|
||||
Container posture policy is evaluated only against evidence OpenClaw can
|
||||
observe for the matched agent. If an enabled `sandbox.containers.*` rule applies
|
||||
to an agent whose sandbox backend cannot expose that field, policy reports
|
||||
`policy/sandbox-container-posture-unobservable` instead of treating the claim as
|
||||
passing. Use separate `agentIds` scopes for agent groups that use different
|
||||
sandbox backends, and leave unsupported container rules unset or false for the
|
||||
groups where those fields cannot be observed.
|
||||
|
||||
| Selector | Supported sections | Use when |
|
||||
| ---------- | ------------------------------------------ | ----------------------------------------------- |
|
||||
| `agentIds` | `tools`, `agents.workspace`, and `sandbox` | One or more runtime agents need stricter rules. |
|
||||
|
||||
Every scope present in `policy.jsonc` must be valid and enforceable.
|
||||
|
||||
#### Channels
|
||||
|
||||
@@ -235,6 +301,23 @@ that runtime agent id instead of being skipped.
|
||||
| `agents.workspace.allowedAccess` | `agents.defaults.sandbox.workspaceAccess` and `agents.list[].sandbox.workspaceAccess` | Allow only sandbox workspace access values such as `none` or `ro`. |
|
||||
| `agents.workspace.denyTools` | Global and per-agent tool deny config | Require workspace/runtime mutation tools such as `exec`, `process`, `write`, `edit`, or `apply_patch` to be denied. |
|
||||
|
||||
#### Sandbox posture
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
| ----------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| `sandbox.requireMode` | `agents.defaults.sandbox.mode` and per-agent mode | Allow only reviewed sandbox modes such as `all` or `non-main`. |
|
||||
| `sandbox.allowBackends` | `agents.defaults.sandbox.backend` and per-agent backend | Allow only reviewed sandbox backends such as `docker`. |
|
||||
| `sandbox.containers.denyHostNetwork` | Container-backed sandbox/browser network mode | Deny host network mode. |
|
||||
| `sandbox.containers.denyContainerNamespaceJoin` | Container-backed sandbox/browser network mode | Deny joining another container network namespace. |
|
||||
| `sandbox.containers.requireReadOnlyMounts` | Container-backed sandbox/browser mount mode | Require mounts to be read-only. |
|
||||
| `sandbox.containers.denyContainerRuntimeSocketMounts` | Container-backed sandbox/browser mount targets | Deny container runtime socket mounts. |
|
||||
| `sandbox.containers.denyUnconfinedProfiles` | Container security profile posture | Deny unconfined container security profiles. |
|
||||
| `sandbox.browser.requireCdpSourceRange` | Sandbox browser CDP source range | Require browser CDP exposure to declare a source range. |
|
||||
|
||||
Policy treats missing `sandbox.mode` as the implicit default `off`, so
|
||||
`sandbox.requireMode` reports a fresh or unconfigured sandbox as outside an
|
||||
allowlist such as `["all"]`.
|
||||
|
||||
#### Secrets
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
@@ -522,47 +605,56 @@ choose a different interval.
|
||||
|
||||
Policy currently verifies:
|
||||
|
||||
| Check id | Finding |
|
||||
| -------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `policy/policy-jsonc-missing` | Policy is enabled but `policy.jsonc` is missing. |
|
||||
| `policy/policy-jsonc-invalid` | Policy cannot be parsed or contains malformed rule entries. |
|
||||
| `policy/policy-hash-mismatch` | Policy does not match configured `expectedHash`. |
|
||||
| `policy/attestation-hash-mismatch` | Current policy evidence no longer matches the accepted attestation. |
|
||||
| `policy/channels-denied-provider` | An enabled channel matches a channel deny rule. |
|
||||
| `policy/mcp-denied-server` | A configured MCP server is denied by policy. |
|
||||
| `policy/mcp-unapproved-server` | A configured MCP server is outside the allowlist. |
|
||||
| `policy/models-denied-provider` | A configured model provider or model ref uses a denied provider. |
|
||||
| `policy/models-unapproved-provider` | A configured model provider or model ref is outside the allowlist. |
|
||||
| `policy/network-private-access-enabled` | A private-network SSRF escape hatch is enabled when policy denies it. |
|
||||
| `policy/gateway-non-loopback-bind` | Gateway bind posture permits non-loopback exposure when policy denies it. |
|
||||
| `policy/gateway-auth-disabled` | Gateway authentication is disabled when policy requires auth. |
|
||||
| `policy/gateway-rate-limit-missing` | Gateway auth rate-limit posture is not explicit when policy requires it. |
|
||||
| `policy/gateway-control-ui-insecure` | Gateway Control UI insecure exposure toggles are enabled. |
|
||||
| `policy/gateway-tailscale-funnel` | Gateway Tailscale Funnel exposure is enabled when policy denies it. |
|
||||
| `policy/gateway-remote-enabled` | Gateway remote mode is active when policy denies it. |
|
||||
| `policy/gateway-http-endpoint-enabled` | A Gateway HTTP API endpoint is enabled while denied by policy. |
|
||||
| `policy/gateway-http-url-fetch-unrestricted` | Gateway HTTP URL-fetch input lacks a required URL allowlist. |
|
||||
| `policy/agents-workspace-access-denied` | Agent sandbox mode or workspace access is outside the policy allowlist. |
|
||||
| `policy/agents-tool-not-denied` | An agent or default config does not deny a tool required by policy. |
|
||||
| `policy/tools-profile-unapproved` | A configured global or per-agent tool profile is outside the allowlist. |
|
||||
| `policy/tools-fs-workspace-only-required` | Filesystem tools are not configured with workspace-only path posture. |
|
||||
| `policy/tools-exec-security-unapproved` | Exec security mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-ask-unapproved` | Exec ask mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-host-unapproved` | Exec host routing is outside the policy allowlist. |
|
||||
| `policy/tools-elevated-enabled` | Elevated tool mode is enabled when policy denies it. |
|
||||
| `policy/tools-also-allow-missing` | A configured `alsoAllow` list is missing an entry required by policy. |
|
||||
| `policy/tools-also-allow-unexpected` | A configured `alsoAllow` list includes an entry not expected by policy. |
|
||||
| `policy/tools-required-deny-missing` | A global or per-agent tool deny list does not include a required denied tool. |
|
||||
| `policy/secrets-unmanaged-provider` | A config SecretRef references a provider not declared under `secrets.providers`. |
|
||||
| `policy/secrets-denied-provider-source` | A config secret provider or SecretRef uses a source denied by policy. |
|
||||
| `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/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. |
|
||||
| `policy/tools-missing-owner` | A governed tool declaration is missing owner metadata. |
|
||||
| `policy/tools-unknown-sensitivity-token` | A governed tool declaration uses an unknown sensitivity value. |
|
||||
| Check id | Finding |
|
||||
| ------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| `policy/policy-jsonc-missing` | Policy is enabled but `policy.jsonc` is missing. |
|
||||
| `policy/policy-jsonc-invalid` | Policy cannot be parsed or contains malformed rule entries. |
|
||||
| `policy/policy-hash-mismatch` | Policy does not match configured `expectedHash`. |
|
||||
| `policy/attestation-hash-mismatch` | Current policy evidence no longer matches the accepted attestation. |
|
||||
| `policy/channels-denied-provider` | An enabled channel matches a channel deny rule. |
|
||||
| `policy/mcp-denied-server` | A configured MCP server is denied by policy. |
|
||||
| `policy/mcp-unapproved-server` | A configured MCP server is outside the allowlist. |
|
||||
| `policy/models-denied-provider` | A configured model provider or model ref uses a denied provider. |
|
||||
| `policy/models-unapproved-provider` | A configured model provider or model ref is outside the allowlist. |
|
||||
| `policy/network-private-access-enabled` | A private-network SSRF escape hatch is enabled when policy denies it. |
|
||||
| `policy/gateway-non-loopback-bind` | Gateway bind posture permits non-loopback exposure when policy denies it. |
|
||||
| `policy/gateway-auth-disabled` | Gateway authentication is disabled when policy requires auth. |
|
||||
| `policy/gateway-rate-limit-missing` | Gateway auth rate-limit posture is not explicit when policy requires it. |
|
||||
| `policy/gateway-control-ui-insecure` | Gateway Control UI insecure exposure toggles are enabled. |
|
||||
| `policy/gateway-tailscale-funnel` | Gateway Tailscale Funnel exposure is enabled when policy denies it. |
|
||||
| `policy/gateway-remote-enabled` | Gateway remote mode is active when policy denies it. |
|
||||
| `policy/gateway-http-endpoint-enabled` | A Gateway HTTP API endpoint is enabled while denied by policy. |
|
||||
| `policy/gateway-http-url-fetch-unrestricted` | Gateway HTTP URL-fetch input lacks a required URL allowlist. |
|
||||
| `policy/agents-workspace-access-denied` | Agent sandbox mode or workspace access is outside the policy allowlist. |
|
||||
| `policy/agents-tool-not-denied` | An agent or default config does not deny a tool required by policy. |
|
||||
| `policy/tools-profile-unapproved` | A configured global or per-agent tool profile is outside the allowlist. |
|
||||
| `policy/tools-fs-workspace-only-required` | Filesystem tools are not configured with workspace-only path posture. |
|
||||
| `policy/tools-exec-security-unapproved` | Exec security mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-ask-unapproved` | Exec ask mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-host-unapproved` | Exec host routing is outside the policy allowlist. |
|
||||
| `policy/tools-elevated-enabled` | Elevated tool mode is enabled when policy denies it. |
|
||||
| `policy/tools-also-allow-missing` | A configured `alsoAllow` list is missing an entry required by policy. |
|
||||
| `policy/tools-also-allow-unexpected` | A configured `alsoAllow` list includes an entry not expected by policy. |
|
||||
| `policy/tools-required-deny-missing` | A global or per-agent tool deny list does not include a required denied tool. |
|
||||
| `policy/sandbox-mode-unapproved` | Sandbox mode is outside the policy allowlist. |
|
||||
| `policy/sandbox-backend-unapproved` | Sandbox backend is outside the policy allowlist. |
|
||||
| `policy/sandbox-container-posture-unobservable` | A container posture rule is enabled for a backend that cannot observe it. |
|
||||
| `policy/sandbox-container-host-network-denied` | A container-backed sandbox or browser uses host network mode. |
|
||||
| `policy/sandbox-container-namespace-join-denied` | A container-backed sandbox or browser joins another container namespace. |
|
||||
| `policy/sandbox-container-mount-mode-required` | A container-backed sandbox or browser mount is not read-only. |
|
||||
| `policy/sandbox-container-runtime-socket-mount` | A container-backed sandbox or browser mount exposes the container runtime socket. |
|
||||
| `policy/sandbox-container-unconfined-profile` | Container sandbox profile is unconfined when policy denies it. |
|
||||
| `policy/sandbox-browser-cdp-source-range-missing` | Sandbox browser CDP source range is missing when policy requires one. |
|
||||
| `policy/secrets-unmanaged-provider` | A config SecretRef references a provider not declared under `secrets.providers`. |
|
||||
| `policy/secrets-denied-provider-source` | A config secret provider or SecretRef uses a source denied by policy. |
|
||||
| `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/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. |
|
||||
| `policy/tools-missing-owner` | A governed tool declaration is missing owner metadata. |
|
||||
| `policy/tools-unknown-sensitivity-token` | A governed tool declaration uses an unknown sensitivity value. |
|
||||
|
||||
Policy findings can include both `target` and `requirement`. `target` is the
|
||||
observed workspace thing that does not conform. `requirement` is the authored
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
---
|
||||
summary: "Per-agent Policy plugin overlays layered on top of global policy rules."
|
||||
read_when:
|
||||
- You are designing per-agent policy requirements
|
||||
- You need to distinguish tool posture policy from workspace policy
|
||||
- You are configuring stricter policy for one named agent
|
||||
title: "Agent-scoped policy overlays"
|
||||
---
|
||||
|
||||
# Agent-scoped policy overlays
|
||||
|
||||
OpenClaw policy supports global requirements and stricter requirements for
|
||||
explicit runtime agent ids. Some deployments need one agent to use a tighter
|
||||
workspace and tool posture than other agents, but deployment-wide rules should
|
||||
not force every agent to use the same posture.
|
||||
|
||||
This page describes the agent-scoped overlay model. The field reference remains
|
||||
[`openclaw policy`](/cli/policy).
|
||||
|
||||
## Design goals
|
||||
|
||||
- Keep global policy as the deployment baseline.
|
||||
- Let a named agent add stricter requirements without weakening global rules.
|
||||
- Reuse existing policy section shapes where the evidence can be attributed to
|
||||
an agent.
|
||||
- Avoid making `agents.workspace` a second tool-permission system.
|
||||
- Leave global-only checks global until their evidence can be mapped to an
|
||||
agent.
|
||||
|
||||
## Shape
|
||||
|
||||
Use `scopes.<scopeName>` for purpose-named agent policy scopes. Each
|
||||
scope lists the runtime `agentIds` it applies to, then reuses the normal
|
||||
top-level policy section grammar where the section evidence can be attributed to
|
||||
those agents. The initial shipped scoped sections are `tools` and
|
||||
`agents.workspace`; sandbox and ingress stay out of this PR and can join the
|
||||
same container once those policy PRs land and their evidence carries agent
|
||||
identity. The scoped field inventory is backed by policy rule metadata that
|
||||
records each field's strictness semantics for later policy-file conformance.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"tools": {
|
||||
"denyTools": ["process"],
|
||||
},
|
||||
"agents": {
|
||||
"workspace": {
|
||||
"allowedAccess": ["none", "ro"],
|
||||
},
|
||||
},
|
||||
"scopes": {
|
||||
"release-agent-lockdown": {
|
||||
"agentIds": ["release-agent"],
|
||||
"agents": {
|
||||
"workspace": {
|
||||
"allowedAccess": ["none", "ro"],
|
||||
},
|
||||
},
|
||||
"tools": {
|
||||
"profiles": { "allow": ["minimal", "messaging"] },
|
||||
"fs": { "requireWorkspaceOnly": true },
|
||||
"exec": {
|
||||
"allowSecurity": ["deny", "allowlist"],
|
||||
"requireAsk": ["always"],
|
||||
"allowHosts": ["sandbox"],
|
||||
},
|
||||
"elevated": { "allow": false },
|
||||
"alsoAllow": { "expected": ["message", "read"] },
|
||||
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`agents.workspace` remains the existing all-agent workspace baseline.
|
||||
`scopes.<scopeName>` is a scoped overlay, not a replacement for global
|
||||
policy. The scope name is descriptive only; matching uses `agentIds`, not
|
||||
display names. It deliberately contains normal section names instead of a
|
||||
bespoke per-agent mini-grammar.
|
||||
Every scope present in `policy.jsonc` must be valid and enforceable. In this
|
||||
PR, the only supported selector is `agentIds`, and it supports only `tools.*`
|
||||
and `agents.workspace.*`.
|
||||
|
||||
## Layering semantics
|
||||
|
||||
Policy evaluation is additive:
|
||||
|
||||
1. Top-level policy applies to all matching evidence.
|
||||
2. Existing `agents.workspace` applies to defaults and every listed agent.
|
||||
3. `scopes.<scopeName>` applies to evidence for each normalized runtime
|
||||
id in `agentIds`.
|
||||
4. Multiple scope blocks may target the same agent when they govern
|
||||
different fields, or when a later value for the same field is equally or
|
||||
more restrictive according to policy metadata.
|
||||
5. A named-agent overlay can tighten policy, but it cannot make a global
|
||||
violation acceptable.
|
||||
|
||||
If both global and agent-scoped rules fail, findings should point at the rule
|
||||
that was violated:
|
||||
|
||||
```text
|
||||
oc://policy.jsonc/tools/denyTools
|
||||
oc://policy.jsonc/scopes/release-agent-lockdown/tools/denyTools
|
||||
oc://policy.jsonc/scopes/release-agent-lockdown/agents/workspace/allowedAccess
|
||||
```
|
||||
|
||||
That keeps broad tool posture, named-agent tool posture, and workspace posture
|
||||
auditable as separate requirements even when they observe the same config
|
||||
fields.
|
||||
|
||||
Exact-list claims such as `tools.alsoAllow.expected` compare the configured list
|
||||
to the expected list and report both missing expected entries and unexpected
|
||||
extra entries. This is intended for additive posture such as `alsoAllow`, where
|
||||
one extra entry can widen an agent beyond its reviewed role.
|
||||
|
||||
## Policy and config layering
|
||||
|
||||
The overlay model separates where policy is authored from where OpenClaw config
|
||||
is observed:
|
||||
|
||||
| Policy scope | Observed config | Applies to | Example result |
|
||||
| --------------------------------------- | ---------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| Top-level `tools.*` | Global `tools.*` and inherited agent tool posture | All agents using matching posture | Deny `gateway` exec host for every agent unless the global policy allows it. |
|
||||
| Top-level `tools.*` | `agents.list[].tools.*` overrides | Any agent with an override | Flag one agent that overrides `tools.exec.host` to an unapproved value. |
|
||||
| `scopes.<scopeName>.tools.*` | Matching `agents.list[]` entry and inherited posture | Only that named agent | Let most agents use `node` exec host while one agent must use only `sandbox`. |
|
||||
| `agents.workspace` | Defaults and every listed agent workspace posture | Defaults and all listed agents | Require every agent workspace access to be `none` or `ro`. |
|
||||
| `scopes.<scopeName>.agents.workspace.*` | Matching `agents.list[]` workspace posture | Only that named agent | Require one agent to be read-only without requiring the same for `main`. |
|
||||
|
||||
Per-agent overlays are additive. A named-agent rule can be stricter than the
|
||||
top-level rule, but it cannot make a global violation acceptable. For allow-list
|
||||
rules, the effective allowed set is the intersection of the global rule and the
|
||||
named-agent overlay when both are present.
|
||||
|
||||
For example, if top-level `tools.exec.allowHosts` permits `["sandbox", "node"]`
|
||||
and `scopes.release-agent-lockdown.tools.exec.allowHosts` permits only
|
||||
`["sandbox"]`, `release-agent` fails when its effective exec host is `node`;
|
||||
another agent can still pass
|
||||
with `node`.
|
||||
|
||||
## Tool posture versus workspace posture
|
||||
|
||||
Tool posture belongs under `tools` because it describes what tool behavior a
|
||||
configuration may expose. The existing `tools.*` policy observes both global
|
||||
`tools.*` config and per-agent `agents.list[].tools.*` overrides.
|
||||
|
||||
Workspace posture belongs under `workspace` because it describes sandbox mode
|
||||
and workspace access. The workspace section should not grow into a general tool
|
||||
policy namespace. If one agent needs stricter tool restrictions to make its
|
||||
workspace posture meaningful, put those restrictions in the same agent overlay
|
||||
under `scopes.<scopeName>.tools`.
|
||||
|
||||
For a restricted release agent, the intended split is:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"scopes": {
|
||||
"release-agent-lockdown": {
|
||||
"agentIds": ["release-agent"],
|
||||
"agents": {
|
||||
"workspace": { "allowedAccess": ["none", "ro"] },
|
||||
},
|
||||
"tools": {
|
||||
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Section eligibility
|
||||
|
||||
An agent-scoped section should be added only when policy evidence carries an
|
||||
agent id or can be attributed to one without guessing.
|
||||
|
||||
| Section | Initial agent-scoped status | Reason |
|
||||
| ----------- | --------------------------- | ------------------------------------------------------------------------ |
|
||||
| `workspace` | Include | Agent sandbox/workspace evidence already has agent identity. |
|
||||
| `tools` | Include | Tool posture evidence includes global and per-agent tool config. |
|
||||
| `sandbox` | Pipeline follow-up | Keep out until the sandbox posture PR lands and evidence can be scoped. |
|
||||
| `ingress` | Pipeline follow-up | Keep out until ingress/channel posture lands with agent attribution. |
|
||||
| `models` | Include when mapped | Selected model refs can be agent-specific. |
|
||||
| `mcp` | Include when mapped | Use only when MCP server evidence is attributable to an agent. |
|
||||
| `auth` | Defer | Auth profile metadata is a config catalog unless agent binding is clear. |
|
||||
| `channels` | Defer | Channel provider posture is deployment-level until routing is scoped. |
|
||||
| `gateway` | Keep global | Gateway exposure/auth/http posture is process-level. |
|
||||
| `network` | Keep global | Private-network SSRF posture is runtime-level. |
|
||||
| `secrets` | Keep global first | Secret provider posture is shared unless refs are agent-attributed. |
|
||||
|
||||
## Compatibility
|
||||
|
||||
The implementation is additive:
|
||||
|
||||
- keep all existing top-level policy fields valid;
|
||||
- keep `agents.workspace` semantics unchanged;
|
||||
- validate `scopes` before evaluating scoped rules;
|
||||
- reject unsupported scoped sections clearly until their evidence and policy
|
||||
contracts are implemented;
|
||||
- do not reinterpret top-level `tools.requireMetadata` as agent-scoped, because
|
||||
tool metadata describes the declared workspace tool catalog;
|
||||
- include agent-scoped evidence in the attestation hash when any scoped rule is
|
||||
present.
|
||||
|
||||
This lets broad tool posture remain a top-level policy contract while named
|
||||
agents add stricter observable claims without weakening the global baseline.
|
||||
@@ -18,6 +18,45 @@ Adds policy-backed doctor checks for workspace conformance.
|
||||
|
||||
plugin
|
||||
|
||||
## Behavior
|
||||
|
||||
The Policy plugin contributes doctor health checks for policy-managed OpenClaw
|
||||
settings and governed workspace declarations. Policy currently covers channel
|
||||
conformance, governed tool metadata, MCP server posture, model-provider posture,
|
||||
private-network access posture, Gateway exposure posture, agent workspace/tool
|
||||
posture, configured global/per-agent tool posture, configured sandbox runtime
|
||||
posture, and OpenClaw config secret provider/auth profile posture.
|
||||
|
||||
Policy stores authored requirements in `policy.jsonc`, observes existing
|
||||
OpenClaw settings and workspace declarations as evidence, and reports drift
|
||||
through `openclaw policy check` and `openclaw doctor --lint`. A clean policy
|
||||
check emits policy, evidence, findings, and attestation hashes that operators
|
||||
can record for audit.
|
||||
|
||||
Tool posture rules can require approved profiles, workspace-only filesystem
|
||||
tools, bounded exec security/ask/host settings, disabled elevated mode, exact
|
||||
`alsoAllow` entries, and required tool deny entries. The evidence records
|
||||
additive `alsoAllow` entries because they can widen effective tool posture.
|
||||
These checks observe config conformance only; they do not read runtime approval
|
||||
state or add runtime enforcement.
|
||||
|
||||
Sandbox posture rules can require approved sandbox modes/backends, deny host
|
||||
container networking, deny container namespace joins, require read-only container
|
||||
mounts, deny container runtime socket mounts and unconfined container profiles,
|
||||
and require sandbox browser CDP source ranges.
|
||||
These checks observe config conformance only; they do not read runtime approval
|
||||
state, inspect live containers, or add runtime enforcement.
|
||||
|
||||
Named agent policy scopes under `scopes.<scopeName>` can add stricter
|
||||
normal policy sections for the runtime agent ids listed in `agentIds`. The
|
||||
supported scoped sections are `tools`, `agents.workspace`, and `sandbox`.
|
||||
Every scope present in `policy.jsonc` must be valid and
|
||||
enforceable for its selector. Overlay rules are additional claims, so they do
|
||||
not weaken top-level policy and can produce their own findings when the same
|
||||
observed config violates both scopes. Runtime agent ids that are not explicitly
|
||||
listed in `agents.list[]` are checked against inherited global/default posture
|
||||
rather than silently passing with no evidence.
|
||||
|
||||
## Related docs
|
||||
|
||||
- [policy](/cli/policy)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,9 @@ import {
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
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";
|
||||
|
||||
export type PolicyAttestation = {
|
||||
readonly checkedAt: string;
|
||||
readonly policy?: {
|
||||
@@ -26,6 +29,7 @@ export type PolicyEvidence = {
|
||||
readonly channels: readonly PolicyChannelEvidence[];
|
||||
readonly tools?: readonly PolicyToolEvidence[];
|
||||
readonly toolPosture?: readonly PolicyToolPostureEvidence[];
|
||||
readonly sandboxPosture?: readonly PolicySandboxPostureEvidence[];
|
||||
readonly mcpServers: readonly PolicyMcpServerEvidence[];
|
||||
readonly modelProviders: readonly PolicyModelProviderEvidence[];
|
||||
readonly modelRefs: readonly PolicyModelRefEvidence[];
|
||||
@@ -82,6 +86,28 @@ export type PolicyToolPostureEvidence = {
|
||||
readonly explicit?: boolean;
|
||||
};
|
||||
|
||||
export type PolicySandboxPostureEvidence = {
|
||||
readonly id: string;
|
||||
readonly kind:
|
||||
| "backend"
|
||||
| "browserCdpSourceRange"
|
||||
| "containerMount"
|
||||
| "containerNetwork"
|
||||
| "containerSecurityProfile"
|
||||
| "mode";
|
||||
readonly source: string;
|
||||
readonly scope: "defaults" | "agent";
|
||||
readonly agentId?: string;
|
||||
readonly value?: boolean | string;
|
||||
readonly bind?: string;
|
||||
readonly bindMode?: string;
|
||||
readonly bindHost?: string;
|
||||
readonly bindSurface?: "browser" | "docker";
|
||||
readonly networkSurface?: "browser" | "docker";
|
||||
readonly profile?: "apparmor" | "seccomp";
|
||||
readonly explicit?: boolean;
|
||||
};
|
||||
|
||||
export type PolicyModelProviderEvidence = {
|
||||
readonly id: string;
|
||||
readonly source: string;
|
||||
@@ -227,6 +253,7 @@ export function collectPolicyEvidence(
|
||||
readonly includeGatewayExposure?: boolean;
|
||||
readonly includeAgentWorkspace?: boolean;
|
||||
readonly includeToolPosture?: boolean;
|
||||
readonly includeSandboxPosture?: boolean;
|
||||
readonly includeSecrets?: boolean;
|
||||
readonly includeAuthProfiles?: boolean;
|
||||
},
|
||||
@@ -238,6 +265,7 @@ export function collectPolicyEvidence(
|
||||
readonly includeGatewayExposure?: boolean;
|
||||
readonly includeAgentWorkspace?: boolean;
|
||||
readonly includeToolPosture?: boolean;
|
||||
readonly includeSandboxPosture?: boolean;
|
||||
readonly includeSecrets?: boolean;
|
||||
readonly includeAuthProfiles?: boolean;
|
||||
},
|
||||
@@ -249,6 +277,7 @@ export function collectPolicyEvidence(
|
||||
readonly includeGatewayExposure?: boolean;
|
||||
readonly includeAgentWorkspace?: boolean;
|
||||
readonly includeToolPosture?: boolean;
|
||||
readonly includeSandboxPosture?: boolean;
|
||||
readonly includeSecrets?: boolean;
|
||||
readonly includeAuthProfiles?: boolean;
|
||||
} = {},
|
||||
@@ -266,6 +295,9 @@ export function collectPolicyEvidence(
|
||||
? {}
|
||||
: { agentWorkspace: scanPolicyAgentWorkspace(cfg) }),
|
||||
...(options.includeToolPosture === false ? {} : { toolPosture: scanPolicyToolPosture(cfg) }),
|
||||
...(options.includeSandboxPosture === false
|
||||
? {}
|
||||
: { sandboxPosture: scanPolicySandboxPosture(cfg) }),
|
||||
...(options.includeSecrets === false ? {} : { secrets: scanPolicySecrets(cfg) }),
|
||||
...(options.includeAuthProfiles === false ? {} : { authProfiles: scanPolicyAuthProfiles(cfg) }),
|
||||
};
|
||||
@@ -560,6 +592,45 @@ export function scanPolicyAgentWorkspace(
|
||||
return entries.toSorted((a, b) => a.source.localeCompare(b.source) || a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
export function scanPolicySandboxPosture(
|
||||
cfg: Record<string, unknown>,
|
||||
): readonly PolicySandboxPostureEvidence[] {
|
||||
const agents = isRecord(cfg.agents) ? cfg.agents : {};
|
||||
const defaults = isRecord(agents.defaults) ? agents.defaults : {};
|
||||
const defaultSandbox = isRecord(defaults.sandbox) ? defaults.sandbox : {};
|
||||
const entries: PolicySandboxPostureEvidence[] = [];
|
||||
pushSandboxPostureEvidence(entries, {
|
||||
id: "agents-defaults",
|
||||
scope: "defaults",
|
||||
sandbox: defaultSandbox,
|
||||
inheritedSandbox: {},
|
||||
sourceBase: "oc://openclaw.config/agents/defaults/sandbox",
|
||||
inheritedSourceBase: "oc://openclaw.config/agents/defaults/sandbox",
|
||||
});
|
||||
|
||||
const list = Array.isArray(agents.list) ? agents.list : [];
|
||||
list.forEach((agent, index) => {
|
||||
if (!isRecord(agent)) {
|
||||
return;
|
||||
}
|
||||
const agentId =
|
||||
typeof agent.id === "string" && agent.id.trim() !== "" ? agent.id.trim() : undefined;
|
||||
const sandbox = isRecord(agent.sandbox) ? agent.sandbox : {};
|
||||
pushSandboxPostureEvidence(entries, {
|
||||
id: agentId ?? `agent-${index}`,
|
||||
scope: "agent",
|
||||
agentId,
|
||||
sandbox,
|
||||
inheritedSandbox: defaultSandbox,
|
||||
sharedSandboxScope: sandboxScopeIsShared(sandbox, defaultSandbox),
|
||||
sourceBase: `oc://openclaw.config/agents/list/#${index}/sandbox`,
|
||||
inheritedSourceBase: "oc://openclaw.config/agents/defaults/sandbox",
|
||||
});
|
||||
});
|
||||
|
||||
return entries.toSorted((a, b) => a.source.localeCompare(b.source) || a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
export function scanPolicyToolPosture(
|
||||
cfg: Record<string, unknown>,
|
||||
): readonly PolicyToolPostureEvidence[] {
|
||||
@@ -998,6 +1069,292 @@ function pushToolElevatedPosture(
|
||||
}
|
||||
}
|
||||
|
||||
type SandboxPostureParams = {
|
||||
readonly id: string;
|
||||
readonly scope: "defaults" | "agent";
|
||||
readonly agentId?: string;
|
||||
readonly effectiveBackend?: string;
|
||||
readonly sandbox: Record<string, unknown>;
|
||||
readonly inheritedSandbox: Record<string, unknown>;
|
||||
readonly sharedSandboxScope?: boolean;
|
||||
readonly sourceBase: string;
|
||||
readonly inheritedSourceBase: string;
|
||||
};
|
||||
|
||||
function pushSandboxPostureEvidence(
|
||||
entries: PolicySandboxPostureEvidence[],
|
||||
params: SandboxPostureParams,
|
||||
): void {
|
||||
const localMode = readString(params.sandbox.mode);
|
||||
const inheritedMode = readString(params.inheritedSandbox.mode);
|
||||
pushSandboxPostureValue(entries, params, {
|
||||
suffix: "mode",
|
||||
kind: "mode",
|
||||
value: localMode ?? inheritedMode ?? "off",
|
||||
explicit: localMode !== undefined || inheritedMode !== undefined,
|
||||
inherited: localMode === undefined && inheritedMode !== undefined,
|
||||
});
|
||||
|
||||
const localBackend = readString(params.sandbox.backend);
|
||||
const inheritedBackend = readString(params.inheritedSandbox.backend);
|
||||
const effectiveBackend = (localBackend ?? inheritedBackend ?? "docker").toLowerCase();
|
||||
const effectiveParams = { ...params, effectiveBackend };
|
||||
pushSandboxPostureValue(entries, params, {
|
||||
suffix: "backend",
|
||||
kind: "backend",
|
||||
value: effectiveBackend,
|
||||
explicit: localBackend !== undefined || inheritedBackend !== undefined,
|
||||
inherited: localBackend === undefined && inheritedBackend !== undefined,
|
||||
});
|
||||
|
||||
if (effectiveBackend === "docker") {
|
||||
pushSandboxDockerPosture(entries, effectiveParams);
|
||||
}
|
||||
pushSandboxBrowserPosture(entries, effectiveParams);
|
||||
}
|
||||
|
||||
function pushSandboxDockerPosture(
|
||||
entries: PolicySandboxPostureEvidence[],
|
||||
params: SandboxPostureParams,
|
||||
): void {
|
||||
const localDocker =
|
||||
!params.sharedSandboxScope && isRecord(params.sandbox.docker) ? params.sandbox.docker : {};
|
||||
const inheritedDocker = isRecord(params.inheritedSandbox.docker)
|
||||
? params.inheritedSandbox.docker
|
||||
: {};
|
||||
const localNetwork = readString(localDocker.network);
|
||||
const inheritedNetwork = readString(inheritedDocker.network);
|
||||
pushSandboxPostureValue(entries, params, {
|
||||
suffix: "docker/network",
|
||||
kind: "containerNetwork",
|
||||
value: localNetwork ?? inheritedNetwork ?? "none",
|
||||
networkSurface: "docker",
|
||||
explicit: localNetwork !== undefined || inheritedNetwork !== undefined,
|
||||
inherited: localNetwork === undefined && inheritedNetwork !== undefined,
|
||||
});
|
||||
|
||||
pushSandboxDockerProfilePosture(entries, params, localDocker, inheritedDocker, "seccomp");
|
||||
pushSandboxDockerProfilePosture(entries, params, localDocker, inheritedDocker, "apparmor");
|
||||
|
||||
pushSandboxBindPosture(entries, params, {
|
||||
inheritedBinds: readStringArray(inheritedDocker.binds),
|
||||
localBinds: readStringArray(localDocker.binds),
|
||||
sourceSuffix: "docker/binds",
|
||||
surface: "docker",
|
||||
});
|
||||
}
|
||||
|
||||
function pushSandboxBindPosture(
|
||||
entries: PolicySandboxPostureEvidence[],
|
||||
params: SandboxPostureParams,
|
||||
bindParams: {
|
||||
readonly inheritedBinds: readonly string[];
|
||||
readonly localBinds: readonly string[];
|
||||
readonly sourceSuffix: string;
|
||||
readonly surface: "browser" | "docker";
|
||||
},
|
||||
): void {
|
||||
const { inheritedBinds, localBinds } = bindParams;
|
||||
for (const [index, bind] of [...inheritedBinds, ...localBinds].entries()) {
|
||||
const inherited = index < inheritedBinds.length;
|
||||
const parsed = splitPolicyBindSpec(bind);
|
||||
entries.push({
|
||||
id: `${params.id}-${bindParams.surface}-bind-${index}`,
|
||||
kind: "containerMount",
|
||||
source: `${inherited ? params.inheritedSourceBase : params.sourceBase}/${bindParams.sourceSuffix}/#${
|
||||
inherited ? index : index - inheritedBinds.length
|
||||
}`,
|
||||
scope: params.scope,
|
||||
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
|
||||
bind,
|
||||
bindHost: parsed?.host,
|
||||
bindMode: parsed?.mode ?? "rw",
|
||||
bindSurface: bindParams.surface,
|
||||
explicit: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pushSandboxDockerProfilePosture(
|
||||
entries: PolicySandboxPostureEvidence[],
|
||||
params: SandboxPostureParams,
|
||||
localDocker: Record<string, unknown>,
|
||||
inheritedDocker: Record<string, unknown>,
|
||||
profile: "apparmor" | "seccomp",
|
||||
): void {
|
||||
const key = profile === "apparmor" ? "apparmorProfile" : "seccompProfile";
|
||||
const localValue = readString(localDocker[key]);
|
||||
const inheritedValue = readString(inheritedDocker[key]);
|
||||
const inherited = localValue === undefined && inheritedValue !== undefined;
|
||||
const value = localValue ?? inheritedValue;
|
||||
entries.push({
|
||||
id: `${params.id}-docker-${profile}-profile`,
|
||||
kind: "containerSecurityProfile",
|
||||
source: `${inherited ? params.inheritedSourceBase : params.sourceBase}/docker/${key}`,
|
||||
scope: params.scope,
|
||||
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
|
||||
profile,
|
||||
...(value === undefined ? {} : { value }),
|
||||
explicit: value !== undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function pushSandboxBrowserPosture(
|
||||
entries: PolicySandboxPostureEvidence[],
|
||||
params: SandboxPostureParams,
|
||||
): void {
|
||||
const localBrowser =
|
||||
!params.sharedSandboxScope && isRecord(params.sandbox.browser) ? params.sandbox.browser : {};
|
||||
const inheritedBrowser = isRecord(params.inheritedSandbox.browser)
|
||||
? params.inheritedSandbox.browser
|
||||
: {};
|
||||
const localEnabled = readBoolean(localBrowser.enabled);
|
||||
const inheritedEnabled = readBoolean(inheritedBrowser.enabled);
|
||||
const enabled = localEnabled ?? inheritedEnabled ?? false;
|
||||
if (!enabled) {
|
||||
const disabledInherited = localEnabled === undefined && inheritedEnabled !== undefined;
|
||||
if (localEnabled !== undefined || inheritedEnabled !== undefined) {
|
||||
entries.push({
|
||||
id: `${params.id}-browser-cdp-source-range`,
|
||||
kind: "browserCdpSourceRange",
|
||||
source: `${disabledInherited ? params.inheritedSourceBase : params.sourceBase}/browser/enabled`,
|
||||
scope: params.scope,
|
||||
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
|
||||
value: false,
|
||||
explicit: true,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const hasLocalRange = Object.hasOwn(localBrowser, "cdpSourceRange");
|
||||
const localRange = readString(localBrowser.cdpSourceRange);
|
||||
const inheritedRange = readString(inheritedBrowser.cdpSourceRange);
|
||||
const inherited = !hasLocalRange && inheritedRange !== undefined;
|
||||
const value = hasLocalRange ? localRange : inheritedRange;
|
||||
entries.push({
|
||||
id: `${params.id}-browser-cdp-source-range`,
|
||||
kind: "browserCdpSourceRange",
|
||||
source: `${inherited ? params.inheritedSourceBase : params.sourceBase}/browser/cdpSourceRange`,
|
||||
scope: params.scope,
|
||||
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
|
||||
...(value === undefined ? {} : { value }),
|
||||
explicit: value !== undefined,
|
||||
});
|
||||
|
||||
const localNetwork = readString(localBrowser.network);
|
||||
const inheritedNetwork = readString(inheritedBrowser.network);
|
||||
pushSandboxPostureValue(entries, params, {
|
||||
suffix: "browser/network",
|
||||
kind: "containerNetwork",
|
||||
value: localNetwork ?? inheritedNetwork ?? DEFAULT_POLICY_SANDBOX_BROWSER_NETWORK,
|
||||
networkSurface: "browser",
|
||||
explicit: localNetwork !== undefined || inheritedNetwork !== undefined,
|
||||
inherited: localNetwork === undefined && inheritedNetwork !== undefined,
|
||||
});
|
||||
|
||||
const browserBindsConfigured =
|
||||
inheritedBrowser.binds !== undefined || localBrowser.binds !== undefined;
|
||||
if (browserBindsConfigured) {
|
||||
pushSandboxBindPosture(entries, params, {
|
||||
inheritedBinds: readStringArray(inheritedBrowser.binds),
|
||||
localBinds: readStringArray(localBrowser.binds),
|
||||
sourceSuffix: "browser/binds",
|
||||
surface: "browser",
|
||||
});
|
||||
} else if (params.effectiveBackend !== "docker") {
|
||||
const localDocker =
|
||||
!params.sharedSandboxScope && isRecord(params.sandbox.docker) ? params.sandbox.docker : {};
|
||||
const inheritedDocker = isRecord(params.inheritedSandbox.docker)
|
||||
? params.inheritedSandbox.docker
|
||||
: {};
|
||||
pushSandboxBindPosture(entries, params, {
|
||||
inheritedBinds: readStringArray(inheritedDocker.binds),
|
||||
localBinds: readStringArray(localDocker.binds),
|
||||
sourceSuffix: "docker/binds",
|
||||
surface: "browser",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function sandboxScopeIsShared(
|
||||
sandbox: Record<string, unknown>,
|
||||
inheritedSandbox: Record<string, unknown>,
|
||||
): boolean {
|
||||
const localScope = readString(sandbox.scope);
|
||||
const inheritedScope = readString(inheritedSandbox.scope);
|
||||
const configuredScope = localScope ?? inheritedScope;
|
||||
if (configuredScope !== undefined) {
|
||||
return configuredScope === "shared";
|
||||
}
|
||||
const localPerSession = readBoolean(sandbox.perSession);
|
||||
const inheritedPerSession = readBoolean(inheritedSandbox.perSession);
|
||||
return (localPerSession ?? inheritedPerSession) === false;
|
||||
}
|
||||
|
||||
function pushSandboxPostureValue(
|
||||
entries: PolicySandboxPostureEvidence[],
|
||||
params: SandboxPostureParams,
|
||||
entry: {
|
||||
readonly suffix: string;
|
||||
readonly kind: PolicySandboxPostureEvidence["kind"];
|
||||
readonly value: string | undefined;
|
||||
readonly networkSurface?: "browser" | "docker";
|
||||
readonly explicit: boolean;
|
||||
readonly inherited: boolean;
|
||||
},
|
||||
): void {
|
||||
entries.push({
|
||||
id: `${params.id}-${entry.suffix.replaceAll("/", "-")}`,
|
||||
kind: entry.kind,
|
||||
source: `${entry.inherited ? params.inheritedSourceBase : params.sourceBase}/${entry.suffix}`,
|
||||
scope: params.scope,
|
||||
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
|
||||
...(entry.value === undefined ? {} : { value: entry.value }),
|
||||
...(entry.networkSurface === undefined ? {} : { networkSurface: entry.networkSurface }),
|
||||
explicit: entry.explicit,
|
||||
});
|
||||
}
|
||||
|
||||
function splitPolicyBindSpec(
|
||||
value: string,
|
||||
): { readonly host: string; readonly mode: string } | undefined {
|
||||
const separator = policyBindSeparatorIndex(value);
|
||||
if (separator < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const host = value.slice(0, separator);
|
||||
const rest = value.slice(separator + 1);
|
||||
const optionsStart = policyBindOptionsSeparatorIndex(rest);
|
||||
const options = optionsStart < 0 ? "" : rest.slice(optionsStart + 1);
|
||||
const mode = options
|
||||
.split(",")
|
||||
.map((entry) => entry.trim().toLowerCase())
|
||||
.includes("ro")
|
||||
? "ro"
|
||||
: "rw";
|
||||
return { host, mode };
|
||||
}
|
||||
|
||||
function policyBindSeparatorIndex(value: string): number {
|
||||
const hasDriveLetterPrefix = /^[A-Za-z]:[\\/]/.test(value);
|
||||
for (let index = hasDriveLetterPrefix ? 2 : 0; index < value.length; index += 1) {
|
||||
if (value[index] === ":") {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function policyBindOptionsSeparatorIndex(value: string): number {
|
||||
const hasDriveLetterPrefix = /^[A-Za-z]:[\\/]/.test(value);
|
||||
for (let index = hasDriveLetterPrefix ? 2 : 0; index < value.length; index += 1) {
|
||||
if (value[index] === ":") {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
type ToolPostureParams = {
|
||||
readonly id: string;
|
||||
readonly scope: "global" | "agent";
|
||||
|
||||
Reference in New Issue
Block a user