Skip to content

feat: LLM-powered PolicyAdvisor agent harness for intelligent policy recommendations #205

Description

@johntmyers

Summary

Replace the mechanistic (deterministic) chunk generator with an LLM-powered PolicyAdvisor agent harness that produces intelligent, context-aware policy recommendations. The harness runs sandbox-side via inference.local, embeds extracted knowledge from the generate-sandbox-policy skill, validates proposals against the local OPA engine, and manages context windows across incremental analysis runs.

This builds on the plumbing from #204 (denial aggregation, transport, persistence, approval workflow, CLI/TUI). The only sandbox-side change is swapping the chunk generation strategy — all transport, persistence, and UX infrastructure is already in place.

Depends on:#204
Parent issue:#153

Architecture

The PolicyAdvisor is a lightweight agent harness — not a general-purpose tool-calling agent, but a fixed 1-2 LLM call pattern with local OPA validation as a tool:

DenialAggregator emits summaries
│
v
┌────────────────────────────────────────┐
│ PolicyAdvisor (sandbox-side) │
│ │
│ 1. Build context window: │
│ - Skill context (~4-5k tokens) │
│ - Current active policy │
│ - New denial summaries + L7 data │
│ - Rolling summary of prior runs │
│ - Rejected chunks + reasons │
│ │
│ 2. LLM call via inference.local │
│ → structured JSON: chunks[] │
│ │
│ 3. Local OPA validation per chunk │
│ - Complete-rule conflicts │
│ - L7 config consistency │
│ - Schema validation │
│ │
│ 4. Fix call (if validation failures) │
│ → include errors, re-propose │
│ → drop if still invalid │
│ │
│ 5. Chunk reconciliation │
│ → update/merge/supersede │
│ │
│ 6. SubmitPolicyAnalysis RPC │
│ (analysis_mode: "llm") │
└────────────────────────────────────────┘

Why sandbox-side LLM

  1. Zero LLM configuration. Uses existing cluster inference (openshell cluster inference set). Every sandbox already has inference.local access via the proxy fast path (proxy.rs:217, bypasses OPA). No new env vars, API keys, or network policy entries.
  2. Distributed scaling. Each sandbox makes its own LLM calls. N sandboxes = N independent analysis pipelines. No gateway bottleneck.
  3. Pre-validated proposals. The sandbox has the OPA engine — it validates proposed rules locally before submission. The gateway doesn't run OPA. Only the sandbox can catch complete-rule conflicts, L7 config inconsistencies, and schema errors before they reach the user.
  4. Thin gateway. Gateway remains purely persistence + validation + approval. No LLM client, no context window management.

Scope

Skill context extraction

Extract and embed policy generation knowledge from .agents/skills/generate-sandbox-policy/:

  • SKILL.md (546 lines): validation rules (hard errors + warnings), access preset definitions (read-only/read-write/full), L4 vs L7 decision tree, glob pattern translation, private IP/SSRF rules
  • examples.md (884 lines): 20+ reference policy examples

Compile into a skill_context blob (~4000-5000 tokens) that serves as the system prompt foundation. This is bundled at build time, not fetched at runtime.

LLM call via inference.local

The PolicyAdvisor calls https://inference.local/v1/chat/completions (OpenAI-compatible):

  • The proxy intercepts inference.local:443 at the pre-OPA fast path (proxy.rs:217), TLS-terminates, and routes to the configured provider
  • api_key sent by client is stripped; the route's real API key is injected
  • model field is rewritten to the cluster model
  • Uses response_format: { type: "json_object" } for structured output
  • Timeout: 30s per call, 2 calls max per analysis cycle

Detect inference availability: check if cluster inference is configured (via build_inference_context() in lib.rs:543). If not configured, fall back to the mechanistic mapper (already implemented in parent issue).

OPA local validation loop

After parsing LLM response:

  1. Convert each proposed NetworkPolicyRule to OPA data format
  2. Query local OPA engine for validation:
    • Complete-rule conflicts (rule name already exists with different endpoints)
    • L7 config consistency (protocol: rest requires tls + enforcement fields)
    • Schema validation (valid host patterns, port ranges, binary paths)
    • Breadth warnings from skill rules (overly permissive wildcards)
  3. If validation passes → add to validated set
  4. If validation fails → collect errors for fix attempt (LLM call 2)
  5. After fix call → re-validate, drop chunks that still fail

Context window management

Three-tier context budget (default 8000 tokens total):

TierBudgetContent
Tier 1: System prompt~500 tokensSkill context (compressed), output format spec
Tier 2: Current policy~1000 tokensActive network_policies YAML, already-approved chunks
Tier 3: Denial analysis~5500 tokensNew summaries (sorted by count), rolling summary of older patterns, existing draft chunks, rejected chunks with reasons

