> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/openshell/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/openshell/_mcp/server.

# Use Policy Advisor

> Let sandboxed agents propose narrow policy changes through policy.local while keeping developer approval in the loop.

Policy advisor lets a running sandboxed agent ask for a narrow network policy change after OpenShell denies a request. The agent submits a draft through `policy.local`, a developer approves or rejects it from outside the sandbox, and approved network policy hot-reloads into the same sandbox.

Policy advisor preserves OpenShell's default-deny posture. The structured rule is the approval contract, and the agent's rationale is supporting context. By default every accepted proposal lands in the draft inbox for human review. Opt-in [auto mode](#approval-modes) approves a proposal without a reviewer only when its [prover delta](#what-auto-approval-checks) is empty and the current draft rule produces no security notes. A prover finding or security note keeps the proposal pending for human review.

## Enable Policy Advisor

Policy advisor is disabled by default. Enable it globally when you want every sandbox on the selected gateway to expose the agent proposal surface:

```shell
openshell settings set --global \
  --key agent_policy_proposals_enabled \
  --value true \
  --yes
```

You can also enable it for one sandbox, unless the key is managed globally:

```shell
openshell settings set <sandbox-name> \
  --key agent_policy_proposals_enabled \
  --value true
```

Check the effective setting for a sandbox:

```shell
openshell settings get <sandbox-name>
```

The output shows whether `agent_policy_proposals_enabled` is `global`, `sandbox`, or `unset`. A global value overrides sandbox-scoped values. To return control to sandbox-scoped settings, delete the global key:

```shell
openshell settings delete --global \
  --key agent_policy_proposals_enabled \
  --yes
```

Set the value before creating a sandbox when you want the first denied request to include policy advisor guidance. Running sandboxes poll settings and can enable the surface after startup, but startup enablement gives the agent the clearest first-denial path.

## Approval Modes

Every proposal, mechanistic or agent-authored, is routed through the [policy prover](#what-auto-approval-checks). The gateway also recalculates security notes from the current draft rule before auto-approval. The `proposal_approval_mode` setting decides whether proposals that pass both checks require human review.

| Mode                                     | When unset / `manual`                      | `auto`                                                                              |
| ---------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------- |
| Empty prover delta and no security notes | Lands in the draft inbox for human review. | Approved automatically. The sandbox hot-reloads the new rule and the agent retries. |
| Any prover finding or security note      | Lands in the draft inbox.                  | Remains pending for human review.                                                   |

`manual` is the default. Auto mode is an explicit opt-in; OpenShell's default-deny posture is preserved unless you choose otherwise.

Enable auto mode at gateway scope when you want every sandbox on this gateway to auto-approve eligible proposals:

```shell
openshell settings set --global \
  --key proposal_approval_mode \
  --value auto \
  --yes
```

Enable it for one sandbox when no global value is set:

```shell
openshell settings set <sandbox-name> \
  --key proposal_approval_mode \
  --value auto
```

The shorthand at create time writes the sandbox-scoped setting for you:

```shell
openshell sandbox create --approval-mode auto <name>
```

Only `manual` and `auto` are accepted; typos like `autom` are rejected at configure time. Stale or unknown values found in storage are still treated as `manual` at runtime as a defense-in-depth measure.

**Precedence.** Gateway scope wins over sandbox scope. A reviewer can pin `manual` for a fleet by setting it globally; per-sandbox overrides only apply when no global value is set.

**Audit trail.** Every auto-approval emits a `CONFIG:APPROVED` event with `auto=true`, `source=<mechanistic|agent_authored>`, `prover_delta=empty`, and `resolved_from=<gateway|sandbox|default>` so operators can reconstruct why a given approval ran without human review.

## How It Works

When policy advisor is enabled, the sandbox supervisor turns on three agent-facing surfaces:

* It installs `/etc/openshell/skills/policy_advisor.md` inside the sandbox.
* It also installs `/etc/openshell/skills/policy-advisor/SKILL.md` as a short Codex/generic-agent pointer, and writes a root `/AGENTS.md` pointer only when the image does not already provide one.
* It serves `http://policy.local` from inside the sandbox.
* It adds `agent_guidance` and `next_steps` to L7 `policy_denied` response bodies so the agent can find the skill and local API.

The loop has seven steps:

1. A sandboxed process attempts a network request that policy denies.
2. For inspected REST traffic, OpenShell returns a structured `403` body with fields such as `layer`, `host`, `port`, `binary`, `method`, `path`, `rule_missing`, `agent_guidance`, and `next_steps`.
3. The agent reads the policy advisor skill, inspects the current policy, and optionally reads recent denial log lines.
4. The agent submits one or more `addRule` proposals to `http://policy.local/v1/proposals`.
5. The gateway turns the proposal into the exact effective-policy candidate it would apply. It preserves any existing L7 or provider-owned endpoint contract, adds the proposed binary as a sandbox overlay, validates the full merge, and runs the [policy prover](#what-auto-approval-checks) against that candidate.
6. The gateway stores the candidate, its prover result, any application error, and a review token tied to the live policy, provider rules, and credential metadata. Provider rules remain immutable inputs.
7. Before approval, the gateway cheaply recomputes the candidate token from live inputs. An unchanged token reuses the stored prover result. A changed token leaves the proposal pending with a refreshed candidate and requires a fresh review. Under `auto` mode, an unchanged candidate is approved only when the prover delta and security notes are empty. Under `manual` mode, every valid proposal lands in the draft inbox.
8. The agent waits on `/v1/proposals/{chunk_id}/wait` until a decision is available. Approved proposals hot-reload into the sandbox; rejected proposals return `rejection_reason` and `validation_result` so the agent can revise.

```mermaid
flowchart TD
  A["Denied request creates a narrow proposal"] --> B["Gateway builds the exact effective-policy candidate"]
  B --> C["Validate merge, L7 contract, providers, and credentials"]
  C -->|Invalid| D["Keep pending and show application error"]
  C -->|Valid| E["Run prover once and store candidate plus review token"]
  E --> F["Reviewer approves using that token"]
  F --> G["Recompute token from live inputs"]
  G -->|Unchanged| H["Reuse stored prover result and apply candidate"]
  G -->|Changed| I["Refresh candidate and token; require fresh review"]
  I --> F
```

When a proposal is approved, `/wait` reports `policy_reloaded: true` only after the local sandbox policy covers the approved rule. At that point the agent can retry the original denied action once. If a proposal is rejected, `/wait` returns `rejection_reason` and `validation_result` so the agent can revise or stop. `validation_result` carries the categorical prover findings — `link_local_reach`, `l7_bypass_credentialed`, `credential_reach_expansion`, `capability_expansion` — so the agent can narrow the next attempt to the specific concern the prover flagged.

## What Gets Proposed

OpenShell has two proposal paths:

| Path                    | Source                                        | Typical rule shape                                                                                                                                                                    |
| ----------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Mechanistic mapper      | Aggregated denial summaries from the sandbox. | Groups by host, port, and binary. If L7 request samples are available, it can draft REST method and path rules. Otherwise it drafts an L4 endpoint.                                   |
| Agent-authored proposal | The in-sandbox agent, using `policy.local`.   | Usually a REST `addRule` with exact host, port, binary, method, and path from the structured denial. It can also omit `protocol` for endpoint-only access through the explicit proxy. |

### How proposal provenance works

OpenShell tracks whether an endpoint and binary came from policy advisor. This is internal provenance; it is not a policy YAML field that authors set. Think of each marker as answering “who introduced this identity?” rather than “what traffic does this allow?”

| Marker              | `false`                                                 | `true`                                   | When both declarations meet                                                                     |
| ------------------- | ------------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Endpoint provenance | A user or provider explicitly declared the endpoint.    | Policy advisor proposed the endpoint.    | The values do not conflict by themselves. The explicit declaration wins if the endpoints merge. |
| Binary provenance   | A user or provider explicitly declared the binary path. | Policy advisor proposed the binary path. | The explicit declaration wins if the same path merges.                                          |

For example, a GitHub provider can explicitly declare `api.github.com:443` for read operations. An agent can then propose `PUT /repos/NVIDIA/OpenShell/contents/docs/**` for the same endpoint. The provider endpoint has provenance `false`; the proposal endpoint has provenance `true`. OpenShell allows that overlap, keeps the provider rule immutable, and stores an approved write rule in the sandbox policy layer.

Provenance does not hide a real endpoint conflict. The same two declarations still fail validation if they disagree on connection or request-processing behavior that must have one value, such as TLS mode, `allowed_ips`, an equally specific L7 protocol or parser contract, credential binding, or enforcement mode. Authorization fields such as compatible allow and deny rules can combine.

Exact-host SSRF trust requires an exact endpoint and the matching binary identity to be explicit in the same rule. An advisor-only endpoint or binary does not create that stronger trust. For example:

* A provider rule that explicitly declares both `/usr/bin/gh` and `api.github.com:443` already establishes exact-host trust for that pair. A later advisor proposal does not create or broaden that trust.
* If the provider declares `api.github.com:443` for `/usr/bin/gh`, but the advisor proposes the endpoint for `/usr/bin/curl`, `curl` does not inherit the provider's binary identity. Its proposed rule remains subject to the normal SSRF checks.
* If an advisor proposes `internal-api.example:443` and it resolves to a private address, approval alone is not enough. A developer must explicitly authorize the intended address range with `allowed_ips`.

For REST APIs, prefer L7 rules over broad L4 access. A good proposal allows one method and the smallest safe path:

```json
{
  "intent_summary": "Allow gh to update documentation in NVIDIA/OpenShell.",
  "operations": [
    {
      "addRule": {
        "ruleName": "github_contents_docs_write",
        "rule": {
          "name": "github_contents_docs_write",
          "endpoints": [
            {
              "host": "api.github.com",
              "port": 443,
              "protocol": "rest",
              "enforcement": "enforce",
              "rules": [
                {
                  "allow": {
                    "method": "PUT",
                    "path": "/repos/NVIDIA/OpenShell/contents/docs/**"
                  }
                }
              ]
            }
          ],
          "binaries": [
            {
              "path": "/usr/bin/gh"
            }
          ]
        }
      }
    }
  ]
}
```

The current `policy.local` JSON shape covers explicit-proxy endpoints and REST method or path rules. Agent-authored proposals cannot set `protocol: tcp` or `tls: skip`, because those modes bypass application-authority inspection. When a task requires native TCP or a raw TLS tunnel, a developer must add the rule through the normal policy-authoring workflow. Omitting `protocol` remains supported and retains the explicit proxy's default TLS termination and HTTP authority checks. Use [Customize Sandbox Policies](/sandboxes/policies) or [Policy Schema Reference](/reference/policy-schema) for policy fields that are not part of the agent-authored proposal surface, such as WebSocket credential rewrite, GraphQL operation matching, endpoint path scoping, and provider-owned policy bundles.

Policy advisor proposals do not add `allowed_ips` automatically. If an advisor-proposed hostname resolves to an internal or private address, OpenShell's SSRF protections still block the connection until a developer explicitly adds the required `allowed_ips` entry.

Private RFC 1918, CGNAT, IPv6 ULA, and other special-use destinations classified as internal produce advisory security notes when they appear as literal endpoint IPs or in `allowed_ips`. CIDR intersections are included, and hostless `allowed_ips` rules receive an additional warning because they can match any hostname resolving into the configured range.

Always-blocked destinations are not advisory. Loopback, link-local, and unspecified IPs or CIDRs, plus `localhost` and known metadata endpoint hostnames, are excluded from security notes. Submit and edit can store such a draft, but approval fails when merge validation prevents it from entering the active policy. Runtime SSRF protections continue to enforce the same boundary.

## What Auto-Approval Checks

Auto-approval requires all three conditions: the effective mode is `auto`, the prover delta is empty, and recalculating security notes from the current stored rule produces none.

The policy prover runs against mechanistic and agent-authored proposals alike and asks four formal questions about the proposed change. Each "yes" is one categorical finding. Any finding blocks auto-approval. An empty delta is necessary but not sufficient.

| Category                     | Triggered when                                                                                                                                     |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `link_local_reach`           | A rule reaches `169.254.0.0/16`, `fe80::/10`, or a known metadata hostname.                                                                        |
| `l7_bypass_credentialed`     | A binary using a wire protocol the L7 proxy cannot inspect (`git-remote-https`, `ssh`, `nc`) gains reach to a host where a credential is in scope. |
| `credential_reach_expansion` | A binary gains credentialed reach to a `(host, port)` it could not reach before.                                                                   |
| `capability_expansion`       | On a `(binary, host, port)` that already had credentialed reach, the proposal adds a new HTTP method. The finding cites the specific method.       |

Findings are categorical. There is no severity tier. The reviewer reads the category and the structured evidence to decide.

Before approval, the gateway rebuilds the candidate token from the live base policy, immutable provider rules, and non-secret credential metadata. When that token is unchanged, it reuses the persisted prover result instead of rerunning the prover. When it changes, the gateway evaluates and persists the refreshed candidate, leaves the chunk pending, and requires the reviewer to inspect and approve the new token. Edits and deduplicated resubmissions follow the same path. Merge, policy-shape, provider-composition, credential, or prover failures are shown as application errors and cannot be approved. Security notes flag concerns such as internal or private destinations and `allowed_ips`, wildcard hosts, hostless `allowed_ips`, ephemeral ports, and well-known database or service ports. Any prover finding or security note keeps the chunk pending in auto mode.

The full reasoning model lives in [`crates/openshell-prover/README.md`](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-prover/README.md). Provider profiles composed in via [Profiles](/providers/profiles) are part of the effective policy the prover reasons over.

## Review Proposals

Review pending chunks from the host:

```shell
openshell rule get <sandbox-name> --status pending
```

Under `auto` mode, proposals with a prover finding or any recalculated security note remain pending for human review. Proposals that pass both checks are visible under `--status approved` with the auto-approval audit fields described in [Approval Modes](#approval-modes). Under `manual` mode, every accepted proposal shows up as pending regardless of the prover verdict or security notes.

The output shows the chunk ID, status, rationale, binary, endpoint summary, prover result, application error (if any), and candidate hash. For L7 proposals, the endpoint summary includes the protocol, method, and path:

```text
Endpoints: api.github.com:443 [L7 rest, allow PUT /repos/NVIDIA/OpenShell/contents/docs/**]
```

Approve only when the structured rule matches the access you intend to grant:

```shell
openshell rule approve <sandbox-name> --chunk-id <chunk-id>
```

The CLI fetches the current review token and submits it with the approval. If policy, provider, or credential inputs changed after the proposal was displayed, the gateway leaves it pending and asks you to review the refreshed candidate. Run `rule get` again before retrying. Bulk approval binds each selected chunk to its own review token in the same way.

Reject with guidance when the rule is too broad or points at the wrong target:

```shell
openshell rule reject <sandbox-name> \
  --chunk-id <chunk-id> \
  --reason "Scope this to docs/ paths only."
```

The rejection reason is returned to the agent through `policy.local`. The agent can use it to draft a narrower proposal.

Reviewers see the same guidance in the terminal UI. Run `openshell term`, open the sandbox's draft inbox, and select a rejected chunk to open its detail popup. The stored reason appears on a `Guidance:` line, and the list row shows a shortened copy of it. Rejection reasons have no length limit, so long guidance wraps and the popup body scrolls with `j`/`k`, `PageUp`/`PageDown`, and `g`/`G`; the approve and close controls stay pinned below the body, and the bottom border shows the scroll position.

## Agent API

`policy.local` is available only inside the sandbox and uses plain HTTP:

| Endpoint                                        | Purpose                                                                                                                          |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/policy/current`                        | Returns the current effective sandbox policy as YAML.                                                                            |
| `GET /v1/denials?last=10`                       | Returns recent denied OCSF shorthand log lines, newest first. Query strings are redacted before lines are returned to the agent. |
| `POST /v1/proposals`                            | Submits `addRule` operations. The response includes `accepted_chunk_ids` and `rejection_reasons`.                                |
| `GET /v1/proposals/{chunk_id}`                  | Returns one proposal's current `pending`, `approved`, or `rejected` status.                                                      |
| `GET /v1/proposals/{chunk_id}/wait?timeout=300` | Holds one HTTP request open until the proposal is approved, rejected, or the timeout expires.                                    |

If policy advisor is disabled, every route returns `404 feature_disabled`, the skill is not installed for new sandboxes, and L7 deny bodies do not advertise `policy.local` routes or include `agent_guidance`.

## What to Expect

Approved network rules hot-reload without restarting the sandbox. HTTP L7 keep-alive connections are closed at the reload boundary so the next parsed request uses the new policy. Raw streams remain connection-scoped, as described in [Customize Sandbox Policies](/sandboxes/policies#policy-structure).

Policy advisor emits audit events into the sandbox log. Use these lines to trace the full loop:

```shell
openshell logs <sandbox-name> --since 10m
```

Look for `HTTP:* DENIED`, `CONFIG:PROPOSED`, `CONFIG:APPROVED` or `CONFIG:REJECTED`, `CONFIG:LOADED`, and the final allowed request if the agent retries successfully. Auto-approved chunks emit `CONFIG:APPROVED` with `auto=true`, `source=<mechanistic|agent_authored>`, `prover_delta=empty`, and `resolved_from=<gateway|sandbox|default>`.

## Next Steps

* Use [Customize Sandbox Policies](/sandboxes/policies) for manual policy updates and L7 rule syntax.
* Use [Policy Schema Reference](/reference/policy-schema) for full YAML field details.
* Use [Logging](/observability/logging) to interpret OCSF shorthand log entries.