Skip to content

feat: policy recommendation plumbing — denial aggregation, transport, approval pipeline, and mechanistic recommendations #204

Closed

Description

@johntmyers

Summary

Build the end-to-end infrastructure for sandbox-initiated policy recommendations: denial aggregation in the sandbox, gRPC transport to the gateway, persistence, approval workflow, policy merge, and CLI/TUI for human-in-the-loop review. Includes a deterministic (no-LLM) chunk generator so the full pipeline is testable without inference configured.

This is the plumbing half of #153. The LLM-powered PolicyAdvisor agent harness is a follow-up issue that plugs into this infrastructure.

Architecture

Follows Option D from the design doc — sandbox aggregates denials locally, generates mechanistic recommendations, submits to gateway for persistence + approval.

SANDBOX                              GATEWAY                        USER
┌──────────────────────┐     ┌──────────────────────┐     ┌──────────────────┐
│                      │     │                      │     │                  │
│  proxy.rs deny event │     │                      │     │                  │
│       │              │     │                      │     │                  │
│       v              │     │                      │     │                  │
│  DenialAggregator    │     │                      │     │                  │
│    group by          │     │                      │     │                  │
│    (host,port,binary)│     │                      │     │                  │
│    dedup + cooldown  │     │                      │     │                  │
│    L7 event ingestion│     │                      │     │                  │
│       │              │     │                      │     │                  │
│       v              │     │                      │     │                  │
│  MechanisticMapper   │     │                      │     │                  │
│    host:port → rule  │     │                      │     │                  │
│    Stage 1: L7 audit │     │                      │     │                  │
│    Stage 2: refine   │     │                      │     │                  │
│       │              │     │                      │     │                  │
│       │ SubmitPolicy │     │                      │     │                  │
│       │ Analysis RPC │     │                      │     │                  │
│       └──────────────┼────>│  Validate + persist  │     │                  │
│                      │     │  DraftPolicyUpdate   │────>│  CLI: draft list │
│                      │     │       │              │     │  TUI: draft panel│
│                      │     │       v              │     │       │          │
│                      │     │  Approve/Reject RPCs │<────│  approve/reject  │
│                      │     │       │              │     │                  │
│                      │     │  Merge into active   │     │                  │
│  Policy poll ←───────┼─────│  policy              │     │                  │
│  OPA reload          │     │                      │     │                  │
└──────────────────────┘     └──────────────────────┘     └──────────────────┘

Scope

Proto definitions (all messages + RPCs)

New messages in proto/:

  • L7RequestSample — observed HTTP method+path from L7 inspection
  • DenialSummary — structured denial data from sandbox aggregator (host, port, binary, counts, L7 samples, cmdlines, denial_stage)
  • PolicyChunk — proposed rule with rationale, status, stage, supersession
  • DraftPolicyUpdate — new SandboxStreamEvent variant for live notifications
  • Request/response pairs for all RPCs below

New RPCs on Navigator service:

  • SubmitPolicyAnalysis — sandbox → gateway: atomic submission of denial summaries + proposed chunks
  • GetDraftPolicy — CLI/TUI → gateway: query draft chunks with optional status filter
  • ApproveDraftChunk / RejectDraftChunk — single-chunk approval/rejection
  • ApproveAllDraftChunks — bulk approval (skips security-flagged unless forced)
  • EditDraftChunk — modify a pending chunk in-place (e.g., narrow allowed_ips)
  • UndoDraftChunk — reverse last approval
  • GetDraftHistory — audit trail of all decisions

See design doc Section 10 for complete proto definitions.

Gateway persistence layer

New tables:

  • draft_policy_chunks — stores proposed chunks with status lifecycle (pending → approved/rejected/superseded)
  • denial_summaries — stores aggregated denial data, upserted by (sandbox_id, host, port, binary)

Indexes for efficient querying by sandbox + status.

Extend the Store trait (crates/navigator-server/src/persistence/mod.rs) with methods for CRUD on both tables.

Gateway gRPC handlers