Incremental analysis: Each run produces chunks[] + rolling_summary. The rolling summary is a compressed representation of all historical denial patterns (LLM-generated), capped at ~2000 tokens. It grows slowly as the LLM summarizes new information into the existing summary.

Overflow: When denial summaries exceed Tier 3 budget, sort by count (highest frequency first), include top N that fit, append note about omitted patterns.

Adaptive trigger intervals

  • Cold-start: trigger after 10s if any summaries pending (fast first recommendation)
  • Steady-state: trigger after 60s or when 5+ new summaries accumulate
  • Back-off: double interval after each no-op cycle (no new summaries), cap at 5m
  • Reset: interval resets to cold-start value when new denial pattern detected

Progressive L7 intelligence (enhances mechanistic Stage 1/Stage 2)

The LLM-powered PolicyAdvisor produces smarter recommendations at both stages:

Stage 1 (LLM): Instead of generic "L7 audit mode", the LLM:

  • Groups related denials (e.g., pypi.org + files.pythonhosted.org = "Python package installation")
  • Provides meaningful rationale and security analysis
  • Suggests appropriate initial access level based on known service patterns
  • Flags security concerns (security_notes field)

Stage 2 (LLM): Instead of simple method counting, the LLM:

  • Analyzes observed (method, path) patterns for semantic meaning
  • Identifies path patterns (e.g., /repos/*/issues → issue management)
  • Recommends nuanced access levels with explanation
  • Flags unexpected methods (e.g., DELETE on a read-heavy API)

Chunk supersession

When the PolicyAdvisor produces a Stage 2 (refined) chunk:

  • Set stage: "refined" and supersedes_chunk_id pointing to the Stage 1 chunk
  • If the Stage 1 chunk was already approved, the Stage 2 chunk replaces its rule in the active policy on approval
  • The original Stage 1 chunk transitions to superseded status

DNS probe for private IP hosts

When the DenialAggregator creates a new entry, perform a speculative one-shot tokio::net::lookup_host():

  • If the host resolves to a private IP (RFC1918), include allowed_ips suggestion in the proposed chunk
  • The gateway independently re-verifies DNS on SubmitPolicyAnalysis receipt (trust anchor — sandbox DNS is untrusted)
  • Private IP chunks get security_notes automatically: "Host resolves to private IP {ip}. Verify this is an expected internal service."

Rejection feedback loop

  • Store rejection reasons on chunks (already supported by parent issue's RejectDraftChunk RPC)
  • Include rejected chunks with reasons in LLM context (Tier 3d, ~500 tokens)
  • LLM uses rejection feedback to improve re-proposals
  • Rejection backoff: after 2 rejections for the same (host, port) pattern, stop auto-proposing. User can draft retry to re-queue.

Configuration

New cluster-level toggle:

openshell cluster policy-advisor enable # enable LLM-powered analysis
openshell cluster policy-advisor disable # fall back to mechanistic
openshell cluster policy-advisor status # show current mode + model

When disabled (or inference not configured), the mechanistic mapper from the parent issue handles all chunk generation.

Codebase references

AreaFileLines
inference.local fast pathcrates/navigator-sandbox/src/proxy.rs217
Build inference contextcrates/navigator-sandbox/src/lib.rs543-647
Bundle to routescrates/navigator-sandbox/src/lib.rsbundle_to_resolved_routes()
OPA enginecrates/navigator-sandbox/src/opa.rs227-237
Rego L7 evaluationcrates/navigator-sandbox/data/sandbox-policy.rego157-224
Router/backendcrates/navigator-router/src/backend.rsauth injection, model rewrite
Provider profilescrates/navigator-core/src/inference.rs93 (profile_for())
CLI inference cmdscrates/navigator-cli/src/main.rs558-592
Policy gen skill.agents/skills/generate-sandbox-policy/SKILL.md546 lines
Policy gen examples.agents/skills/generate-sandbox-policy/examples.md884 lines
L7 relay eventscrates/navigator-sandbox/src/l7/relay.rs123-133
L7 enforcement modescrates/navigator-sandbox/src/l7/mod.rsEnforcementMode

Design document

Full design: https://gitlab-master.nvidia.com/-/snippets/12930
Local copy: architecture/plans/issue-153-policy-recommendations/00-deep-analysis.md

  • Section 5: LLM integration (system prompt, expected output format, mechanistic fallback)
  • Section 6: Context window management
  • Section 9b: PolicyAdvisor analysis pipeline diagram

Effort estimate

~5-6 days (Phase 3 from the design doc)

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:inferenceInference routing and configuration workarea:policyPolicy engine and policy lifecycle workarea:sandboxSandbox runtime and isolation workstate:agent-readyApproved for agent implementation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions