Repository navigation
feat: policy recommendation plumbing — denial aggregation, transport, approval pipeline, and mechanistic recommendations #204
Description
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 inspectionDenialSummary— structured denial data from sandbox aggregator (host, port, binary, counts, L7 samples, cmdlines, denial_stage)PolicyChunk— proposed rule with rationale, status, stage, supersessionDraftPolicyUpdate— newSandboxStreamEventvariant for live notifications- Request/response pairs for all RPCs below
New RPCs on Navigator service:
SubmitPolicyAnalysis— sandbox → gateway: atomic submission of denial summaries + proposed chunksGetDraftPolicy— CLI/TUI → gateway: query draft chunks with optional status filterApproveDraftChunk/RejectDraftChunk— single-chunk approval/rejectionApproveAllDraftChunks— bulk approval (skips security-flagged unless forced)EditDraftChunk— modify a pending chunk in-place (e.g., narrowallowed_ips)UndoDraftChunk— reverse last approvalGetDraftHistory— 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:
SubmitPolicyAnalysishandler: validate trust boundary (reject loopback/link-local hosts, rate limit per sandbox, format checks), DNS resolution forresolved_ips+is_private_ipannotation, persist summaries + chunks, increment draft version, publishDraftPolicyUpdatetoSandboxWatchBusGetDraftPolicyhandler: query chunks by sandbox, optional status filterApproveDraftChunkhandler: load current active policy, merge chunk'sproposed_ruleintonetwork_policiesmap, persist via existingUpdateSandboxPolicyinternals (deterministic_hash, put_policy_revision, supersede_older, notify watch_bus), update chunk status → approved, update denial_summaries status → resolvedRejectDraftChunkhandler: update chunk status → rejected, store rejection reasonApproveAllDraftChunkshandler: iterate pending chunks, skip those withsecurity_notesunlessinclude_security_flagged=true, merge all into active policyEditDraftChunkhandler: replaceproposed_ruleon a pending chunkUndoDraftChunkhandler: remove merged rule from active policy, revert chunk to pendingGetDraftHistoryhandler: 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()fromproxy.rs:956-974as 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_ipsandis_private_ipon denial summaries - Pre-merge conflict detection: check if proposed rule overlaps existing
network_policiesentries (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)fromL7_REQUESTtracing events (relay.rs:123-133) intol7_request_samplesmap, 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
- All GET/HEAD/OPTIONS →
- Set
enforcement: enforce,supersedes_chunk_idpointing 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: sendsSubmitPolicyAnalysisRequestwith 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:
- Load current active
SandboxPolicy(latest loaded version) - Insert chunk's
proposed_ruleintonetwork_policiesmap underchunk.rule_name - Validate merged policy (static fields unchanged, no duplicate endpoint coverage)
- Persist via existing
UpdateSandboxPolicyinternals - Update chunk status → approved, denial_summaries status → resolved
- Sandbox picks up new policy on next 30s poll cycle, reloads OPA
For chunk supersession (Stage 2 replacing Stage 1):
- Approve the Stage 2 chunk (same merge flow)
- The Stage 2 rule replaces the Stage 1 rule (same
rule_name) - Mark the original Stage 1 chunk as
superseded
DraftPolicyUpdate streaming event
New SandboxStreamEvent variant sent via WatchSandbox stream:
draft_version,new_chunkscount,total_pendingcount, briefsummary- CLI
logs --tailrenders:"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:
aapprove,rreject,Aapprove-all,Enterdetail popup - Chunk detail popup: full proposed YAML, rationale, denial event summary
- Live updates via
DraftPolicyUpdatestream 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
johntmyers commentedon Mar 11, 2026
Implementation Plan
Issue type:
featComplexity: 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
DenialAggregatorthat 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 existingUpdateSandboxPolicyinternals, streaming notifications viaDraftPolicyUpdate, CLI subcommands underopenshell 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 onNavigatorservice + newSandboxStreamEventoneof variantcrates/navigator-server/migrations/sqlite/003_create_policy_recommendations.sql+postgres/003_...: Newdraft_policy_chunksanddenial_summariestablescrates/navigator-server/src/persistence/mod.rs: New record types (DraftChunkRecord,DenialSummaryRecord) + new delegation methods onStoreenumcrates/navigator-server/src/persistence/sqlite.rs: Concrete implementations for new Store methodscrates/navigator-server/src/persistence/postgres.rs: Same for Postgrescrates/navigator-server/src/grpc.rs: 8 new RPC handlers +DraftPolicyUpdateonWatchSandboxstreamcrates/navigator-sandbox/src/denial_aggregator.rs(new):DenialAggregatorstate machine with dedup windows, cooldown, L7 sample ingestion, cmdline sanitization, memory boundscrates/navigator-sandbox/src/mechanistic_mapper.rs(new): DeterministicDenialSummary[] → PolicyChunk[]mapper (Stage 1 L7 audit + Stage 2 access refinement)crates/navigator-sandbox/src/proxy.rs: ThreadDenialEventsender 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: Newsubmit_policy_analysis()method onCachedNavigatorClientcrates/navigator-cli/src/main.rs: NewDraftCommandsenum nested underSandboxCommands, withGet,Approve,Reject,ApproveAll/Apply,Edit,Undo,Clear,Historysubcommandscrates/navigator-cli/src/run.rs: Handler functions for each draft subcommandcrates/navigator-tui/src/app.rs: NewFocus::SandboxDraftvariant, draft state fields,handle_draft_key()handlercrates/navigator-tui/src/ui/sandbox_draft.rs(new): Draft recommendations panel renderercrates/navigator-tui/src/ui/mod.rs: Wire draft panel intodraw_sandbox_screen()crates/navigator-tui/src/lib.rs: Draft data fetching,Event::DraftResultvariantImplementation 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)SubmitPolicyAnalysisRequest(summaries[], proposed_chunks[], analysis_mode)New
SandboxStreamEventoneof variant:DraftPolicyUpdate draft_policy_update = 5New RPCs on
Navigatorservice: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.sqlfor both SQLite and Postgres:draft_policy_chunkstable: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(protobufNetworkPolicyRule),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(sandbox_id, status)denial_summariestable: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(sandbox_id, status), unique index:(sandbox_id, host, port, binary)Add record types (
DraftChunkRecord,DenialSummaryRecord) andStoremethods:put_draft_chunk,get_draft_chunk,list_draft_chunks(with status filter),update_draft_chunk_status,update_draft_chunk_rule,delete_draft_chunks_by_sandboxupsert_denial_summary,list_denial_summaries(with status filter),update_denial_summary_statusget_draft_version(max draft_version for sandbox),increment_draft_versionImplement on both
SqliteStoreandPostgresStore. Write unit tests for CRUD operations.Step 3: Gateway gRPC handlers — submission + query
Implement the receiving side:
SubmitPolicyAnalysishandler:tokio::net::lookup_host), annotateresolved_ips+is_private_ip, reject loopback/link-local hosts (reuseis_internal_iplogic from proxy.rs)max_outstanding(10)DraftPolicyUpdateevent viaSandboxWatchBus::notify()GetDraftPolicyhandler:list_draft_chunkswith optionalstatus_filterGetDraftHistoryhandler:created_at_msDraftHistoryEntry(created, approved, rejected events)Wire
DraftPolicyUpdateintoWatchSandboxstream — add a new arm in thetokio::select!loop. ReuseSandboxWatchBusnotifications (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
ApproveDraftChunkhandler:get_latest_loaded_policy(orget_latest_policywith fallback)rule_namealready exists innetwork_policiesmapproposed_ruleintonetwork_policiesmap underchunk.rule_namevalidate_policy_safety()on merged policydeterministic_hash,put_policy_revision,supersede_older_policies,sandbox_watch_bus.notify()decided_at_ms,decided_byRejectDraftChunkhandler:ApproveAllDraftChunkshandler:!include_security_flagged, filter out chunks wheresecurity_notesis non-emptyEditDraftChunkhandler:proposed_rulewith new rule from requestvalidate_policy_safety()on the edited rule in isolationUndoDraftChunkhandler:rule_namefromnetwork_policiesStep 5: Sandbox DenialAggregator
New module
crates/navigator-sandbox/src/denial_aggregator.rs.Define
DenialEventenum:DenialAggregatorstruct withHashMap<(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,ancestorsState machine (from RFC Section 9d):
total_count++,last_seen=now, add cmdline, update L7 samples if L7 eventsuppressed_count++, skipwindow_count++, check threshold (3) or persistent_threshold (10)DenialSummary, enter cooldown, reset window + suppressedPeriodic sweep (30s):
persistent=trueThread the sender channel through:
ProxyHandle::start_with_bind_addr()in proxy.rs → accept loop →handle_tcp_connection()→ CONNECT/FORWARD handlersrelay_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):
protocol: rest,tls: terminate(443),enforcement: audit,access: fullauto_{sanitized_host}_{port}Stage 2 (summary has L7 samples, existing approved Stage 1 chunk):
enforcement: enforce,supersedes_chunk_id→ Stage 1 chunkAnalysis trigger loop (spawned in
lib.rsafter policy poll):submit_policy_analysis()viaCachedNavigatorClientStep 7: CLI subcommands
Add
DraftCommandsenum nested underSandboxCommands:DraftSubcommands:Get(with--chunks,--chunk <id>flags),Approve(chunk_id or--allwith--force),Reject(chunk_id,--reason),Apply,Clear,Edit(chunk_id,--allowed-ips,--access,--add-binary),Undo(chunk_id),HistoryHandler functions in
run.rs:draft_get(): callGetDraftPolicy, format output (policy overview or chunk detail)draft_approve(): callApproveDraftChunk, display merge result + wait for sandbox reloaddraft_reject(): callRejectDraftChunkdraft_approve_all()/draft_apply(): callApproveAllDraftChunksdraft_edit(): load chunk, modify fields, callEditDraftChunkdraft_undo(): callUndoDraftChunkdraft_clear(): call bulk delete (may need aClearDraftChunksRPC or iterate)draft_history(): callGetDraftHistory, format timelineOutput 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::SandboxDraftvariant inapp.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/knavigate,Entertoggle detail popup,aapprove selected,rreject selected,Aapprove all,p/lswitch views,Escback.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. Inlib.rsevent loop, callGetDraftPolicyRPC. For approve/reject actions, spawn gRPC calls and refresh on completion.Wire
DraftPolicyUpdatefromWatchSandboxlog stream: detectdraft_policy_updatevariant in the stream handler, update draft state.Step 9: Architecture doc —
architecture/policy-advisor.mdNew 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:openshell sandbox draftcommands, TUI draft panel, log stream notificationsThis 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 sanitizationmechanistic_mapper.rs: Stage 1 generation (HTTP vs non-HTTP ports), Stage 2 refinement (method analysis), rule naming, supersessionIntegration tests (if feasible without full cluster):
Pre-commit:
mise run pre-commitTest Plan
sandbox draftsmoke tests could be added if cluster is available.Risks & Open Questions
handle_tcp_connectionandhandle_forward_proxytouches a hot path. Benchmarktry_sendoverhead (should be negligible but worth confirming).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.deterministic_hash+ no-op check provides this.ClearDraftChunksRPC: The issue mentionssandbox draft clearbut no dedicated RPC was specified. May need aClearDraftChunksRPC or reuse bulk status update.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 workflowarchitecture/sandbox.md: Add DenialAggregator and mechanistic mapper descriptionarchitecture/gateway.md: Add draft policy persistence and approval RPCsRevision 2 — added Step 9: architecture/policy-advisor.md product-level doc
Revision 1 — initial plan
feat(policy): add policy recommendation plumbing — denial aggregation…
feat(policy): add policy recommendation plumbing (#204) (#222)
feat(policy): add policy recommendation plumbing (#204) (#222)
feat(policy): add policy recommendation plumbing — denial aggregation…