In crates/navigator-server/src/grpc.rs:

  • SubmitPolicyAnalysis handler: validate trust boundary (reject loopback/link-local hosts, rate limit per sandbox, format checks), DNS resolution for resolved_ips + is_private_ip annotation, persist summaries + chunks, increment draft version, publish DraftPolicyUpdate to SandboxWatchBus
  • GetDraftPolicy handler: query chunks by sandbox, optional status filter
  • ApproveDraftChunk handler: load current active policy, merge chunk's proposed_rule into network_policies map, persist via existing UpdateSandboxPolicy internals (deterministic_hash, put_policy_revision, supersede_older, notify watch_bus), update chunk status → approved, update denial_summaries status → resolved
  • RejectDraftChunk handler: update chunk status → rejected, store rejection reason
  • ApproveAllDraftChunks handler: iterate pending chunks, skip those with security_notes unless include_security_flagged=true, merge all into active policy
  • EditDraftChunk handler: replace proposed_rule on a pending chunk
  • UndoDraftChunk handler: remove merged rule from active policy, revert chunk to pending
  • GetDraftHistory handler: return chronological decision log

Gateway validation (trust boundary)

Per R7 from the design doc:

  • Reject chunks with loopback (127.0.0.0/8) or link-local (169.254.0.0/16) hosts — use is_internal_ip() from proxy.rs:956-974 as reference
  • Rate limit: max 10 outstanding pending chunks per sandbox (max_outstanding)
  • Format validation: rule names, endpoint fields, binary paths
  • DNS re-verification: gateway resolves hostnames independently (sandbox DNS is untrusted), annotates resolved_ips and is_private_ip on denial summaries
  • Pre-merge conflict detection: check if proposed rule overlaps existing network_policies entries (same host:port:binary)

Sandbox DenialAggregator

New module in crates/navigator-sandbox/src/:

  • Groups denial events by primary key (host, port, binary)
  • Dedup window (default 60s) with threshold (default 3) before emission
  • Cooldown (default 5m) between emissions for same key
  • Count tracking: window_count (resets), suppressed_count (cooldown drops), total_count (cumulative, never resets)
  • persistent_threshold (default 10): emit regardless of windowing for slow-drip patterns
  • Memory bounds: max_keys=1000, overflow counter for flood protection
  • Stale-flush: periodic sweep (30s) emits entries older than 5m with any activity
  • L7 event ingestion: collect (method, path, decision) from L7_REQUEST tracing events (relay.rs:123-133) into l7_request_samples map, capped at 50 distinct pairs per entry
  • Cmdline sanitization: redact Authorization, X-Vault-Token, Cookie, X-Api-Key headers, passwords, query string tokens before storage
  • Two event sources: L4 CONNECT deny from proxy.rs, L7 audit/deny from relay.rs

Design doc Section 9d has the full state machine diagram.

Sandbox mechanistic chunk generator

A deterministic mapper (no LLM) that converts denial summaries into proposed PolicyChunks:

Stage 1 (L4 denial → initial recommendation):

  • For HTTP-capable ports (80, 443, 8080, 8200, 9200): recommend rule with protocol: rest, tls: terminate (443), enforcement: audit, access: full — this unblocks traffic while enabling L7 visibility
  • For other ports: recommend plain L4 allow rule
  • Rule name: auto_{host}_{port} (sanitized)
  • Rationale: "Denied {count} connections to {host}:{port} from {binary}"

Stage 2 (L7 audit data → refined recommendation):

  • When an approved Stage 1 chunk has accumulated l7_request_samples:
    • All GET/HEAD/OPTIONS → access: read-only
    • Includes POST but no DELETE → access: read-write
    • Includes DELETE → access: full
  • Set enforcement: enforce, supersedes_chunk_id pointing to the Stage 1 chunk
  • Rationale: "Observed {n} HTTP requests ({method_summary}). Recommending {access} access."

This is intentionally simple — the LLM PolicyAdvisor (follow-up issue) replaces it with intelligent grouping, security analysis, and richer rationale.

Sandbox gRPC client

Extend CachedNavigatorClient (crates/navigator-sandbox/src/grpc_client.rs) with:

  • submit_policy_analysis() method: sends SubmitPolicyAnalysisRequest with denial summaries + proposed chunks
  • Error handling: log and retry on transient failures, drop on permanent failures
  • Include analysis_mode: "mechanistic" field

Approval merge flow

When a chunk is approved:

  1. Load current active SandboxPolicy (latest loaded version)
  2. Insert chunk's proposed_rule into network_policies map under chunk.rule_name
  3. Validate merged policy (static fields unchanged, no duplicate endpoint coverage)
  4. Persist via existing UpdateSandboxPolicy internals
  5. Update chunk status → approved, denial_summaries status → resolved
  6. Sandbox picks up new policy on next 30s poll cycle, reloads OPA

For chunk supersession (Stage 2 replacing Stage 1):

  1. Approve the Stage 2 chunk (same merge flow)
  2. The Stage 2 rule replaces the Stage 1 rule (same rule_name)
  3. Mark the original Stage 1 chunk as superseded

DraftPolicyUpdate streaming event

New SandboxStreamEvent variant sent via WatchSandbox stream:

  • draft_version, new_chunks count, total_pending count, brief summary
  • CLI logs --tail renders: "Draft policy updated: N new chunks. Run 'openshell sandbox draft <name>' to review."
  • TUI shows notification badge on Draft tab

CLI commands

New subcommands under openshell sandbox draft:

Command Description
sandbox draft <name> View the full living draft policy
sandbox draft <name> --chunks View individual chunks with rationale
sandbox draft <name> --chunk <id> View a specific chunk in detail
sandbox draft approve <name> <chunk_id> Approve a specific chunk
sandbox draft approve <name> --all Approve all pending (skips security-flagged)
sandbox draft approve <name> --all --force Include security-flagged chunks
sandbox draft reject <name> <chunk_id> Reject a specific chunk
sandbox draft reject <name> <chunk_id> --reason "..." Reject with reason
sandbox draft apply <name> Approve all + merge into active policy
sandbox draft clear <name> Clear all pending chunks
sandbox draft edit <name> <chunk_id> [--allowed-ips ...] [--access ...] Modify a pending chunk
sandbox draft undo <name> <chunk_id> Reverse last approval
sandbox draft history <name> Show decision history

See design doc Section 8a for detailed CLI flow examples.

TUI draft panel

New view mode in sandbox detail screen (keybinding cycle: p → l → d → p):

  • List pending/approved/rejected chunks with rationale summaries
  • Keybindings: a approve, r reject, A approve-all, Enter detail popup
  • Chunk detail popup: full proposed YAML, rationale, denial event summary
  • Live updates via DraftPolicyUpdate stream event
  • Status bar notification when new chunks arrive

See design doc Section 8b for TUI mockups.

Codebase references

Area File Lines
Proxy deny path crates/navigator-sandbox/src/proxy.rs 237-299
L7 relay events crates/navigator-sandbox/src/l7/relay.rs 123-133
Log push crates/navigator-sandbox/src/log_push.rs 44, 93
Policy poll loop crates/navigator-sandbox/src/lib.rs ~543-647
gRPC client crates/navigator-sandbox/src/grpc_client.rs —
OPA evaluate crates/navigator-sandbox/src/opa.rs 227-237
Gateway gRPC crates/navigator-server/src/grpc.rs 1223+
Persistence Store crates/navigator-server/src/persistence/mod.rs —
Tracing bus crates/navigator-server/src/tracing_bus.rs —
SSRF check crates/navigator-sandbox/src/proxy.rs 956-974
Proto: sandbox events proto/navigator.proto SandboxStreamEvent
Proto: policy rules proto/sandbox.proto NetworkPolicyRule
CLI commands crates/navigator-cli/src/main.rs SandboxCommands
CLI handlers crates/navigator-cli/src/run.rs —
TUI app crates/navigator-tui/src/app.rs Focus enum
Policy schema ref architecture/security-policy.md —

Design document

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

Effort estimate

~15-18 days (Phases 1, 2, 4, 5 from the design doc, minus LLM-specific parts)

Activity

  1. added
    area:cliCLI-related work
    area:policyPolicy engine and policy lifecycle work
    area:sandboxSandbox runtime and isolation work
    area:gatewayGateway server and control-plane work
    area:tuiTerminal UI work
    and removed on Mar 10, 2026
  2. johntmyers commented on Mar 11, 2026

    @johntmyers
    CollaboratorAuthor

    🏗️ build-plan

    Implementation Plan

    Issue type: feat
    Complexity: High
    Confidence: High — all patterns are well-established in the codebase, the RFC is thorough, and the design has been simulated

    Summary

    Build the end-to-end policy recommendation pipeline: a sandbox-side DenialAggregator that captures structured deny events from the proxy and L7 relay, a deterministic mechanistic mapper that converts denial summaries into proposed policy chunks, gRPC transport to submit analysis results, gateway persistence with two new tables, an approval workflow that merges approved chunks into the active policy via existing UpdateSandboxPolicy internals, streaming notifications via DraftPolicyUpdate, CLI subcommands under openshell sandbox draft, and a TUI draft panel.

    Scope

    • proto/navigator.proto: New messages (DenialSummary, L7RequestSample, PolicyChunk, DraftPolicyUpdate, SubmitPolicyAnalysis*, GetDraftPolicy*, ApproveDraftChunk*, RejectDraftChunk*, ApproveAllDraftChunks*, EditDraftChunk*, UndoDraftChunk*, GetDraftHistory*) + new RPCs on Navigator service + new SandboxStreamEvent oneof variant
    • crates/navigator-server/migrations/sqlite/003_create_policy_recommendations.sql + postgres/003_...: New draft_policy_chunks and denial_summaries tables
    • crates/navigator-server/src/persistence/mod.rs: New record types (DraftChunkRecord, DenialSummaryRecord) + new delegation methods on Store enum
    • crates/navigator-server/src/persistence/sqlite.rs: Concrete implementations for new Store methods
    • crates/navigator-server/src/persistence/postgres.rs: Same for Postgres
    • crates/navigator-server/src/grpc.rs: 8 new RPC handlers + DraftPolicyUpdate on WatchSandbox stream
    • crates/navigator-sandbox/src/denial_aggregator.rs (new): DenialAggregator state machine with dedup windows, cooldown, L7 sample ingestion, cmdline sanitization, memory bounds
    • crates/navigator-sandbox/src/mechanistic_mapper.rs (new): Deterministic DenialSummary[] → PolicyChunk[] mapper (Stage 1 L7 audit + Stage 2 access refinement)
    • crates/navigator-sandbox/src/proxy.rs: Thread DenialEvent sender through proxy; emit structured deny events at CONNECT deny (line ~331), FORWARD deny (line ~1490), and SSRF deny points — Option A from codebase analysis (direct callback, no string parsing)
    • crates/navigator-sandbox/src/l7/relay.rs: Emit structured L7 deny/audit events to the aggregator channel (alongside existing tracing event at line ~123)
    • crates/navigator-sandbox/src/lib.rs: Spawn aggregator + analysis trigger loop as background tasks (after policy poll spawn at line ~537)
    • crates/navigator-sandbox/src/grpc_client.rs: New submit_policy_analysis() method on CachedNavigatorClient
    • crates/navigator-cli/src/main.rs: New DraftCommands enum nested under SandboxCommands, with Get, Approve, Reject, ApproveAll/Apply, Edit, Undo, Clear, History subcommands
    • crates/navigator-cli/src/run.rs: Handler functions for each draft subcommand
    • crates/navigator-tui/src/app.rs: New Focus::SandboxDraft variant, draft state fields, handle_draft_key() handler
    • crates/navigator-tui/src/ui/sandbox_draft.rs (new): Draft recommendations panel renderer
    • crates/navigator-tui/src/ui/mod.rs: Wire draft panel into draw_sandbox_screen()
    • crates/navigator-tui/src/lib.rs: Draft data fetching, Event::DraftResult variant

    Implementation Steps

    The steps are sequenced so each is independently testable.

    Step 1: Proto definitions

    Add all new messages and RPCs to proto/navigator.proto. This is the contract everything else builds against.

    New messages:

    • L7RequestSample (method, path, decision, count)
    • DenialSummary (sandbox_id, host, port, binary, ancestors, deny_reason, timestamps, counts, L7 samples, denial_stage, etc.)
    • PolicyChunk (id, status, stage, rule_name, proposed_rule: NetworkPolicyRule, rationale, security_notes, confidence, denial_summary_ids, supersedes_chunk_id)
    • DraftPolicyUpdate (draft_version, new_chunks, total_pending, summary)
    • Request/response pairs for all 8 RPCs
    • SubmitPolicyAnalysisRequest (summaries[], proposed_chunks[], analysis_mode)

    New SandboxStreamEvent oneof variant: DraftPolicyUpdate draft_policy_update = 5

    New RPCs on Navigator service:

    • SubmitPolicyAnalysis (sandbox → gateway)
    • GetDraftPolicy, ApproveDraftChunk, RejectDraftChunk, ApproveAllDraftChunks, EditDraftChunk, UndoDraftChunk, GetDraftHistory (CLI/TUI → gateway)

    Build to verify proto generation succeeds.

    Step 2: Persistence layer

    Create migration files 003_create_policy_recommendations.sql for both SQLite and Postgres:

    draft_policy_chunks table:

    • id TEXT PRIMARY KEY, sandbox_id TEXT NOT NULL, draft_version INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'pending', stage TEXT NOT NULL DEFAULT 'initial', rule_name TEXT NOT NULL, proposed_rule BLOB NOT NULL (protobuf NetworkPolicyRule), rationale TEXT NOT NULL, security_notes TEXT, confidence REAL, denial_refs TEXT NOT NULL (JSON array), supersedes_chunk_id TEXT, created_at_ms INTEGER NOT NULL, decided_at_ms INTEGER, decided_by TEXT
    • Index: (sandbox_id, status)

    denial_summaries table:

    • id TEXT PRIMARY KEY, sandbox_id TEXT NOT NULL, host TEXT NOT NULL, port INTEGER NOT NULL, binary TEXT NOT NULL, ancestors TEXT, deny_reason TEXT NOT NULL, timestamps, counts (count, suppressed_count, total_count), sample_cmdlines TEXT (JSON), binary_sha256 TEXT, persistent BOOLEAN, denial_stage TEXT, resolved_ips TEXT (JSON), is_private_ip BOOLEAN, l7_request_samples TEXT (JSON), l7_inspection_active BOOLEAN, status TEXT DEFAULT 'new', timestamps
    • Index: (sandbox_id, status), unique index: (sandbox_id, host, port, binary)

    Add record types (DraftChunkRecord, DenialSummaryRecord) and Store methods:

    • put_draft_chunk, get_draft_chunk, list_draft_chunks (with status filter), update_draft_chunk_status, update_draft_chunk_rule, delete_draft_chunks_by_sandbox
    • upsert_denial_summary, list_denial_summaries (with status filter), update_denial_summary_status
    • get_draft_version (max draft_version for sandbox), increment_draft_version

    Implement on both SqliteStore and PostgresStore. Write unit tests for CRUD operations.

    Step 3: Gateway gRPC handlers — submission + query

    Implement the receiving side:

    SubmitPolicyAnalysis handler:

    1. Validate sandbox exists
    2. For each denial summary: resolve DNS (tokio::net::lookup_host), annotate resolved_ips + is_private_ip, reject loopback/link-local hosts (reuse is_internal_ip logic from proxy.rs)
    3. Rate limit: count pending chunks for sandbox, reject if >= max_outstanding (10)
    4. Upsert denial summaries
    5. Validate each proposed chunk: format checks (rule_name, endpoint fields, binary paths), reject bad ones
    6. Persist valid chunks, increment draft_version
    7. Publish DraftPolicyUpdate event via SandboxWatchBus::notify()

    GetDraftPolicy handler:

    1. Validate sandbox exists
    2. Query list_draft_chunks with optional status_filter
    3. Return chunks + draft_version

    GetDraftHistory handler:

    1. List all chunks for sandbox ordered by created_at_ms
    2. Map to DraftHistoryEntry (created, approved, rejected events)

    Wire DraftPolicyUpdate into WatchSandbox stream — add a new arm in the tokio::select! loop. Reuse SandboxWatchBus notifications (the handler already re-reads from store on notification, so we just need to include draft data in the notification path, or add a separate subscription channel for draft events).

    Step 4: Gateway gRPC handlers — approval workflow

    ApproveDraftChunk handler:

    1. Validate chunk exists and is pending
    2. Load current active policy via get_latest_loaded_policy (or get_latest_policy with fallback)
    3. Pre-merge conflict detection: check if rule_name already exists in network_policies map
    4. Insert chunk's proposed_rule into network_policies map under chunk.rule_name
    5. Run validate_policy_safety() on merged policy
    6. Persist via existing update_sandbox_policy internals: deterministic_hash, put_policy_revision, supersede_older_policies, sandbox_watch_bus.notify()
    7. Update chunk status → approved, decided_at_ms, decided_by
    8. Update denial_summaries referenced by chunk → resolved
    9. If chunk.stage == "refined" and chunk.supersedes_chunk_id is set: update superseded chunk status → superseded, remove its rule from active policy (replace with refined rule)
    10. Return new policy version + hash

    RejectDraftChunk handler:

    1. Validate chunk exists and is pending
    2. Update status → rejected, store reason
    3. Return success

    ApproveAllDraftChunks handler:

    1. List all pending chunks for sandbox
    2. If !include_security_flagged, filter out chunks where security_notes is non-empty
    3. Merge all remaining chunks into active policy (batch)
    4. Persist, update statuses
    5. Return version + hash + counts

    EditDraftChunk handler:

    1. Validate chunk exists and is pending
    2. Replace proposed_rule with new rule from request
    3. Run validate_policy_safety() on the edited rule in isolation
    4. Persist

    UndoDraftChunk handler:

    1. Validate chunk exists and is approved
    2. Load current policy, remove chunk's rule_name from network_policies
    3. Persist updated policy
    4. Revert chunk status → pending

    Step 5: Sandbox DenialAggregator

    New module crates/navigator-sandbox/src/denial_aggregator.rs.

    Define DenialEvent enum:

    L4Deny { host, port, binary, binary_pid, ancestors, cmdline_paths, reason }
    L7Event { host, port, binary, l7_action, l7_target, l7_decision, l7_deny_reason }
    SsrfDeny { host, port, reason }
    

    DenialAggregator struct with HashMap<(String, u16, String), AggregatorEntry>:

    • AggregatorEntry: window_count, suppressed_count, total_count, distinct_cmdlines: HashSet<String>, first_seen, last_seen, in_cooldown, cooldown_until, l7_request_samples: HashMap<(String,String), L7SampleAccum>, l7_inspection_active, deny_reason, ancestors

    State machine (from RFC Section 9d):

    1. Sanitize cmdline (redact Authorization, X-Vault-Token, Cookie, X-Api-Key, passwords, query tokens)
    2. Lookup or create entry (with max_keys=1000 cap + overflow counter)
    3. Always: total_count++, last_seen=now, add cmdline, update L7 samples if L7 event
    4. If in cooldown: suppressed_count++, skip
    5. Else: window_count++, check threshold (3) or persistent_threshold (10)
    6. On threshold: emit DenialSummary, enter cooldown, reset window + suppressed

    Periodic sweep (30s):

    • Expire stale entries (age > 2*dedup_window, total < persistent_threshold)
    • Stale-flush (age > 5m, not in cooldown): emit with persistent=true
    • Overflow summary if overflow_count > 0

    Thread the sender channel through:

    • ProxyHandle::start_with_bind_addr() in proxy.rs → accept loop → handle_tcp_connection() → CONNECT/FORWARD handlers
    • relay_with_inspection() in relay.rs (or emit alongside existing tracing event)

    Step 6: Sandbox mechanistic mapper + analysis loop

    New module crates/navigator-sandbox/src/mechanistic_mapper.rs.

    generate_chunks(summaries: &[DenialSummary], existing_chunks: &[PolicyChunk]) -> Vec<PolicyChunk>:

    Stage 1 (new host:port, no existing chunk):

    • HTTP ports (80, 443, 8080, 8200, 9200): protocol: rest, tls: terminate (443), enforcement: audit, access: full
    • Other ports: plain L4 allow
    • Rule name: auto_{sanitized_host}_{port}
    • Rationale: template string with count + host + port + binary

    Stage 2 (summary has L7 samples, existing approved Stage 1 chunk):

    • Analyze methods: all GET/HEAD/OPTIONS → read-only; POST no DELETE → read-write; DELETE → full
    • enforcement: enforce, supersedes_chunk_id → Stage 1 chunk
    • Rationale: template with method summary

    Analysis trigger loop (spawned in lib.rs after policy poll):

    • Collect summaries from aggregator
    • Run mechanistic mapper
    • Call submit_policy_analysis() via CachedNavigatorClient
    • Adaptive interval: 10s cold-start, 60s steady-state, back-off on no-op

    Step 7: CLI subcommands

    Add DraftCommands enum nested under SandboxCommands:

    Draft {
        name: Option<String>,
        #[command(subcommand)]
        command: Option<DraftSubcommands>,
    }
    

    DraftSubcommands: Get (with --chunks, --chunk <id> flags), Approve (chunk_id or --all with --force), Reject (chunk_id, --reason), Apply, Clear, Edit (chunk_id, --allowed-ips, --access, --add-binary), Undo (chunk_id), History

    Handler functions in run.rs:

    • draft_get(): call GetDraftPolicy, format output (policy overview or chunk detail)
    • draft_approve(): call ApproveDraftChunk, display merge result + wait for sandbox reload
    • draft_reject(): call RejectDraftChunk
    • draft_approve_all() / draft_apply(): call ApproveAllDraftChunks
    • draft_edit(): load chunk, modify fields, call EditDraftChunk
    • draft_undo(): call UndoDraftChunk
    • draft_clear(): call bulk delete (may need a ClearDraftChunks RPC or iterate)
    • draft_history(): call GetDraftHistory, format timeline

    Output formatting follows existing patterns: section headers in cyan bold, key-value in dimmed labels, YAML for proposed rules via navigator_policy::serialize_*.

    Step 8: TUI draft panel

    Add Focus::SandboxDraft variant in app.rs. Add state fields: draft_chunks: Vec<PolicyChunk>, draft_scroll: usize, draft_selected: usize, draft_version: u64, pending_draft_fetch: bool, show_draft_detail: bool.

    Keybinding cycle update: SandboxPolicy → l → SandboxLogs → d → SandboxDraft → p → SandboxPolicy.

    handle_draft_key(): j/k navigate, Enter toggle detail popup, a approve selected, r reject selected, A approve all, p/l switch views, Esc back.

    New ui/sandbox_draft.rs: render chunk list with status badges, rationale preview, denial count + recency. Detail popup with full proposed YAML + rationale + denial refs.

    Data fetching: on entering sandbox draft view, set pending_draft_fetch = true. In lib.rs event loop, call GetDraftPolicy RPC. For approve/reject actions, spawn gRPC calls and refresh on completion.

    Wire DraftPolicyUpdate from WatchSandbox log stream: detect draft_policy_update variant in the stream handler, update draft state.

    Step 9: Architecture doc — architecture/policy-advisor.md

    New product-level architecture document capturing the holistic view of the policy recommendation feature (spanning both #204 and #205). Written in the style of the existing architecture/ docs (mermaid diagrams, descriptive prose, tables). Covers:

    • What the feature does and why (the problem: dead-end denials, manual remediation)
    • End-to-end logical flow: deny event → aggregation → analysis → transport → persistence → approval → merge → sandbox reload
    • Architecture diagram (sandbox components, gateway components, user touchpoints)
    • Message infrastructure: DenialSummary, PolicyChunk, DraftPolicyUpdate, and how they flow between components
    • The living draft policy concept (chunks, stages, supersession, status lifecycle)
    • Two analysis modes: mechanistic (deterministic, no LLM) and LLM-powered PolicyAdvisor (future, feat: LLM-powered PolicyAdvisor agent harness for intelligent policy recommendations #205)
    • Progressive L7 visibility: Stage 1 (L7 audit) → Stage 2 (refined from observed traffic)
    • Trust boundary: sandbox proposes, gateway validates, user approves
    • UX surfaces: CLI openshell sandbox draft commands, TUI draft panel, log stream notifications

    This is a product/architecture doc, not an engineering spec — it should be readable by someone who wants to understand what the system does without reading code.

    Step 10: Tests + verification

    Unit tests:

    • denial_aggregator.rs: threshold/cooldown/dedup, L7 sample accumulation, memory cap, stale-flush, cmdline sanitization
    • mechanistic_mapper.rs: Stage 1 generation (HTTP vs non-HTTP ports), Stage 2 refinement (method analysis), rule naming, supersession

    Integration tests (if feasible without full cluster):

    • Proto round-trip: serialize/deserialize new messages
    • Persistence: CRUD on both new tables via SQLite Store
    • Approval merge: create sandbox with policy, submit chunks, approve, verify merged policy

    Pre-commit: mise run pre-commit

    Test Plan

    • Unit tests: DenialAggregator state machine (threshold, cooldown, L7 samples, memory bounds, cmdline sanitization), mechanistic mapper (Stage 1/2 generation, edge cases), persistence CRUD for new tables. Tests live alongside their modules.
    • Integration tests: Full approval flow (submit → persist → approve → merge → verify policy version). Proto serialization round-trips for new message types. These can run against an in-memory SQLite store.
    • E2E tests: Defer to follow-up — the full sandbox deny → aggregate → submit → approve flow requires a running sandbox. CLI sandbox draft smoke tests could be added if cluster is available.

    Risks & Open Questions

    1. Proxy channel threading: Adding a sender to handle_tcp_connection and handle_forward_proxy touches a hot path. Benchmark try_send overhead (should be negligible but worth confirming).
    2. WatchSandbox DraftPolicyUpdate delivery: Reusing SandboxWatchBus::notify() means the client re-reads all sandbox state. A dedicated draft bus would be more efficient but adds complexity. Start with the notify approach.
    3. Approval atomicity: Approving a chunk involves read-modify-write on the active policy. Concurrent approvals need optimistic locking (check policy hash before persisting). The existing deterministic_hash + no-op check provides this.
    4. ClearDraftChunks RPC: The issue mentions sandbox draft clear but no dedicated RPC was specified. May need a ClearDraftChunks RPC or reuse bulk status update.
    5. Large file changes: grpc.rs (3645 lines), proxy.rs (2276 lines), main.rs (2196 lines), run.rs (3678 lines) are already large. Changes are additive (new handlers/commands), not modifying existing code, so risk is manageable.

    Documentation Impact

    • architecture/policy-advisor.md (new): Product-level architecture doc for the full policy recommendation feature (Step 9)
    • architecture/security-policy.md: Add section on draft policy chunks and approval workflow
    • architecture/sandbox.md: Add DenialAggregator and mechanistic mapper description
    • architecture/gateway.md: Add draft policy persistence and approval RPCs

    Revision 2 — added Step 9: architecture/policy-advisor.md product-level doc
    Revision 1 — initial plan

  3. self-assigned this
    on Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:cliCLI-related workarea:gatewayGateway server and control-plane workarea:policyPolicy engine and policy lifecycle workarea:sandboxSandbox runtime and isolation workarea:tuiTerminal UI workstate:agent-readyApproved for agent implementationstate:in-progressWork is currently in progressstate:review-readyReady for human review

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions