diff --git a/.agents/skills/openshell-cli/SKILL.md b/.agents/skills/openshell-cli/SKILL.md index 8c8ba6f546..69a889a8e2 100644 --- a/.agents/skills/openshell-cli/SKILL.md +++ b/.agents/skills/openshell-cli/SKILL.md @@ -493,7 +493,7 @@ Avoid `--yes` during interactive work. A global policy locks policy control for ### Review agent-authored rule proposals -Sandboxes created with `--approval-mode manual` place every proposal in the review inbox. `auto` approves only proposals with an empty prover delta; findings still require review. +Sandboxes created with `--approval-mode manual` place every proposal in the review inbox. `auto` approves only valid effective-policy candidates with an empty prover delta; findings still require review. The CLI binds approval to the candidate's current review token. If live policy, provider, or credential inputs change, approval leaves the chunk pending with a refreshed candidate and requires a fresh review. ```bash openshell rule get dev --status pending @@ -502,7 +502,7 @@ openshell rule reject dev --chunk-id --reason "too broad" openshell rule history dev ``` -Review the proposed scope and prover findings before approval. Treat `rule approve-all --include-security-flagged` as a high-risk bulk action. +Review the proposed scope, candidate hash, prover findings, and application errors before approval. Treat `rule approve-all --include-security-flagged` as a high-risk bulk action. --- diff --git a/.agents/skills/openshell-cli/cli-reference.md b/.agents/skills/openshell-cli/cli-reference.md index 61beab95f1..695aceedfe 100644 --- a/.agents/skills/openshell-cli/cli-reference.md +++ b/.agents/skills/openshell-cli/cli-reference.md @@ -471,7 +471,7 @@ Review agent-authored network rule proposals. This command group is intentionall - `openshell rule clear [name]` - `openshell rule history [name]` -Sandbox names default to the last-used sandbox. Bulk approval of security-flagged proposals requires explicit `--include-security-flagged`. +Sandbox names default to the last-used sandbox. The CLI fetches and submits each proposal's current review token; a changed live candidate remains pending until it is reviewed again. Bulk approval of security-flagged proposals requires explicit `--include-security-flagged`. --- diff --git a/architecture/security-policy.md b/architecture/security-policy.md index 0cc3aa618d..3f582eb25d 100644 --- a/architecture/security-policy.md +++ b/architecture/security-policy.md @@ -216,10 +216,16 @@ through the proposal loop instead of treating the denial as terminal. 1. **Submit.** Both proposers POST through the same `SubmitPolicyAnalysis` path. Each chunk is persisted with its `analysis_mode` for audit provenance. -2. **Validate.** The gateway runs the prover (`openshell-prover`) on every - chunk regardless of mode. The prover builds a Z3 model from the merged - policy plus the sandbox's attached-provider credential set, then computes - the delta of findings between the current baseline and the merged policy. +2. **Build and validate the candidate.** The gateway first canonicalizes a + mechanistic proposal against the live effective policy. If an endpoint is + already governed by an inspected or provider-owned contract, the candidate + preserves that contract and adds only the proposed sandbox binary. Provider + rules are immutable inputs; the sandbox contribution is stored as an + overlay. The gateway then performs the same merge, policy validation, + provider composition, credential preflight, and prover evaluation that the + candidate would encounter when applied. Each chunk stores the resulting + effective candidate, its hashes, any application error, and a review token + derived from the candidate and its non-secret live inputs. 3. **Auto-approval gate (proposer-agnostic, opt-in).** Auto-approval fires only when *all three* conditions hold: (a) `proposal_approval_mode` resolves to `"auto"` — gateway scope wins, sandbox scope is the @@ -227,13 +233,12 @@ through the proposal loop instead of treating the denial as terminal. (`prover: no new findings`); and (c) the security notes recomputed from the chunk's current proposed rule are empty (see [Security-notes gate](#security-notes-gate)). Before merging, the gateway - reloads the stored chunk and reruns both checks on its current rule. This is - important after edits and mechanistic deduplication: the stored rule, not a - duplicate incoming payload or stale persisted analysis, controls the - decision. The recalculated prover verdict is decision-local rather than - persisted, so `validation_result` reads can still show the submit-time - verdict after an edit or deduplication. Decode, prover, or merge failures - leave the chunk pending. The audit event uses `CONFIG:APPROVED` and carries + reloads the stored chunk and recomputes its candidate from live policy, + provider, and credential inputs. If the review token is unchanged, the + gateway reuses the persisted prover result. If it changed, the gateway + persists the refreshed candidate and requires a fresh review instead of + applying it. Decode, prover, merge, provider-composition, or credential + failures leave the chunk pending with an application error. The audit event uses `CONFIG:APPROVED` and carries `auto=true`, `source=`, `prover_delta=empty`, and `resolved_from=` as unmapped fields, with message text `"auto-approved: no new prover findings"` — never `safe`. The opt-in gate @@ -259,6 +264,10 @@ through the proposal loop instead of treating the denial as terminal. policy. 6. **Escalation.** Anything else lands in `pending` for human review. +After any successful policy write, pending chunks already covered by the new +live effective policy are rejected as redundant. This keeps the review inbox +aligned with what the sandbox currently enforces. + ### Security-notes gate Separately from the prover, each chunk carries advisory `security_notes`. diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index fd0e585068..5147ee2831 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -6878,10 +6878,25 @@ pub async fn sandbox_draft_get( if !chunk.validation_result.is_empty() { println!( " {} {}", - "Validation:".dimmed(), + "Prover:".dimmed(), chunk.validation_result.cyan() ); } + if !chunk.application_error.is_empty() { + println!( + " {} {}", + "Application:".dimmed(), + chunk.application_error.red() + ); + } + if !chunk.candidate_effective_policy_hash.is_empty() { + println!( + " {} {}", + "Candidate:".dimmed(), + &chunk.candidate_effective_policy_hash + [..12.min(chunk.candidate_effective_policy_hash.len())] + ); + } if let Some(ref rule) = chunk.proposed_rule { println!(" {} {}", "Endpoints:".dimmed(), format_endpoints(rule)); @@ -6915,12 +6930,27 @@ pub async fn sandbox_draft_approve( tls: &TlsOptions, ) -> Result<()> { let mut client = grpc_client(server, tls).await?; + let review_token = client + .get_draft_policy(GetDraftPolicyRequest { + name: name.to_string(), + status_filter: String::new(), + workspace: workspace.to_string(), + }) + .await + .into_diagnostic()? + .into_inner() + .chunks + .into_iter() + .find(|chunk| chunk.id == chunk_id) + .ok_or_else(|| miette::miette!("draft chunk '{chunk_id}' not found"))? + .review_token; let response = client .approve_draft_chunk(ApproveDraftChunkRequest { name: name.to_string(), chunk_id: chunk_id.to_string(), workspace: workspace.to_string(), + review_token, }) .await .into_diagnostic()?; @@ -6971,12 +7001,29 @@ pub async fn sandbox_draft_approve_all( tls: &TlsOptions, ) -> Result<()> { let mut client = grpc_client(server, tls).await?; + let approvals = client + .get_draft_policy(GetDraftPolicyRequest { + name: name.to_string(), + status_filter: "pending".to_string(), + workspace: workspace.to_string(), + }) + .await + .into_diagnostic()? + .into_inner() + .chunks + .into_iter() + .map(|chunk| openshell_core::proto::DraftChunkApproval { + chunk_id: chunk.id, + review_token: chunk.review_token, + }) + .collect(); let response = client .approve_all_draft_chunks(ApproveAllDraftChunksRequest { name: name.to_string(), include_security_flagged, workspace: workspace.to_string(), + approvals, }) .await .into_diagnostic()?; diff --git a/crates/openshell-policy/src/lib.rs b/crates/openshell-policy/src/lib.rs index c8c5dd99c3..5be8173e3e 100644 --- a/crates/openshell-policy/src/lib.rs +++ b/crates/openshell-policy/src/lib.rs @@ -41,8 +41,8 @@ pub use l7_validate::{ validate_l7_endpoint_semantics, }; pub use merge::{ - PolicyMergeError, PolicyMergeOp, PolicyMergeResult, PolicyMergeWarning, generated_rule_name, - merge_policy, policy_covers_rule, + PolicyMergeError, PolicyMergeOp, PolicyMergeResult, PolicyMergeWarning, + canonicalize_advisor_add_rule, generated_rule_name, merge_policy, policy_covers_rule, }; pub use middleware::middleware_host_matches; pub use middleware::validate_json as validate_network_middleware_json; @@ -1206,6 +1206,12 @@ pub enum PolicyViolation { }, /// `credential_signing` and `request_body_credential_rewrite` are both set. CredentialSigningWithBodyRewrite { policy_name: String, host: String }, + /// An endpoint contains a deterministic L7 semantic error. + InvalidL7Endpoint { + policy_name: String, + endpoint_index: usize, + reason: String, + }, /// A middleware configuration is structurally invalid. InvalidMiddlewareConfig { name: String, reason: String }, /// Too many middleware configurations are attached to one policy. @@ -1340,6 +1346,14 @@ impl fmt::Display for PolicyViolation { and request_body_credential_rewrite set; these options are mutually exclusive" ) } + Self::InvalidL7Endpoint { + policy_name, + endpoint_index, + reason, + } => write!( + f, + "network policy '{policy_name}': endpoint {endpoint_index} has invalid L7 configuration: {reason}" + ), Self::InvalidMiddlewareConfig { name, reason } => { write!(f, "middleware config '{name}' is invalid: {reason}") } @@ -1475,7 +1489,7 @@ pub fn validate_sandbox_policy( } else { rule.name.clone() }; - for ep in &rule.endpoints { + for (endpoint_index, ep) in rule.endpoints.iter().enumerate() { let explicit_tcp = l7_validate::is_explicit_tcp_protocol(&ep.protocol); if ep.host.trim().is_empty() && explicit_tcp { violations.push(PolicyViolation::MissingTcpEndpointHost { @@ -1558,6 +1572,127 @@ pub fn validate_sandbox_policy( host: ep.host.clone(), }); } + + let rules_would_deny_all = !ep.rules.is_empty() + && ep.rules.iter().all(|rule| { + rule.allow.as_ref().is_none_or(|allow| { + allow.method.is_empty() + && allow.path.is_empty() + && allow.command.is_empty() + && allow.operation_type.is_empty() + && allow.operation_name.is_empty() + && allow.fields.is_empty() + && allow.params.is_empty() + }) + }); + let fields = L7EndpointFields { + protocol: &ep.protocol, + access: &ep.access, + has_rules: !ep.rules.is_empty(), + has_deny_rules: !ep.deny_rules.is_empty(), + rules_would_deny_all, + allow_all_known_mcp_methods: ep + .mcp + .as_ref() + .and_then(|mcp| mcp.allow_all_known_mcp_methods) + .unwrap_or(false), + }; + let mut l7_errors = validate_l7_endpoint_semantics(&fields); + let mut explicit_tcp_fields = Vec::new(); + if !ep.enforcement.is_empty() { + explicit_tcp_fields.push("enforcement"); + } + if !ep.path.is_empty() { + explicit_tcp_fields.push("path"); + } + if ep.allow_encoded_slash { + explicit_tcp_fields.push("allow_encoded_slash"); + } + if ep.websocket_credential_rewrite { + explicit_tcp_fields.push("websocket_credential_rewrite"); + } + if ep.request_body_credential_rewrite { + explicit_tcp_fields.push("request_body_credential_rewrite"); + } + if !ep.persisted_queries.is_empty() { + explicit_tcp_fields.push("persisted_queries"); + } + if !ep.graphql_persisted_queries.is_empty() { + explicit_tcp_fields.push("graphql_persisted_queries"); + } + if ep.graphql_max_body_bytes > 0 { + explicit_tcp_fields.push("graphql_max_body_bytes"); + } + if ep.json_rpc_max_body_bytes > 0 { + explicit_tcp_fields.push("json_rpc_max_body_bytes"); + } + if ep.mcp.is_some() { + explicit_tcp_fields.push("mcp"); + } + l7_errors.extend(validate_explicit_tcp_additional_fields( + &ep.protocol, + &explicit_tcp_fields, + )); + if !ep.path.is_empty() && !ep.path.starts_with('/') && ep.path != "**" { + l7_errors.push("path must start with '/' or be '**'".to_string()); + } + if !ep.persisted_queries.is_empty() + && !matches!(ep.persisted_queries.as_str(), "deny" | "allow_registered") + { + l7_errors.push(format!( + "persisted_queries must be 'deny' or 'allow_registered', got '{}'", + ep.persisted_queries + )); + } + if ep.protocol == "sql" && ep.enforcement == "enforce" { + l7_errors.push( + "SQL enforcement requires full SQL parsing; use enforcement: audit".to_string(), + ); + } + if ep.mcp.is_some() && ep.protocol != "mcp" { + l7_errors.push("mcp options are only valid for protocol mcp".to_string()); + } + if ep.protocol == "graphql" { + for (rule_index, rule) in ep.rules.iter().enumerate() { + let operation_type = rule + .allow + .as_ref() + .map(|allow| allow.operation_type.as_str()) + .unwrap_or_default(); + if !matches!(operation_type, "query" | "mutation" | "subscription") { + l7_errors.push(format!( + "rules[{rule_index}].allow.operation_type must be query, mutation, or subscription" + )); + } + } + for (rule_index, rule) in ep.deny_rules.iter().enumerate() { + if !matches!( + rule.operation_type.as_str(), + "query" | "mutation" | "subscription" + ) { + l7_errors.push(format!( + "deny_rules[{rule_index}].operation_type must be query, mutation, or subscription" + )); + } + } + for (key, operation) in &ep.graphql_persisted_queries { + if !matches!( + operation.operation_type.as_str(), + "query" | "mutation" | "subscription" + ) { + l7_errors.push(format!( + "graphql_persisted_queries[{key}].operation_type must be query, mutation, or subscription" + )); + } + } + } + violations.extend(l7_errors.into_iter().map(|reason| { + PolicyViolation::InvalidL7Endpoint { + policy_name: name.clone(), + endpoint_index, + reason, + } + })); } } diff --git a/crates/openshell-policy/src/merge.rs b/crates/openshell-policy/src/merge.rs index 75bc700975..12026bbea3 100644 --- a/crates/openshell-policy/src/merge.rs +++ b/crates/openshell-policy/src/merge.rs @@ -11,6 +11,97 @@ use crate::is_provider_rule_name; const DEFAULT_JSON_RPC_MAX_BODY_BYTES: u32 = 64 * 1024; +/// Rewrite an observation-only advisor rule against the live effective policy. +/// +/// A denial reports only `(host, port, binary)`. If that destination already +/// has one unambiguous endpoint contract, proposing a second generic L4 +/// endpoint loses inspection metadata and can make the effective policy +/// ambiguous. Preserve the existing contract instead. Sandbox-owned rules are +/// expanded in place; provider-owned rules remain immutable and are mirrored +/// into the requested sandbox-owned overlay. +pub fn canonicalize_advisor_add_rule( + base_policy: &SandboxPolicy, + effective_policy: &SandboxPolicy, + requested_rule_name: &str, + incoming_rule: &NetworkPolicyRule, +) -> Result<(String, NetworkPolicyRule), String> { + if incoming_rule.endpoints.len() != 1 || incoming_rule.binaries.is_empty() { + return Ok((requested_rule_name.to_string(), incoming_rule.clone())); + } + + let incoming_endpoint = &incoming_rule.endpoints[0]; + let incoming_ports = canonical_ports(incoming_endpoint); + if incoming_endpoint.host.trim().is_empty() || incoming_ports.len() != 1 { + return Ok((requested_rule_name.to_string(), incoming_rule.clone())); + } + let port = incoming_ports[0]; + let contracts = effective_policy + .network_policies + .values() + .flat_map(|rule| &rule.endpoints) + .filter(|endpoint| { + endpoint.host.eq_ignore_ascii_case(&incoming_endpoint.host) + && canonical_ports(endpoint).contains(&port) + }) + .cloned() + .map(|mut endpoint| { + // This marker is derived from provider/credential context by the + // gateway and must never be persisted from an advisor proposal. + endpoint.provider_credentialed = false; + // A denial observes one binary-to-port authorization. Preserve the + // existing inspection contract, but never copy sibling ports from + // a multi-port endpoint into the proposal. + endpoint.port = port; + endpoint.ports = vec![port]; + normalize_endpoint(&mut endpoint); + endpoint + }) + .collect::>(); + let mut unique_contracts = Vec::new(); + for contract in contracts { + if !unique_contracts.contains(&contract) { + unique_contracts.push(contract); + } + } + + let Some(contract) = unique_contracts.first().cloned() else { + return Ok((requested_rule_name.to_string(), incoming_rule.clone())); + }; + if unique_contracts.len() != 1 { + return Err(format!( + "cannot infer one existing endpoint contract for {}:{}", + incoming_endpoint.host, port + )); + } + + let mut sandbox_owners = base_policy + .network_policies + .iter() + .filter(|(name, _)| !is_provider_rule_name(name)) + .filter_map(|(name, rule)| { + rule.endpoints + .iter() + .any(|endpoint| { + let mut normalized = endpoint.clone(); + normalized.provider_credentialed = false; + normalize_endpoint(&mut normalized); + normalized == contract + }) + .then_some(name.clone()) + }) + .collect::>(); + sandbox_owners.sort(); + + let target_name = sandbox_owners + .first() + .cloned() + .unwrap_or_else(|| requested_rule_name.to_string()); + let mut canonical = incoming_rule.clone(); + canonical.name.clone_from(&target_name); + canonical.endpoints = vec![contract]; + Ok((target_name, canonical)) +} + #[derive(Debug, Clone, PartialEq)] pub enum PolicyMergeOp { AddRule { @@ -1994,7 +2085,8 @@ mod tests { use super::{ ANY_BINARY_SCOPE, DEFAULT_JSON_RPC_MAX_BODY_BYTES, PolicyMergeError, PolicyMergeOp, - PolicyMergeWarning, canonical_ports, generated_rule_name, merge_policy, policy_covers_rule, + PolicyMergeWarning, canonical_ports, canonicalize_advisor_add_rule, generated_rule_name, + merge_policy, policy_covers_rule, }; use crate::restrictive_default_policy; use openshell_core::proto::{ @@ -2046,6 +2138,154 @@ mod tests { } } + #[test] + fn canonicalize_advisor_expands_existing_inspected_rule_without_l7_downgrade() { + let mut existing_endpoint = endpoint("index.crates.io", 443); + existing_endpoint.protocol = "rest".to_string(); + existing_endpoint.enforcement = "enforce".to_string(); + existing_endpoint.access = "read-only".to_string(); + let existing = NetworkPolicyRule { + name: "cargo-registry".to_string(), + endpoints: vec![existing_endpoint.clone()], + binaries: vec![NetworkBinary { + path: "/usr/bin/cargo".to_string(), + ..Default::default() + }], + }; + let mut base = SandboxPolicy::default(); + base.network_policies + .insert("cargo_registry".to_string(), existing); + let effective = base.clone(); + let mut observed = endpoint("index.crates.io", 443); + observed.advisor_proposed = true; + let incoming = NetworkPolicyRule { + name: "allow_index_crates_io_443".to_string(), + endpoints: vec![observed], + binaries: vec![advisor_binary("/usr/bin/curl")], + }; + + let (rule_name, canonical) = canonicalize_advisor_add_rule( + &base, + &effective, + "allow_index_crates_io_443", + &incoming, + ) + .unwrap(); + + assert_eq!(rule_name, "cargo_registry"); + assert_eq!(canonical.endpoints, vec![existing_endpoint]); + assert_eq!(canonical.binaries[0].path, "/usr/bin/curl"); + #[allow(deprecated)] + { + assert!(canonical.binaries[0].harness); + } + } + + #[test] + fn canonicalize_advisor_mirrors_provider_contract_into_sandbox_overlay() { + let base = SandboxPolicy::default(); + let mut provider_endpoint = endpoint("api.example.com", 443); + provider_endpoint.protocol = "rest".to_string(); + provider_endpoint.enforcement = "enforce".to_string(); + provider_endpoint.access = "read-only".to_string(); + provider_endpoint.provider_credentialed = true; + let mut effective = SandboxPolicy::default(); + effective.network_policies.insert( + "_provider_example".to_string(), + NetworkPolicyRule { + name: "provider-example".to_string(), + endpoints: vec![provider_endpoint.clone()], + binaries: Vec::new(), + }, + ); + let incoming = NetworkPolicyRule { + name: "advisor_example".to_string(), + endpoints: vec![NetworkEndpoint { + host: "api.example.com".to_string(), + port: 443, + advisor_proposed: true, + ..Default::default() + }], + binaries: vec![advisor_binary("/usr/bin/curl")], + }; + + let (rule_name, canonical) = + canonicalize_advisor_add_rule(&base, &effective, "advisor_example", &incoming).unwrap(); + + assert_eq!(rule_name, "advisor_example"); + assert_eq!(canonical.endpoints[0].protocol, "rest"); + assert_eq!(canonical.endpoints[0].access, "read-only"); + assert!(!canonical.endpoints[0].provider_credentialed); + assert_eq!( + effective.network_policies["_provider_example"].endpoints[0], + provider_endpoint + ); + } + + #[test] + fn canonicalize_advisor_narrows_multi_port_contract_and_keeps_overlay() { + let mut existing_endpoint = endpoint("index.crates.io", 443); + existing_endpoint.port = 80; + existing_endpoint.ports = vec![80, 443]; + existing_endpoint.protocol = "rest".to_string(); + existing_endpoint.enforcement = "enforce".to_string(); + existing_endpoint.access = "read-only".to_string(); + let existing = NetworkPolicyRule { + name: "cargo-registry".to_string(), + endpoints: vec![existing_endpoint], + binaries: vec![binary("/usr/bin/cargo")], + }; + let mut base = SandboxPolicy::default(); + base.network_policies + .insert("cargo_registry".to_string(), existing); + let effective = base.clone(); + let incoming = NetworkPolicyRule { + name: "allow_index_crates_io_443".to_string(), + endpoints: vec![NetworkEndpoint { + host: "index.crates.io".to_string(), + port: 443, + advisor_proposed: true, + ..Default::default() + }], + binaries: vec![advisor_binary("/usr/bin/curl")], + }; + + let (rule_name, canonical) = canonicalize_advisor_add_rule( + &base, + &effective, + "allow_index_crates_io_443", + &incoming, + ) + .unwrap(); + + assert_eq!(rule_name, "allow_index_crates_io_443"); + assert_eq!(canonical.endpoints[0].ports, vec![443]); + assert_eq!(canonical.endpoints[0].protocol, "rest"); + assert_eq!(canonical.endpoints[0].access, "read-only"); + + let merged = merge_policy( + base, + &[PolicyMergeOp::AddRule { + rule_name, + rule: canonical, + }], + ) + .unwrap() + .policy; + assert_eq!( + merged.network_policies["cargo_registry"].endpoints[0].ports, + vec![80, 443] + ); + assert_eq!( + merged.network_policies["allow_index_crates_io_443"].endpoints[0].ports, + vec![443] + ); + assert_eq!( + merged.network_policies["allow_index_crates_io_443"].binaries[0].path, + "/usr/bin/curl" + ); + } + fn binary(path: &str) -> NetworkBinary { NetworkBinary { path: path.to_string(), diff --git a/crates/openshell-sandbox/src/mechanistic_mapper.rs b/crates/openshell-sandbox/src/mechanistic_mapper.rs index 8ee2fc37f9..9be5f8e438 100644 --- a/crates/openshell-sandbox/src/mechanistic_mapper.rs +++ b/crates/openshell-sandbox/src/mechanistic_mapper.rs @@ -228,6 +228,7 @@ pub fn generate_proposals(summaries: &[DenialSummary]) -> Vec { binary: binary.clone(), validation_result: String::new(), rejection_reason: String::new(), + ..Default::default() }); } diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 71dd11acb7..e9650a3560 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -59,8 +59,8 @@ use openshell_ocsf::{ ConfigStateChangeBuilder, OCSF_TARGET, OcsfEvent, SandboxContext, SeverityId, StateId, StatusId, }; use openshell_policy::{ - PolicyMergeOp, ProviderPolicyLayer, compose_effective_policy, merge_policy, - serialize_sandbox_policy, + PolicyMergeOp, ProviderPolicyLayer, canonicalize_advisor_add_rule, compose_effective_policy, + merge_policy, policy_covers_rule, serialize_sandbox_policy, strip_provider_rule_names, }; use openshell_prover::{ credentials::{Credential, CredentialSet}, @@ -448,6 +448,7 @@ fn summarize_draft_chunk_rule(chunk: &DraftChunkRecord) -> Result, + validation_result: String, + application_error: String, + review_token: String, +} + +impl ProposalEvaluation { + fn current_hash(&self) -> String { + deterministic_policy_hash(&self.current_effective_policy) + } + + fn candidate_hash(&self) -> String { + self.candidate_effective_policy + .as_ref() + .map(deterministic_policy_hash) + .unwrap_or_default() + } +} + +fn proposal_prover_result( + current_effective_policy: &ProtoSandboxPolicy, + candidate_effective_policy: &ProtoSandboxPolicy, + credentials: &CredentialSet, +) -> String { + let candidate_findings = match run_prover_findings(candidate_effective_policy, credentials) { + Ok(findings) => findings, + Err(error) => { + warn!(error = %error, "prover validation unavailable for candidate policy"); + return "validation unavailable".to_string(); + } + }; + let base_findings = match run_prover_findings(current_effective_policy, credentials) { + Ok(findings) => findings, + Err(error) => { + warn!(error = %error, "prover baseline run failed; treating baseline as empty"); + Vec::new() + } + }; + let new_findings = finding_delta(&base_findings, &candidate_findings); + if new_findings.is_empty() { + return "prover: no new findings".to_string(); + } + let mut out = format!( + "prover: {} new finding{}", + new_findings.len(), + if new_findings.len() == 1 { "" } else { "s" } + ); + for finding in &new_findings { + out.push_str("\n "); + out.push_str(&finding_shorthand(finding)); + } + out +} + +fn compute_proposal_review_token( + rule_name: &str, + rule: &NetworkPolicyRule, + current_effective_policy: &ProtoSandboxPolicy, + candidate_effective_policy: &ProtoSandboxPolicy, + credentials: &CredentialSet, +) -> String { + let mut hasher = Sha256::new(); + hasher.update(b"openshell-proposal-evaluator-v2\0"); + hasher.update(rule_name.as_bytes()); + hasher.update(canonical_rule_bytes(rule)); + hasher.update(deterministic_policy_hash(current_effective_policy).as_bytes()); + hasher.update(deterministic_policy_hash(candidate_effective_policy).as_bytes()); + + let mut credential_fingerprints = credentials + .credentials + .iter() + .map(|credential| { + let mut scopes = credential.scopes.clone(); + scopes.sort(); + let mut hosts = credential.target_hosts.clone(); + hosts.sort(); + format!( + "{}\0{}\0{}\0{}\0{}", + credential.name, + credential.cred_type, + credential.injected_via, + scopes.join("\0"), + hosts.join("\0") + ) + }) + .collect::>(); + credential_fingerprints.sort(); + for fingerprint in credential_fingerprints { + hasher.update(fingerprint.as_bytes()); + } + hex::encode(hasher.finalize()) +} + +fn compute_failed_proposal_evaluation_hash( + rule_name: &str, + rule: &NetworkPolicyRule, + current_effective_policy: &ProtoSandboxPolicy, + application_error: &str, +) -> String { + let mut hasher = Sha256::new(); + hasher.update(b"openshell-proposal-evaluator-v2-failed\0"); + hasher.update(rule_name.as_bytes()); + hasher.update(canonical_rule_bytes(rule)); + hasher.update(deterministic_policy_hash(current_effective_policy).as_bytes()); + hasher.update(application_error.as_bytes()); + hex::encode(hasher.finalize()) +} + +#[allow(clippy::too_many_arguments)] +fn evaluate_proposal_candidate( + base_policy: &ProtoSandboxPolicy, + current_effective_policy: &ProtoSandboxPolicy, + requested_rule_name: &str, + proposed_rule: &NetworkPolicyRule, + analysis_mode: &str, + credentials: &CredentialSet, + validation_context: PolicyMergeValidationContext<'_>, + reuse_validation_result: Option<&str>, +) -> ProposalEvaluation { + let canonical = if analysis_mode == "mechanistic" { + canonicalize_advisor_add_rule( + base_policy, + current_effective_policy, + requested_rule_name, + proposed_rule, + ) + } else { + Ok((requested_rule_name.to_string(), proposed_rule.clone())) + }; + let (rule_name, rule) = match canonical { + Ok(value) => value, + Err(error) => { + let application_error = format!("candidate canonicalization failed: {error}"); + return ProposalEvaluation { + rule_name: requested_rule_name.to_string(), + rule: proposed_rule.clone(), + current_effective_policy: current_effective_policy.clone(), + candidate_effective_policy: None, + validation_result: String::new(), + review_token: compute_failed_proposal_evaluation_hash( + requested_rule_name, + proposed_rule, + current_effective_policy, + &application_error, + ), + application_error, + }; + } + }; + + let operations = [PolicyMergeOp::AddRule { + rule_name: rule_name.clone(), + rule: rule.clone(), + }]; + let candidate_base = match merge_policy(base_policy.clone(), &operations) { + Ok(result) => result.policy, + Err(error) => { + let application_error = format!("merge failed: {}", one_line(&error.to_string())); + let review_token = compute_failed_proposal_evaluation_hash( + &rule_name, + &rule, + current_effective_policy, + &application_error, + ); + return ProposalEvaluation { + rule_name, + rule, + current_effective_policy: current_effective_policy.clone(), + candidate_effective_policy: None, + validation_result: String::new(), + review_token, + application_error, + }; + } + }; + + let validation = (|| -> Result { + validate_policy_safety(&candidate_base)?; + validate_candidate_effective_policy(&candidate_base, validation_context.provider_layers)?; + let mut effective = if validation_context.provider_layers.is_empty() { + candidate_base.clone() + } else { + compose_effective_policy(&candidate_base, validation_context.provider_layers) + }; + let bindings = policy_static_credential_endpoint_bindings(Some(&effective))?; + if let Some(context) = validation_context.credential_binding { + validate_operator_merged_credential_policy(&mut effective, &bindings, context)?; + } + Ok(effective) + })(); + + let candidate_effective_policy = match validation { + Ok(policy) => policy, + Err(error) => { + let application_error = format!("candidate invalid: {}", one_line(error.message())); + let review_token = compute_failed_proposal_evaluation_hash( + &rule_name, + &rule, + current_effective_policy, + &application_error, + ); + return ProposalEvaluation { + rule_name, + rule, + current_effective_policy: current_effective_policy.clone(), + candidate_effective_policy: None, + validation_result: String::new(), + review_token, + application_error, + }; + } + }; + let validation_result = reuse_validation_result.map_or_else( + || { + proposal_prover_result( + current_effective_policy, + &candidate_effective_policy, + credentials, + ) + }, + ToString::to_string, + ); + let application_error = if validation_result == "validation unavailable" { + "prover validation unavailable; proposal cannot be reviewed".to_string() + } else { + String::new() + }; + let review_token = if application_error.is_empty() { + compute_proposal_review_token( + &rule_name, + &rule, + current_effective_policy, + &candidate_effective_policy, + credentials, + ) + } else { + compute_failed_proposal_evaluation_hash( + &rule_name, + &rule, + current_effective_policy, + &application_error, + ) + }; + ProposalEvaluation { + rule_name, + rule, + current_effective_policy: current_effective_policy.clone(), + candidate_effective_policy: Some(candidate_effective_policy), + validation_result, + application_error, + review_token, + } +} + /// Run the prover end-to-end against a single policy with the given /// credential set. Returns the raw finding list, or a short error string /// identifying which infrastructure step failed. @@ -682,6 +942,68 @@ fn one_line(s: &str) -> String { .join("; ") } +async fn reconcile_pending_chunks_covered_by_policy( + state: &Arc, + sandbox_id: &str, + effective_policy: &ProtoSandboxPolicy, + policy_version: i64, +) -> Result { + let pending = state + .store + .list_draft_chunks(sandbox_id, Some("pending")) + .await + .map_err(|error| Status::internal(format!("list pending chunks failed: {error}")))?; + let mut reconciled = 0; + for chunk in pending { + let Some(rule) = decode_draft_chunk_rule(&chunk)? else { + continue; + }; + if !policy_covers_rule(effective_policy, &rule) { + continue; + } + let reason = format!("covered by active policy revision {policy_version}"); + if state + .store + .conditionally_reject_draft_chunk(&chunk.id, current_time_ms(), &reason) + .await + .map_err(|error| Status::internal(format!("reconcile covered chunk failed: {error}")))? + { + reconciled += 1; + } + } + if reconciled > 0 { + state.sandbox_watch_bus.notify(sandbox_id); + } + Ok(reconciled) +} + +async fn reconcile_pending_chunks_after_policy_change( + state: &Arc, + workspace: &str, + sandbox: &Sandbox, +) -> Result { + let catalog = state + .provider_profile_sources + .snapshot_catalog(state.store.as_ref(), workspace) + .await?; + let effective = current_effective_policy_for_sandbox( + state, + &catalog, + workspace, + sandbox, + sandbox.object_id(), + ) + .await?; + let version = state + .store + .get_latest_policy(sandbox.object_id()) + .await + .map_err(|error| Status::internal(format!("fetch latest policy failed: {error}")))? + .map_or(0, |record| record.version); + reconcile_pending_chunks_covered_by_policy(state, sandbox.object_id(), &effective, version) + .await +} + /// Auto-reject any pending chunks for the same sandbox that share the /// `(host, port, binary)` of the newly-submitted chunk. Mode-agnostic: the /// rule is "the latest submission for this endpoint wins; older pending @@ -914,13 +1236,198 @@ async fn resolve_proposal_approval_mode( Ok((false, "default")) } +fn apply_evaluation_to_chunk(chunk: &mut DraftChunkRecord, evaluation: &ProposalEvaluation) { + chunk.rule_name.clone_from(&evaluation.rule_name); + chunk.proposed_rule = evaluation.rule.encode_to_vec(); + chunk + .validation_result + .clone_from(&evaluation.validation_result); + chunk + .application_error + .clone_from(&evaluation.application_error); + chunk.review_token.clone_from(&evaluation.review_token); + chunk.current_effective_policy_hash = evaluation.current_hash(); + chunk.candidate_effective_policy_hash = evaluation.candidate_hash(); + chunk.current_effective_policy = Some(evaluation.current_effective_policy.clone()); + chunk + .candidate_effective_policy + .clone_from(&evaluation.candidate_effective_policy); + chunk.last_seen_ms = current_time_ms(); +} + +async fn evaluate_stored_chunk_against_live_inputs( + state: &Arc, + workspace: &str, + sandbox: &Sandbox, + chunk: &DraftChunkRecord, + reuse_validation_result: Option<&str>, +) -> Result { + let rule = decode_draft_chunk_rule(chunk)? + .ok_or_else(|| Status::failed_precondition("draft chunk has no proposed rule"))?; + let provider_names = sandbox + .spec + .as_ref() + .map(|spec| spec.providers.clone()) + .unwrap_or_default(); + let catalog = state + .provider_profile_sources + .snapshot_catalog(state.store.as_ref(), workspace) + .await?; + let current_effective = current_effective_policy_for_sandbox( + state, + &catalog, + workspace, + sandbox, + sandbox.object_id(), + ) + .await?; + let current_base = current_base_policy_for_sandbox(state.store.as_ref(), sandbox).await?; + let credentials = build_credential_set_for_sandbox_with_catalog( + state.store.as_ref(), + &catalog, + workspace, + &provider_names, + ) + .await?; + let merge_validation = sandbox_policy_merge_validation_data_with_catalog( + state, + workspace, + sandbox, + &provider_names, + &catalog, + ) + .await?; + let credential_binding_context = merge_validation.credential_binding_context(); + Ok(evaluate_proposal_candidate( + ¤t_base, + ¤t_effective, + &chunk.rule_name, + &rule, + "stored", + &credentials, + PolicyMergeValidationContext { + provider_layers: &merge_validation.provider_layers, + credential_binding: Some(&credential_binding_context), + }, + reuse_validation_result, + )) +} + +async fn persist_refreshed_evaluation( + state: &Arc, + chunk: &DraftChunkRecord, + evaluation: &ProposalEvaluation, +) -> Result { + let mut refreshed = chunk.clone(); + apply_evaluation_to_chunk(&mut refreshed, evaluation); + let updated = state + .store + .update_draft_chunk_evaluation(&refreshed) + .await + .map_err(|error| { + Status::internal(format!("persist proposal evaluation failed: {error}")) + })?; + if !updated { + return Err(Status::failed_precondition( + "proposal is no longer pending; refetch before deciding", + )); + } + state.sandbox_watch_bus.notify(&chunk.sandbox_id); + Ok(refreshed) +} + +async fn persist_pending_application_error( + state: &Arc, + chunk_id: &str, + error: &Status, +) { + let Ok(Some(mut chunk)) = state.store.get_draft_chunk(chunk_id).await else { + return; + }; + if chunk.status != "pending" { + return; + } + chunk.application_error = one_line(error.message()); + chunk.last_seen_ms = current_time_ms(); + if let Err(persist_error) = state.store.update_draft_chunk_evaluation(&chunk).await { + warn!( + chunk_id, + error = %persist_error, + "failed to persist proposal application error" + ); + return; + } + state.sandbox_watch_bus.notify(&chunk.sandbox_id); +} + +async fn clear_pending_application_error(state: &Arc, chunk_id: &str) { + let Ok(Some(mut chunk)) = state.store.get_draft_chunk(chunk_id).await else { + return; + }; + if chunk.status != "pending" || chunk.application_error.is_empty() { + return; + } + chunk.application_error.clear(); + chunk.last_seen_ms = current_time_ms(); + if let Err(error) = state.store.update_draft_chunk_evaluation(&chunk).await { + warn!(chunk_id, error = %error, "failed to clear proposal application error"); + } +} + +async fn require_current_proposal_evaluation( + state: &Arc, + workspace: &str, + sandbox: &Sandbox, + chunk: &DraftChunkRecord, + supplied_review_token: Option<&str>, +) -> Result { + if !chunk.review_token.is_empty() + && supplied_review_token.is_some_and(|token| token != chunk.review_token) + { + return Err(Status::failed_precondition( + "review token does not match the fetched proposal; refetch and review again", + )); + } + + let reuse = (!chunk.review_token.is_empty()).then_some(chunk.validation_result.as_str()); + let live = + evaluate_stored_chunk_against_live_inputs(state, workspace, sandbox, chunk, reuse).await?; + if !live.application_error.is_empty() { + persist_refreshed_evaluation(state, chunk, &live).await?; + return Err(Status::failed_precondition(format!( + "proposal is not applicable: {}", + live.application_error + ))); + } + + if chunk.review_token.is_empty() { + // Compatibility path for proposals stored before review tokens were + // introduced. Evaluate once, persist the token, and allow the legacy + // approval request to proceed against that exact live candidate. + let evaluated = + evaluate_stored_chunk_against_live_inputs(state, workspace, sandbox, chunk, None) + .await?; + persist_refreshed_evaluation(state, chunk, &evaluated).await?; + return Ok(evaluated); + } + + if live.review_token != chunk.review_token { + let refreshed = + evaluate_stored_chunk_against_live_inputs(state, workspace, sandbox, chunk, None) + .await?; + persist_refreshed_evaluation(state, chunk, &refreshed).await?; + return Err(Status::failed_precondition( + "proposal inputs changed; evaluation refreshed, refetch and review again", + )); + } + Ok(live) +} + struct AutoApproveChunkContext<'a> { sandbox: &'a Sandbox, workspace: &'a str, source: &'a str, resolved_from: &'a str, - current_policy: &'a ProtoSandboxPolicy, - credential_set: &'a CredentialSet, } async fn auto_approve_chunk( @@ -953,17 +1460,15 @@ async fn auto_approve_chunk( return Ok(()); } - // Mechanistic dedup may return an existing row whose proposed rule was - // edited after its original validation. Re-run the prover against that - // stored rule instead of trusting the incoming proposal's verdict. - let rule = decode_draft_chunk_rule(&chunk)? - .ok_or_else(|| Status::failed_precondition("draft chunk has no proposed rule"))?; - let validation_result = validation_result_for_agent_proposal( - context.current_policy.clone(), - &chunk.rule_name, - &rule, - context.credential_set, - ); + let live_evaluation = require_current_proposal_evaluation( + state, + context.workspace, + context.sandbox, + &chunk, + None, + ) + .await?; + let validation_result = live_evaluation.validation_result; if validation_result != "prover: no new findings" { info!( sandbox_id = %sandbox_id, @@ -1005,7 +1510,7 @@ async fn auto_approve_chunk( ) .await?; let credential_binding_context = merge_validation.credential_binding_context(); - let (version, hash) = merge_chunk_into_policy_with_validation( + let merge_result = merge_chunk_into_policy_with_validation( state.store.as_ref(), sandbox_id, context.workspace, @@ -1015,10 +1520,18 @@ async fn auto_approve_chunk( credential_binding: Some(&credential_binding_context), }, ) - .await?; + .await; + let (version, hash) = match merge_result { + Ok(result) => result, + Err(status) => { + persist_pending_application_error(state, chunk_id, &status).await; + return Err(status); + } + }; let chunk_summary = summarize_draft_chunk_rule(&chunk)?; let now_ms = current_time_ms(); + clear_pending_application_error(state, chunk_id).await; state .store .update_draft_chunk_status(chunk_id, "approved", Some(now_ms), None) @@ -1026,6 +1539,16 @@ async fn auto_approve_chunk( .map_err(|e| Status::internal(format!("update chunk status failed: {e}")))?; state.sandbox_watch_bus.notify(sandbox_id); + if let Err(error) = + reconcile_pending_chunks_after_policy_change(state, context.workspace, context.sandbox) + .await + { + warn!( + sandbox_id, + error = %error, + "failed to reconcile pending policy proposals after auto-approval" + ); + } let source_label = if context.source.is_empty() { "unspecified" @@ -3008,6 +3531,7 @@ async fn handle_update_config_inner( provider_layers: &merge_validation.provider_layers, credential_binding: Some(&credential_binding_context), }, + None, Some(&atomic_context), ) .await?; @@ -3086,7 +3610,7 @@ async fn handle_update_config_inner( .ok_or_else(|| Status::internal("sandbox has no spec"))?; if sandbox_caller { - if openshell_policy::strip_provider_rule_names(&mut new_policy) { + if strip_provider_rule_names(&mut new_policy) { debug!( sandbox_id = %sandbox_id, "UpdateConfig: stripped provider-derived policy entries from sandbox sync" @@ -3681,7 +4205,22 @@ pub(super) async fn handle_submit_policy_analysis( &sandbox_id, ) .await?; - + let active_policy_version = state + .store + .get_latest_policy(&sandbox_id) + .await + .map_err(|error| Status::internal(format!("fetch latest policy failed: {error}")))? + .map_or(0, |record| record.version); + reconcile_pending_chunks_covered_by_policy( + state, + &sandbox_id, + ¤t_policy, + active_policy_version, + ) + .await?; + let current_base_policy = + current_base_policy_for_sandbox(state.store.as_ref(), &sandbox).await?; + // Auto-approval is an opt-in behavior, sourced from the settings model // (sandbox or gateway scope) so it can be flipped on a running sandbox // and managed fleet-wide. Default (no setting, or any value other than @@ -3707,6 +4246,19 @@ pub(super) async fn handle_submit_policy_analysis( &provider_names_for_creds, ) .await?; + let merge_validation = sandbox_policy_merge_validation_data_with_catalog( + state, + &workspace, + &sandbox, + &provider_names_for_creds, + &provider_profile_catalog, + ) + .await?; + let credential_binding_context = merge_validation.credential_binding_context(); + let proposal_validation_context = PolicyMergeValidationContext { + provider_layers: &merge_validation.provider_layers, + credential_binding: Some(&credential_binding_context), + }; let current_version = state .store @@ -3747,14 +4299,98 @@ pub(super) async fn handle_submit_policy_analysis( continue; } - let now_ms = current_time_ms(); - let proposed_rule_bytes = chunk - .proposed_rule + let rule_ref = chunk.proposed_rule.as_ref().expect("checked above"); + let incoming_observation_key = rule_ref.endpoints.first().and_then(|endpoint| { + rule_ref.binaries.first().map(|binary| { + ( + endpoint.host.to_lowercase(), + endpoint.port as i32, + binary.path.clone(), + ) + }) + }); + let existing_mechanistic = if req.analysis_mode == "mechanistic" { + let chunks = state + .store + .list_draft_chunks(&sandbox_id, None) + .await + .map_err(|error| { + Status::internal(format!("list draft chunks for dedup failed: {error}")) + })?; + incoming_observation_key + .as_ref() + .and_then(|(host, port, binary)| { + chunks.into_iter().find(|existing| { + existing.host == *host + && existing.port == *port + && existing.binary == *binary + }) + }) + } else { + None + }; + // A duplicate observation updates the existing pending decision; it + // must not replace an edited rule with the new mapper payload. Build + // and hash the candidate from the stored rule, while retaining the + // incoming observation key for the persistence-layer dedup lookup. + let existing_pending_rule = existing_mechanistic .as_ref() - .map(Message::encode_to_vec) - .unwrap_or_default(); + .filter(|existing| existing.status == "pending") + .map(decode_draft_chunk_rule) + .transpose()? + .flatten(); + let evaluation_rule = existing_pending_rule.as_ref().unwrap_or(rule_ref); + let evaluation_rule_name = existing_mechanistic + .as_ref() + .filter(|existing| existing.status == "pending") + .map_or(chunk.rule_name.as_str(), |existing| { + existing.rule_name.as_str() + }); + let evaluation_mode = if existing_pending_rule.is_some() { + "stored" + } else { + req.analysis_mode.as_str() + }; + let reusable_validation = existing_mechanistic + .as_ref() + .filter(|existing| existing.status == "pending" && !existing.review_token.is_empty()) + .map(|existing| existing.validation_result.as_str()); + let mut evaluation = evaluate_proposal_candidate( + ¤t_base_policy, + ¤t_policy, + evaluation_rule_name, + evaluation_rule, + evaluation_mode, + &credential_set, + proposal_validation_context, + reusable_validation, + ); + if let Some(existing) = &existing_mechanistic + && !existing.review_token.is_empty() + && evaluation.review_token != existing.review_token + { + evaluation = evaluate_proposal_candidate( + ¤t_base_policy, + ¤t_policy, + evaluation_rule_name, + evaluation_rule, + evaluation_mode, + &credential_set, + proposal_validation_context, + None, + ); + } + if req.analysis_mode != "mechanistic" && !evaluation.application_error.is_empty() { + rejected += 1; + rejection_reasons.push(format!( + "chunk '{}': {}", + chunk.rule_name, evaluation.application_error + )); + continue; + } - let rule_ref = chunk.proposed_rule.as_ref().expect("checked above"); + let now_ms = current_time_ms(); + let proposed_rule_bytes = evaluation.rule.encode_to_vec(); let (ep_host, ep_port) = rule_ref .endpoints .first() @@ -3766,17 +4402,6 @@ pub(super) async fn handle_submit_policy_analysis( .map(|b| b.path.clone()) .unwrap_or_default(); - // The prover runs on every proposal regardless of `analysis_mode`. - // Source provenance (mechanistic vs agent_authored) is preserved in - // OCSF audit fields, but the safety decision is grounded in the - // merged-policy consequence, not the author — proposer-agnostic. - let validation_result = validation_result_for_agent_proposal( - current_policy.clone(), - &chunk.rule_name, - rule_ref, - &credential_set, - ); - let record = DraftChunkRecord { // The handler proposes an id; the store may swap it for an // existing row's id on dedup. Always trust `effective_id` for @@ -3785,10 +4410,10 @@ pub(super) async fn handle_submit_policy_analysis( sandbox_id: sandbox_id.clone(), draft_version, status: "pending".to_string(), - rule_name: chunk.rule_name.clone(), + rule_name: evaluation.rule_name.clone(), proposed_rule: proposed_rule_bytes, rationale: chunk.rationale.clone(), - security_notes: generate_security_notes(rule_ref), + security_notes: generate_security_notes(&evaluation.rule), confidence: f64::from(chunk.confidence.clamp(0.0, 1.0)), created_at_ms: now_ms, decided_at_ms: None, @@ -3806,8 +4431,14 @@ pub(super) async fn handle_submit_policy_analysis( } else { now_ms }, - validation_result: validation_result.clone(), + validation_result: evaluation.validation_result.clone(), rejection_reason: String::new(), + application_error: evaluation.application_error.clone(), + review_token: evaluation.review_token.clone(), + current_effective_policy_hash: evaluation.current_hash(), + candidate_effective_policy_hash: evaluation.candidate_hash(), + current_effective_policy: Some(evaluation.current_effective_policy.clone()), + candidate_effective_policy: evaluation.candidate_effective_policy.clone(), }; // Mechanistic mode dedups N denials targeting the same endpoint // into one chunk. All other modes (agent-authored proposals, future @@ -3821,6 +4452,14 @@ pub(super) async fn handle_submit_policy_analysis( .put_draft_chunk(&record, dedup_key.as_deref(), &workspace) .await .map_err(|e| Status::internal(format!("persist draft chunk failed: {e}")))?; + if effective_id != record.id + && existing_mechanistic.as_ref().is_some_and(|existing| { + existing.status == "pending" && existing.review_token != evaluation.review_token + }) + && let Some(existing) = existing_mechanistic.as_ref() + { + persist_refreshed_evaluation(state, existing, &evaluation).await?; + } accepted += 1; // Implicit supersede: any other pending chunk for the same @@ -3876,12 +4515,11 @@ pub(super) async fn handle_submit_policy_analysis( workspace: &workspace, source: &req.analysis_mode, resolved_from, - current_policy: ¤t_policy, - credential_set: &credential_set, }, ) .await { + persist_pending_application_error(state, &effective_id, &err).await; warn!( chunk_id = %effective_id, sandbox_id = %sandbox_id, @@ -4046,6 +4684,15 @@ async fn handle_approve_draft_chunk_inner( ))); } + require_current_proposal_evaluation( + state, + &workspace, + &sandbox, + &chunk, + Some(&req.review_token), + ) + .await?; + info!( sandbox_id = %sandbox_id, chunk_id = %req.chunk_id, @@ -4065,7 +4712,7 @@ async fn handle_approve_draft_chunk_inner( let merge_validation = sandbox_policy_merge_validation_data(state, &workspace, &sandbox, provider_names).await?; let credential_binding_context = merge_validation.credential_binding_context(); - let (version, hash) = merge_chunk_into_policy_with_validation( + let merge_result = merge_chunk_into_policy_with_validation( state.store.as_ref(), &sandbox_id, &workspace, @@ -4075,10 +4722,18 @@ async fn handle_approve_draft_chunk_inner( credential_binding: Some(&credential_binding_context), }, ) - .await?; + .await; + let (version, hash) = match merge_result { + Ok(result) => result, + Err(status) => { + persist_pending_application_error(state, &req.chunk_id, &status).await; + return Err(status); + } + }; let chunk_summary = summarize_draft_chunk_rule(&chunk)?; let now_ms = current_time_ms(); + clear_pending_application_error(state, &req.chunk_id).await; state .store .update_draft_chunk_status(&req.chunk_id, "approved", Some(now_ms), None) @@ -4086,6 +4741,15 @@ async fn handle_approve_draft_chunk_inner( .map_err(|e| Status::internal(format!("update chunk status failed: {e}")))?; state.sandbox_watch_bus.notify(&sandbox_id); + if let Err(error) = + reconcile_pending_chunks_after_policy_change(state, &workspace, &sandbox).await + { + warn!( + sandbox_id, + error = %error, + "failed to reconcile pending policy proposals after approval" + ); + } emit_gateway_policy_audit_log( &sandbox_id, sandbox.object_name(), @@ -4277,6 +4941,31 @@ async fn handle_approve_all_draft_chunks_inner( return Err(Status::failed_precondition("no pending chunks to approve")); } + let chunks_to_approve = if req.approvals.is_empty() { + pending_chunks.clone() + } else { + let by_id = pending_chunks + .iter() + .map(|chunk| (chunk.id.as_str(), chunk)) + .collect::>(); + let mut selected = Vec::with_capacity(req.approvals.len()); + for approval in &req.approvals { + let chunk = by_id.get(approval.chunk_id.as_str()).ok_or_else(|| { + Status::failed_precondition(format!( + "chunk '{}' is not pending; refetch before bulk approval", + approval.chunk_id + )) + })?; + selected.push((*chunk).clone()); + } + selected + }; + let review_tokens = req + .approvals + .iter() + .map(|approval| (approval.chunk_id.as_str(), approval.review_token.as_str())) + .collect::>(); + info!( sandbox_id = %sandbox_id, pending_count = pending_chunks.len(), @@ -4284,10 +4973,7 @@ async fn handle_approve_all_draft_chunks_inner( "ApproveAllDraftChunks: starting bulk approval" ); - let mut chunks_approved: u32 = 0; let mut chunks_skipped: u32 = 0; - let mut last_version: i64 = 0; - let mut last_hash = String::new(); let provider_names = sandbox .spec .as_ref() @@ -4300,40 +4986,10 @@ async fn handle_approve_all_draft_chunks_inner( provider_layers: &merge_validation.provider_layers, credential_binding: Some(&credential_binding_context), }; - let mut bulk_candidate = - current_base_policy_for_sandbox(state.store.as_ref(), &sandbox).await?; - for chunk in &pending_chunks { - let security_notes = current_draft_chunk_security_notes(chunk)?; - if !req.include_security_flagged && !security_notes.is_empty() { - continue; - } - let rule = NetworkPolicyRule::decode(chunk.proposed_rule.as_slice()) - .map_err(|e| Status::internal(format!("decode proposed_rule failed: {e}")))?; - let operations = [PolicyMergeOp::AddRule { - rule_name: chunk.rule_name.clone(), - rule, - }]; - validate_merge_operations_for_server(&operations)?; - bulk_candidate = merge_policy(bulk_candidate, &operations) - .map_err(map_policy_merge_error)? - .policy; - validate_policy_safety(&bulk_candidate)?; - validate_candidate_effective_policy(&bulk_candidate, &merge_validation.provider_layers)?; - let mut prefix_effective_policy = if merge_validation.provider_layers.is_empty() { - bulk_candidate.clone() - } else { - compose_effective_policy(&bulk_candidate, &merge_validation.provider_layers) - }; - let prefix_bindings = - policy_static_credential_endpoint_bindings(Some(&prefix_effective_policy))?; - validate_operator_merged_credential_policy( - &mut prefix_effective_policy, - &prefix_bindings, - &credential_binding_context, - )?; - } - - for chunk in &pending_chunks { + let mut staged_policy = current_base_policy_for_sandbox(state.store.as_ref(), &sandbox).await?; + let mut expected_effective_hash: Option = None; + let mut accepted = Vec::<(DraftChunkRecord, PolicyMergeOp, String)>::new(); + for chunk in &chunks_to_approve { let security_notes = current_draft_chunk_security_notes(chunk)?; if !req.include_security_flagged && !security_notes.is_empty() { info!( @@ -4347,28 +5003,151 @@ async fn handle_approve_all_draft_chunks_inner( continue; } + let supplied_token = review_tokens.get(chunk.id.as_str()).copied().unwrap_or(""); + let evaluation = match require_current_proposal_evaluation( + state, + &workspace, + &sandbox, + chunk, + Some(supplied_token), + ) + .await + { + Ok(evaluation) => evaluation, + Err(status) if status.code() == tonic::Code::FailedPrecondition => { + info!( + sandbox_id = %sandbox_id, + chunk_id = %chunk.id, + reason = %status.message(), + "ApproveAllDraftChunks: skipping stale or invalid candidate" + ); + chunks_skipped += 1; + continue; + } + Err(status) => return Err(status), + }; + + let current_hash = evaluation.current_hash(); + if let Some(expected_hash) = expected_effective_hash.as_deref() + && current_hash != expected_hash + { + return Err(Status::failed_precondition( + "proposal inputs changed during bulk review; refetch and review again", + )); + } + info!( sandbox_id = %sandbox_id, chunk_id = %chunk.id, rule_name = %chunk.rule_name, host = %chunk.host, port = chunk.port, - "ApproveAllDraftChunks: merging chunk" + "ApproveAllDraftChunks: staging chunk" ); - let (version, hash) = merge_chunk_into_policy_with_validation( + let operation = PolicyMergeOp::AddRule { + rule_name: evaluation.rule_name, + rule: evaluation.rule, + }; + let candidate = match stage_validated_merge_operation( + &staged_policy, + &operation, + merge_validation_context, + ) { + Ok(candidate) => candidate, + Err(status) => { + persist_pending_application_error(state, &chunk.id, &status).await; + info!( + sandbox_id = %sandbox_id, + chunk_id = %chunk.id, + reason = %status.message(), + "ApproveAllDraftChunks: skipping chunk that conflicts with the staged batch" + ); + chunks_skipped += 1; + continue; + } + }; + let chunk_summary = summarize_draft_chunk_rule(chunk)?; + if expected_effective_hash.is_none() { + expected_effective_hash = Some(current_hash); + } + staged_policy = candidate; + accepted.push((chunk.clone(), operation, chunk_summary)); + } + + let (last_version, last_hash) = if accepted.is_empty() { + (0, String::new()) + } else { + // Rebuild the staged candidate from fresh live inputs immediately before + // persistence. This reuses each unchanged chunk's cached prover result; + // it does not silently bind the request to a newly refreshed token. + for (chunk, _, _) in &accepted { + let supplied_token = review_tokens.get(chunk.id.as_str()).copied().unwrap_or(""); + require_current_proposal_evaluation( + state, + &workspace, + &sandbox, + chunk, + Some(supplied_token), + ) + .await?; + } + + let final_merge_validation = + sandbox_policy_merge_validation_data(state, &workspace, &sandbox, provider_names) + .await?; + let final_credential_binding_context = final_merge_validation.credential_binding_context(); + let final_validation_context = PolicyMergeValidationContext { + provider_layers: &final_merge_validation.provider_layers, + credential_binding: Some(&final_credential_binding_context), + }; + let final_base = current_base_policy_for_sandbox(state.store.as_ref(), &sandbox).await?; + let mut rebuilt_policy = final_base.clone(); + for (_, operation, _) in &accepted { + rebuilt_policy = stage_validated_merge_operation( + &rebuilt_policy, + operation, + final_validation_context, + )?; + } + if deterministic_policy_hash(&rebuilt_policy) != deterministic_policy_hash(&staged_policy) { + return Err(Status::failed_precondition( + "proposal inputs changed during bulk review; refetch and review again", + )); + } + + let operations = accepted + .iter() + .map(|(_, operation, _)| operation.clone()) + .collect::>(); + let expected_hash = expected_effective_hash.as_deref().ok_or_else(|| { + Status::failed_precondition("bulk approval has no reviewed policy snapshot") + })?; + match apply_merge_operations_with_retry( state.store.as_ref(), &sandbox_id, &workspace, - chunk, - merge_validation_context, + Some(&final_base), + &operations, + final_validation_context, + Some(expected_hash), + None, ) - .await?; - last_version = version; - last_hash = hash; - let chunk_summary = summarize_draft_chunk_rule(chunk)?; + .await + { + Ok((version, hash, _)) => (version, hash), + Err(status) => { + for (chunk, _, _) in &accepted { + persist_pending_application_error(state, &chunk.id, &status).await; + } + return Err(status); + } + } + }; + for (chunk, _, chunk_summary) in &accepted { let now_ms = current_time_ms(); + clear_pending_application_error(state, &chunk.id).await; state .store .update_draft_chunk_status(&chunk.id, "approved", Some(now_ms), None) @@ -4380,14 +5159,23 @@ async fn handle_approve_all_draft_chunks_inner( sandbox.object_name(), "approved", format!("gateway approved draft chunk {}: {chunk_summary}", chunk.id), - version, + last_version, &last_hash, ); - chunks_approved += 1; emit_sandbox_policy_update_success(); } + let chunks_approved = u32::try_from(accepted.len()).unwrap_or(u32::MAX); state.sandbox_watch_bus.notify(&sandbox_id); + if let Err(error) = + reconcile_pending_chunks_after_policy_change(state, &workspace, &sandbox).await + { + warn!( + sandbox_id, + error = %error, + "failed to reconcile pending policy proposals after bulk approval" + ); + } emit_gateway_policy_audit_log( &sandbox_id, sandbox.object_name(), @@ -4470,12 +5258,17 @@ pub(super) async fn handle_edit_draft_chunk( ))); } - let rule_bytes = proposed_rule.encode_to_vec(); - state - .store - .update_draft_chunk_rule(&req.chunk_id, &rule_bytes) - .await - .map_err(|e| Status::internal(format!("update chunk rule failed: {e}")))?; + let mut edited_chunk = chunk.clone(); + edited_chunk.proposed_rule = proposed_rule.encode_to_vec(); + edited_chunk.review_token.clear(); + edited_chunk.validation_result.clear(); + edited_chunk.application_error.clear(); + edited_chunk.current_effective_policy = None; + edited_chunk.candidate_effective_policy = None; + let evaluation = + evaluate_stored_chunk_against_live_inputs(state, &workspace, &sandbox, &edited_chunk, None) + .await?; + persist_refreshed_evaluation(state, &edited_chunk, &evaluation).await?; info!( chunk_id = %req.chunk_id, @@ -4715,41 +5508,201 @@ pub(super) async fn handle_get_draft_history( // Policy helper functions // --------------------------------------------------------------------------- -/// Compute a deterministic SHA-256 hash of a `SandboxPolicy`. -fn deterministic_policy_hash(policy: &ProtoSandboxPolicy) -> String { - let mut hasher = Sha256::new(); - hasher.update(policy.version.to_le_bytes()); - if let Some(fs) = &policy.filesystem { - hasher.update(fs.encode_to_vec()); +fn append_canonical_bytes(out: &mut Vec, value: &[u8]) { + out.extend_from_slice( + &u64::try_from(value.len()) + .expect("canonical value length fits in u64") + .to_le_bytes(), + ); + out.extend_from_slice(value); +} + +fn append_canonical_message(out: &mut Vec, value: &M) { + append_canonical_bytes(out, &value.encode_to_vec()); +} + +fn append_sorted_message_map( + out: &mut Vec, + label: &[u8], + values: &HashMap, +) { + append_canonical_bytes(out, label); + let mut entries = values.iter().collect::>(); + entries.sort_by_key(|(key, _)| key.as_str()); + out.extend_from_slice( + &u64::try_from(entries.len()) + .expect("canonical map length fits in u64") + .to_le_bytes(), + ); + for (key, value) in entries { + append_canonical_bytes(out, key.as_bytes()); + append_canonical_message(out, value); + } +} + +/// Encode a policy rule without depending on randomized protobuf map order. +fn canonical_rule_bytes(rule: &NetworkPolicyRule) -> Vec { + let mut map_free = rule.clone(); + for endpoint in &mut map_free.endpoints { + endpoint.graphql_persisted_queries.clear(); + for rule in &mut endpoint.rules { + if let Some(allow) = &mut rule.allow { + allow.query.clear(); + allow.params.clear(); + } + } + for deny in &mut endpoint.deny_rules { + deny.query.clear(); + deny.params.clear(); + } } - if let Some(ll) = &policy.landlock { - hasher.update(ll.encode_to_vec()); + + let mut out = Vec::new(); + append_canonical_message(&mut out, &map_free); + for (endpoint_index, endpoint) in rule.endpoints.iter().enumerate() { + out.extend_from_slice( + &u64::try_from(endpoint_index) + .expect("endpoint index fits in u64") + .to_le_bytes(), + ); + append_sorted_message_map( + &mut out, + b"graphql_persisted_queries", + &endpoint.graphql_persisted_queries, + ); + for (rule_index, rule) in endpoint.rules.iter().enumerate() { + out.extend_from_slice( + &u64::try_from(rule_index) + .expect("rule index fits in u64") + .to_le_bytes(), + ); + if let Some(allow) = &rule.allow { + append_sorted_message_map(&mut out, b"allow_query", &allow.query); + append_sorted_message_map(&mut out, b"allow_params", &allow.params); + } + } + for (rule_index, deny) in endpoint.deny_rules.iter().enumerate() { + out.extend_from_slice( + &u64::try_from(rule_index) + .expect("deny-rule index fits in u64") + .to_le_bytes(), + ); + append_sorted_message_map(&mut out, b"deny_query", &deny.query); + append_sorted_message_map(&mut out, b"deny_params", &deny.params); + } } - if let Some(p) = &policy.process { - hasher.update(p.encode_to_vec()); + out +} + +fn canonical_struct_bytes(value: &prost_types::Struct) -> Vec { + let mut out = Vec::new(); + let mut fields = value.fields.iter().collect::>(); + fields.sort_by_key(|(key, _)| key.as_str()); + out.extend_from_slice( + &u64::try_from(fields.len()) + .expect("struct field count fits in u64") + .to_le_bytes(), + ); + for (key, value) in fields { + append_canonical_bytes(&mut out, key.as_bytes()); + append_canonical_bytes(&mut out, &canonical_value_bytes(value)); } - let mut entries: Vec<_> = policy.network_policies.iter().collect(); - entries.sort_by_key(|(k, _)| k.as_str()); - for (key, value) in entries { - hasher.update(key.as_bytes()); - hasher.update(value.encode_to_vec()); - } - if !policy.network_middlewares.is_empty() { - hasher.update(b"network_middlewares"); - let mut entries: Vec<_> = policy.network_middlewares.iter().collect(); - entries.sort_by_key(|(name, _)| name.as_str()); - for (name, middleware) in entries { - hasher.update(name.as_bytes()); - let encoded = middleware.encode_to_vec(); - hasher.update( - u64::try_from(encoded.len()) - .expect("protobuf payload length fits in u64") + out +} + +fn canonical_value_bytes(value: &prost_types::Value) -> Vec { + use prost_types::value::Kind; + + let mut out = Vec::new(); + match &value.kind { + None => out.push(0), + Some(Kind::NullValue(value)) => { + out.push(1); + out.extend_from_slice(&value.to_le_bytes()); + } + Some(Kind::NumberValue(value)) => { + out.push(2); + out.extend_from_slice(&value.to_bits().to_le_bytes()); + } + Some(Kind::StringValue(value)) => { + out.push(3); + append_canonical_bytes(&mut out, value.as_bytes()); + } + Some(Kind::BoolValue(value)) => { + out.push(4); + out.push(u8::from(*value)); + } + Some(Kind::StructValue(value)) => { + out.push(5); + append_canonical_bytes(&mut out, &canonical_struct_bytes(value)); + } + Some(Kind::ListValue(value)) => { + out.push(6); + out.extend_from_slice( + &u64::try_from(value.values.len()) + .expect("list length fits in u64") .to_le_bytes(), ); - hasher.update(encoded); + for item in &value.values { + append_canonical_bytes(&mut out, &canonical_value_bytes(item)); + } } } - hex::encode(hasher.finalize()) + out +} + +fn canonical_middleware_bytes( + middleware: &openshell_core::proto::NetworkMiddlewareConfig, +) -> Vec { + let mut map_free = middleware.clone(); + map_free.config = None; + let mut out = Vec::new(); + append_canonical_message(&mut out, &map_free); + if let Some(config) = &middleware.config { + append_canonical_bytes(&mut out, &canonical_struct_bytes(config)); + } + out +} + +fn canonical_policy_bytes(policy: &ProtoSandboxPolicy) -> Vec { + let mut map_free = policy.clone(); + map_free.network_policies.clear(); + map_free.network_middlewares.clear(); + let mut out = Vec::new(); + append_canonical_message(&mut out, &map_free); + + let mut policy_entries = policy.network_policies.iter().collect::>(); + policy_entries.sort_by_key(|(key, _)| key.as_str()); + append_canonical_bytes(&mut out, b"network_policies"); + out.extend_from_slice( + &u64::try_from(policy_entries.len()) + .expect("policy count fits in u64") + .to_le_bytes(), + ); + for (key, rule) in policy_entries { + append_canonical_bytes(&mut out, key.as_bytes()); + append_canonical_bytes(&mut out, &canonical_rule_bytes(rule)); + } + + let mut middleware_entries = policy.network_middlewares.iter().collect::>(); + middleware_entries.sort_by_key(|(key, _)| key.as_str()); + append_canonical_bytes(&mut out, b"network_middlewares"); + out.extend_from_slice( + &u64::try_from(middleware_entries.len()) + .expect("middleware count fits in u64") + .to_le_bytes(), + ); + for (key, middleware) in middleware_entries { + append_canonical_bytes(&mut out, key.as_bytes()); + append_canonical_bytes(&mut out, &canonical_middleware_bytes(middleware)); + } + out +} + +/// Compute a deterministic SHA-256 hash of a `SandboxPolicy`, recursively +/// sorting every protobuf map while preserving repeated-field order. +fn deterministic_policy_hash(policy: &ProtoSandboxPolicy) -> String { + hex::encode(Sha256::digest(canonical_policy_bytes(policy))) } /// Compute a fingerprint for the effective sandbox configuration. @@ -4861,6 +5814,12 @@ fn draft_chunk_record_to_proto(record: &DraftChunkRecord) -> Result Result { - let global_settings = load_global_settings(state.store.as_ref()).await?; - let composition_enabled = provider_policy_composition_enabled_in(&global_settings)?; let catalog = state .provider_profile_sources .snapshot_catalog(state.store.as_ref(), workspace) .await?; + sandbox_policy_merge_validation_data_with_catalog( + state, + workspace, + sandbox, + provider_names, + &catalog, + ) + .await +} + +async fn sandbox_policy_merge_validation_data_with_catalog( + state: &ServerState, + workspace: &str, + sandbox: &Sandbox, + provider_names: &[String], + catalog: &EffectiveProviderProfileCatalog, +) -> Result { + let global_settings = load_global_settings(state.store.as_ref()).await?; + let composition_enabled = provider_policy_composition_enabled_in(&global_settings)?; let ProviderPolicyContext { layers, credentialed_scopes, endpointless_provider_names, } = provider_policy_context_with_catalog( state.store.as_ref(), - &catalog, + catalog, workspace, provider_names, ) @@ -5315,7 +6291,7 @@ async fn sandbox_policy_merge_validation_data( .await?; Ok(SandboxPolicyMergeValidationData { provider_layers, - catalog, + catalog: catalog.clone(), records, credentialed_scopes, endpointless_provider_names, @@ -5350,6 +6326,30 @@ fn validate_operator_merged_credential_policy( validate_uninspected_credentialed_endpoints(effective_policy) } +fn stage_validated_merge_operation( + current_policy: &ProtoSandboxPolicy, + operation: &PolicyMergeOp, + validation_context: PolicyMergeValidationContext<'_>, +) -> Result { + validate_merge_operations_for_server(std::slice::from_ref(operation))?; + let merged = merge_policy(current_policy.clone(), std::slice::from_ref(operation)) + .map_err(map_policy_merge_error)?; + let candidate = merged.policy; + validate_policy_safety(&candidate)?; + validate_candidate_effective_policy(&candidate, validation_context.provider_layers)?; + let mut effective = if validation_context.provider_layers.is_empty() { + candidate.clone() + } else { + compose_effective_policy(&candidate, validation_context.provider_layers) + }; + let bindings = policy_static_credential_endpoint_bindings(Some(&effective))?; + if let Some(context) = validation_context.credential_binding { + validate_operator_merged_credential_policy(&mut effective, &bindings, context)?; + } + Ok(candidate) +} + +#[allow(clippy::too_many_arguments)] async fn apply_merge_operations_with_retry( store: &Store, sandbox_id: &str, @@ -5357,6 +6357,7 @@ async fn apply_merge_operations_with_retry( baseline_policy: Option<&ProtoSandboxPolicy>, operations: &[PolicyMergeOp], validation_context: PolicyMergeValidationContext<'_>, + expected_current_effective_hash: Option<&str>, atomic_context: Option<&AtomicPolicyWriteContext<'_>>, ) -> Result<(i64, String, Option), Status> { let provider_layers = validation_context.provider_layers; @@ -5373,8 +6374,29 @@ async fn apply_merge_operations_with_retry( baseline_policy.cloned().unwrap_or_default() }; - let merged = merge_policy(current_policy, operations).map_err(map_policy_merge_error)?; - let new_policy = merged.policy; + if let Some(expected_hash) = expected_current_effective_hash { + let mut current_effective = if provider_layers.is_empty() { + current_policy.clone() + } else { + compose_effective_policy(¤t_policy, provider_layers) + }; + let bindings = policy_static_credential_endpoint_bindings(Some(¤t_effective))?; + if let Some(context) = validation_context.credential_binding { + validate_operator_merged_credential_policy( + &mut current_effective, + &bindings, + context, + )?; + } + if deterministic_policy_hash(¤t_effective) != expected_hash { + return Err(Status::failed_precondition( + "proposal inputs changed before persistence; refetch and review again", + )); + } + } + + let merged = merge_policy(current_policy, operations).map_err(map_policy_merge_error)?; + let new_policy = merged.policy; let hash = deterministic_policy_hash(&new_policy); if let Some(baseline_policy) = baseline_policy { @@ -5459,6 +6481,11 @@ async fn apply_merge_operations_with_retry( } Err(e) => { if e.is_unique_violation_on("objects_version_uq") { + if expected_current_effective_hash.is_some() { + return Err(Status::failed_precondition( + "policy changed while applying reviewed proposal; refetch and review again", + )); + } warn!( sandbox_id = %sandbox_id, attempt, @@ -5495,13 +6522,20 @@ async fn merge_chunk_into_policy_with_validation( rule, }]; validate_merge_operations_for_server(&operations)?; + let mut baseline_policy = chunk.current_effective_policy.clone(); + if let Some(policy) = &mut baseline_policy { + strip_provider_rule_names(policy); + clear_provider_credentialed_markers(policy); + } apply_merge_operations_with_retry( store, sandbox_id, workspace, - None, + baseline_policy.as_ref(), &operations, validation_context, + (!chunk.current_effective_policy_hash.is_empty()) + .then_some(chunk.current_effective_policy_hash.as_str()), None, ) .await @@ -5549,6 +6583,7 @@ async fn remove_chunk_from_policy( credential_binding: None, }, None, + None, ) .await .map(|(version, hash, _)| (version, hash)) @@ -8172,6 +9207,7 @@ mod tests { credential_binding: None, }, None, + None, ) .await .expect_err("ambiguous merge must fail before persistence"); @@ -9992,6 +11028,345 @@ mod tests { .unwrap(); } + #[tokio::test] + async fn approve_all_applies_multiple_independent_chunks_and_reuses_cached_validation() { + let state = test_server_state().await; + let sandbox_id = "sb-approve-all-multiple"; + let sandbox_name = "approve-all-multiple"; + state + .store + .put_message(&test_sandbox( + sandbox_id, + sandbox_name, + ProtoSandboxPolicy::default(), + vec![], + )) + .await + .unwrap(); + + let submit = handle_submit_policy_analysis( + &state, + with_user(Request::new(SubmitPolicyAnalysisRequest { + name: sandbox_name.to_string(), + analysis_mode: "agent_authored".to_string(), + proposed_chunks: [ + ("alpha", "alpha.example.com", "/usr/bin/curl"), + ("beta", "beta.example.com", "/usr/bin/wget"), + ] + .into_iter() + .map(|(name, host, binary)| PolicyChunk { + rule_name: name.to_string(), + proposed_rule: Some(NetworkPolicyRule { + name: name.to_string(), + endpoints: vec![NetworkEndpoint { + host: host.to_string(), + port: 443, + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: binary.to_string(), + ..Default::default() + }], + }), + ..Default::default() + }) + .collect(), + ..Default::default() + })), + ) + .await + .unwrap() + .into_inner(); + assert_eq!(submit.accepted_chunk_ids.len(), 2); + + let mut chunks = Vec::new(); + for (index, chunk_id) in submit.accepted_chunk_ids.iter().enumerate() { + let mut chunk = state + .store + .get_draft_chunk(chunk_id) + .await + .unwrap() + .unwrap(); + chunk.validation_result = format!("prover: cached sentinel {index}"); + assert!( + state + .store + .update_draft_chunk_evaluation(&chunk) + .await + .unwrap() + ); + chunks.push(chunk); + } + + let approved = handle_approve_all_draft_chunks( + &state, + with_user(Request::new(ApproveAllDraftChunksRequest { + name: sandbox_name.to_string(), + workspace: "default".to_string(), + approvals: chunks + .iter() + .map(|chunk| openshell_core::proto::DraftChunkApproval { + chunk_id: chunk.id.clone(), + review_token: chunk.review_token.clone(), + }) + .collect(), + ..Default::default() + })), + ) + .await + .unwrap() + .into_inner(); + + assert_eq!(approved.chunks_approved, 2); + assert_eq!(approved.chunks_skipped, 0); + assert_eq!(approved.policy_version, 1); + let revision = state + .store + .get_latest_policy(sandbox_id) + .await + .unwrap() + .unwrap(); + let policy = ProtoSandboxPolicy::decode(revision.policy_payload.as_slice()).unwrap(); + assert!(policy.network_policies.contains_key("alpha")); + assert!(policy.network_policies.contains_key("beta")); + for (index, chunk) in chunks.iter().enumerate() { + let stored = state + .store + .get_draft_chunk(&chunk.id) + .await + .unwrap() + .unwrap(); + assert_eq!(stored.status, "approved"); + assert_eq!( + stored.validation_result, + format!("prover: cached sentinel {index}") + ); + } + } + + #[tokio::test] + async fn approve_all_skips_later_batch_conflict_and_applies_compatible_prefix() { + let state = test_server_state().await; + let sandbox_id = "sb-approve-all-conflict"; + let sandbox_name = "approve-all-conflict"; + state + .store + .put_message(&test_sandbox( + sandbox_id, + sandbox_name, + ProtoSandboxPolicy::default(), + vec![], + )) + .await + .unwrap(); + + let submit = handle_submit_policy_analysis( + &state, + with_user(Request::new(SubmitPolicyAnalysisRequest { + name: sandbox_name.to_string(), + analysis_mode: "agent_authored".to_string(), + proposed_chunks: vec![ + PolicyChunk { + rule_name: "inspected".to_string(), + proposed_rule: Some(NetworkPolicyRule { + name: "inspected".to_string(), + endpoints: vec![NetworkEndpoint { + host: "shared.example.com".to_string(), + port: 443, + protocol: "rest".to_string(), + enforcement: "enforce".to_string(), + access: "read-only".to_string(), + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: "/usr/bin/curl".to_string(), + ..Default::default() + }], + }), + ..Default::default() + }, + PolicyChunk { + rule_name: "passthrough".to_string(), + proposed_rule: Some(NetworkPolicyRule { + name: "passthrough".to_string(), + endpoints: vec![NetworkEndpoint { + host: "shared.example.com".to_string(), + port: 443, + advisor_proposed: true, + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: "/usr/bin/wget".to_string(), + ..Default::default() + }], + }), + ..Default::default() + }, + ], + ..Default::default() + })), + ) + .await + .unwrap() + .into_inner(); + assert_eq!(submit.accepted_chunk_ids.len(), 2); + let chunks = futures::future::try_join_all( + submit + .accepted_chunk_ids + .iter() + .map(|id| state.store.get_draft_chunk(id)), + ) + .await + .unwrap() + .into_iter() + .map(Option::unwrap) + .collect::>(); + + let approved = handle_approve_all_draft_chunks( + &state, + with_user(Request::new(ApproveAllDraftChunksRequest { + name: sandbox_name.to_string(), + workspace: "default".to_string(), + approvals: chunks + .iter() + .map(|chunk| openshell_core::proto::DraftChunkApproval { + chunk_id: chunk.id.clone(), + review_token: chunk.review_token.clone(), + }) + .collect(), + ..Default::default() + })), + ) + .await + .unwrap() + .into_inner(); + + assert_eq!(approved.chunks_approved, 1); + assert_eq!(approved.chunks_skipped, 1); + assert_eq!( + state + .store + .get_draft_chunk(&chunks[0].id) + .await + .unwrap() + .unwrap() + .status, + "approved" + ); + let skipped = state + .store + .get_draft_chunk(&chunks[1].id) + .await + .unwrap() + .unwrap(); + assert_eq!(skipped.status, "pending"); + assert!(!skipped.application_error.is_empty()); + let revision = state + .store + .get_latest_policy(sandbox_id) + .await + .unwrap() + .unwrap(); + let policy = ProtoSandboxPolicy::decode(revision.policy_payload.as_slice()).unwrap(); + assert!(policy.network_policies.contains_key("inspected")); + assert!(!policy.network_policies.contains_key("passthrough")); + } + + #[tokio::test] + async fn reviewed_batch_operations_stale_snapshot_apply_nothing() { + let state = test_server_state().await; + let sandbox_id = "sb-approve-all-stale"; + let sandbox_name = "approve-all-stale"; + state + .store + .put_message(&test_sandbox( + sandbox_id, + sandbox_name, + ProtoSandboxPolicy::default(), + vec![], + )) + .await + .unwrap(); + + let reviewed_policy = ProtoSandboxPolicy::default(); + let reviewed_hash = deterministic_policy_hash(&reviewed_policy); + let changed_policy = test_policy_with_rule("concurrent", "concurrent.example.com"); + let changed_hash = deterministic_policy_hash(&changed_policy); + state + .store + .put_policy_revision( + "concurrent-policy", + sandbox_id, + "default", + 1, + &changed_policy.encode_to_vec(), + &changed_hash, + ) + .await + .unwrap(); + + let operations = [ + PolicyMergeOp::AddRule { + rule_name: "alpha".to_string(), + rule: NetworkPolicyRule { + name: "alpha".to_string(), + endpoints: vec![NetworkEndpoint { + host: "alpha.example.com".to_string(), + port: 443, + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: "/usr/bin/curl".to_string(), + ..Default::default() + }], + }, + }, + PolicyMergeOp::AddRule { + rule_name: "beta".to_string(), + rule: NetworkPolicyRule { + name: "beta".to_string(), + endpoints: vec![NetworkEndpoint { + host: "beta.example.com".to_string(), + port: 443, + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: "/usr/bin/wget".to_string(), + ..Default::default() + }], + }, + }, + ]; + let error = apply_merge_operations_with_retry( + state.store.as_ref(), + sandbox_id, + "default", + Some(&reviewed_policy), + &operations, + PolicyMergeValidationContext { + provider_layers: &[], + credential_binding: None, + }, + Some(&reviewed_hash), + None, + ) + .await + .expect_err("a stale reviewed snapshot must reject the complete batch"); + + assert_eq!(error.code(), Code::FailedPrecondition); + let latest = state + .store + .get_latest_policy(sandbox_id) + .await + .unwrap() + .unwrap(); + assert_eq!(latest.policy_hash, changed_hash); + let policy = ProtoSandboxPolicy::decode(latest.policy_payload.as_slice()).unwrap(); + assert!(policy.network_policies.contains_key("concurrent")); + assert!(!policy.network_policies.contains_key("alpha")); + assert!(!policy.network_policies.contains_key("beta")); + } + #[tokio::test] async fn approve_all_skips_private_allowed_ips_unless_included() { let state = test_server_state().await; @@ -10054,6 +11429,10 @@ mod tests { name: sandbox_name.to_string(), include_security_flagged: false, workspace: "default".to_string(), + approvals: vec![openshell_core::proto::DraftChunkApproval { + chunk_id: chunk_id.clone(), + review_token: chunk.review_token.clone(), + }], })), ) .await @@ -10078,6 +11457,10 @@ mod tests { name: sandbox_name.to_string(), include_security_flagged: true, workspace: "default".to_string(), + approvals: vec![openshell_core::proto::DraftChunkApproval { + chunk_id: chunk_id.clone(), + review_token: chunk.review_token.clone(), + }], })), ) .await @@ -10188,6 +11571,7 @@ mod tests { name: sandbox_name.to_string(), include_security_flagged: false, workspace: "default".to_string(), + ..Default::default() })), ) .await @@ -10253,6 +11637,7 @@ mod tests { name: sandbox_name.to_string(), include_security_flagged: false, workspace: "default".to_string(), + ..Default::default() })), ) .await @@ -10581,6 +11966,7 @@ mod tests { // manual approve path this test exercises. assert_eq!(draft_policy.chunks[0].status, "pending"); let chunk_id = draft_policy.chunks[0].id.clone(); + let review_token = draft_policy.chunks[0].review_token.clone(); let approve = handle_approve_draft_chunk( &state, @@ -10588,6 +11974,7 @@ mod tests { name: sandbox_name.clone(), chunk_id: chunk_id.clone(), workspace: "default".to_string(), + review_token, }), ) .await @@ -10988,6 +12375,7 @@ mod tests { endpoints: vec![NetworkEndpoint { host: "api.github.com".to_string(), port: 443, + allow_uninspected_credentials: true, ..Default::default() }], binaries: vec![NetworkBinary { @@ -11148,52 +12536,483 @@ mod tests { ); } - /// Auto-approval is **proposer-agnostic**: a mechanistic proposal whose - /// prover delta is empty auto-approves the same way an agent-authored one - /// does. Source provenance is preserved in the audit trail (OCSF event - /// `source=mechanistic`) but does not change the safety decision. + /// Auto-approval is **proposer-agnostic**: a mechanistic proposal whose + /// prover delta is empty auto-approves the same way an agent-authored one + /// does. Source provenance is preserved in the audit trail (OCSF event + /// `source=mechanistic`) but does not change the safety decision. + #[tokio::test] + async fn mechanistic_proposal_with_empty_delta_also_auto_approves() { + use openshell_core::proto::{ + FilesystemPolicy, NetworkBinary, NetworkEndpoint, SandboxPhase, SandboxPolicy, + SandboxSpec, + }; + + let state = test_server_state().await; + let sandbox_name = "mechanistic-clean".to_string(); + let mut sandbox = Sandbox { + metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: "sb-mechanistic-clean".to_string(), + name: sandbox_name.clone(), + created_at_ms: 1_000_000, + labels: std::collections::HashMap::new(), + resource_version: 0, + annotations: HashMap::new(), + workspace: "default".to_string(), + deletion_timestamp_ms: 0, + }), + spec: Some(SandboxSpec { + policy: Some(SandboxPolicy { + version: 1, + filesystem: Some(FilesystemPolicy { + read_write: vec!["/sandbox".to_string()], + ..Default::default() + }), + ..Default::default() + }), + // No providers → no credential in scope for the proposed host. + ..Default::default() + }), + ..Default::default() + }; + sandbox.set_phase(SandboxPhase::Ready as i32); + state.store.put_message(&sandbox).await.unwrap(); + // Opt into auto mode via the settings model to test the + // proposer-agnostic gate. + seed_sandbox_approval_mode(&state, &sandbox_name, "auto").await; + + let proposed_rule = NetworkPolicyRule { + name: "anon_l4".to_string(), + endpoints: vec![NetworkEndpoint { + host: "example.com".to_string(), + port: 443, + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: "/usr/bin/curl".to_string(), + ..Default::default() + }], + }; + + handle_submit_policy_analysis( + &state, + with_user(Request::new(SubmitPolicyAnalysisRequest { + name: sandbox_name.clone(), + analysis_mode: "mechanistic".to_string(), + proposed_chunks: vec![PolicyChunk { + rule_name: "anon_l4".to_string(), + proposed_rule: Some(proposed_rule), + rationale: "Allow /usr/bin/curl to connect to example.com:443.".to_string(), + ..Default::default() + }], + ..Default::default() + })), + ) + .await + .unwrap(); + + let draft = handle_get_draft_policy( + &state, + with_user(Request::new(GetDraftPolicyRequest { + name: sandbox_name, + status_filter: String::new(), + workspace: "default".to_string(), + })), + ) + .await + .unwrap() + .into_inner(); + let verdict = &draft.chunks[0].validation_result; + assert_eq!(verdict, "prover: no new findings"); + assert_eq!( + draft.chunks[0].status, "approved", + "empty-delta mechanistic proposal under auto mode must auto-approve \ + (proposer-agnostic); got status: {}", + draft.chunks[0].status + ); + } + + #[tokio::test] + async fn mechanistic_existing_multi_port_rest_endpoint_auto_approves_narrow_overlay() { + use openshell_core::proto::{ + NetworkBinary, NetworkEndpoint, NetworkPolicyRule, SandboxPhase, SandboxPolicy, + SandboxSpec, + }; + + let state = test_server_state().await; + let sandbox_name = "mechanistic-existing-rest".to_string(); + let mut base_policy = SandboxPolicy::default(); + base_policy.network_policies.insert( + "cargo_registry".to_string(), + NetworkPolicyRule { + name: "cargo-registry".to_string(), + endpoints: vec![NetworkEndpoint { + host: "index.crates.io".to_string(), + port: 80, + ports: vec![80, 443], + protocol: "rest".to_string(), + enforcement: "enforce".to_string(), + access: "read-only".to_string(), + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: "/usr/bin/cargo".to_string(), + ..Default::default() + }], + }, + ); + let mut sandbox = Sandbox { + metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: "sb-mechanistic-existing-rest".to_string(), + name: sandbox_name.clone(), + created_at_ms: 1_000_000, + labels: HashMap::new(), + resource_version: 0, + annotations: HashMap::new(), + workspace: "default".to_string(), + deletion_timestamp_ms: 0, + }), + spec: Some(SandboxSpec { + policy: Some(base_policy), + ..Default::default() + }), + ..Default::default() + }; + sandbox.set_phase(SandboxPhase::Ready as i32); + state.store.put_message(&sandbox).await.unwrap(); + seed_sandbox_approval_mode(&state, &sandbox_name, "auto").await; + + let mut advisor_binary = NetworkBinary { + path: "/usr/bin/curl".to_string(), + ..Default::default() + }; + #[allow(deprecated)] + { + advisor_binary.harness = true; + } + handle_submit_policy_analysis( + &state, + with_user(Request::new(SubmitPolicyAnalysisRequest { + name: sandbox_name.clone(), + analysis_mode: "mechanistic".to_string(), + proposed_chunks: vec![PolicyChunk { + rule_name: "allow_index_crates_io_443".to_string(), + proposed_rule: Some(NetworkPolicyRule { + name: "allow_index_crates_io_443".to_string(), + endpoints: vec![NetworkEndpoint { + host: "index.crates.io".to_string(), + port: 443, + advisor_proposed: true, + ..Default::default() + }], + binaries: vec![advisor_binary], + }), + hit_count: 1, + ..Default::default() + }], + ..Default::default() + })), + ) + .await + .unwrap(); + + let draft = handle_get_draft_policy( + &state, + with_user(Request::new(GetDraftPolicyRequest { + name: sandbox_name, + workspace: "default".to_string(), + ..Default::default() + })), + ) + .await + .unwrap() + .into_inner(); + let chunk = &draft.chunks[0]; + assert_eq!( + chunk.status, "approved", + "application error: {}; prover: {}", + chunk.application_error, chunk.validation_result + ); + assert_eq!(chunk.rule_name, "allow_index_crates_io_443"); + assert_eq!(chunk.validation_result, "prover: no new findings"); + assert!(chunk.application_error.is_empty()); + assert!(!chunk.review_token.is_empty()); + let canonical = chunk.proposed_rule.as_ref().unwrap(); + assert_eq!(canonical.endpoints[0].protocol, "rest"); + assert_eq!(canonical.endpoints[0].access, "read-only"); + assert!(!canonical.endpoints[0].advisor_proposed); + + let revision = state + .store + .get_latest_policy("sb-mechanistic-existing-rest") + .await + .unwrap() + .expect("auto approval persisted a policy revision"); + let applied = ProtoSandboxPolicy::decode(revision.policy_payload.as_slice()).unwrap(); + let cargo_rule = &applied.network_policies["cargo_registry"]; + assert_eq!(cargo_rule.endpoints.len(), 1); + assert_eq!(cargo_rule.endpoints[0].ports, vec![80, 443]); + assert_eq!(cargo_rule.endpoints[0].protocol, "rest"); + assert_eq!(cargo_rule.endpoints[0].access, "read-only"); + assert_eq!(cargo_rule.binaries.len(), 1); + assert_eq!(cargo_rule.binaries[0].path, "/usr/bin/cargo"); + let curl_rule = &applied.network_policies["allow_index_crates_io_443"]; + assert_eq!(curl_rule.endpoints.len(), 1); + assert_eq!(curl_rule.endpoints[0].ports, vec![443]); + assert_eq!(curl_rule.endpoints[0].protocol, "rest"); + assert_eq!(curl_rule.endpoints[0].access, "read-only"); + assert_eq!(curl_rule.binaries.len(), 1); + assert_eq!(curl_rule.binaries[0].path, "/usr/bin/curl"); + } + + #[tokio::test] + async fn malformed_graphql_candidate_is_rejected_before_reviewer_inbox() { + use openshell_core::proto::{ + L7Allow, L7Rule, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, SandboxPhase, + SandboxPolicy, SandboxSpec, + }; + + let state = test_server_state().await; + let sandbox_name = "invalid-graphql-preflight".to_string(); + let mut sandbox = Sandbox { + metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { + id: "sb-invalid-graphql-preflight".to_string(), + name: sandbox_name.clone(), + created_at_ms: 1_000_000, + workspace: "default".to_string(), + ..Default::default() + }), + spec: Some(SandboxSpec { + policy: Some(SandboxPolicy::default()), + ..Default::default() + }), + ..Default::default() + }; + sandbox.set_phase(SandboxPhase::Ready as i32); + state.store.put_message(&sandbox).await.unwrap(); + + let response = handle_submit_policy_analysis( + &state, + with_user(Request::new(SubmitPolicyAnalysisRequest { + name: sandbox_name.clone(), + analysis_mode: "agent_authored".to_string(), + proposed_chunks: vec![PolicyChunk { + rule_name: "bad_graphql".to_string(), + proposed_rule: Some(NetworkPolicyRule { + name: "bad-graphql".to_string(), + endpoints: vec![NetworkEndpoint { + host: "api.example.com".to_string(), + port: 443, + protocol: "graphql".to_string(), + enforcement: "enforce".to_string(), + rules: vec![L7Rule { + allow: Some(L7Allow { + // Runtime requires an operation type for + // GraphQL rules; this intentionally omits it. + fields: vec!["viewer".to_string()], + ..Default::default() + }), + }], + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: "/usr/bin/curl".to_string(), + ..Default::default() + }], + }), + ..Default::default() + }], + ..Default::default() + })), + ) + .await + .unwrap() + .into_inner(); + + assert_eq!(response.accepted_chunks, 0); + assert_eq!(response.rejected_chunks, 1); + assert!(response.rejection_reasons[0].contains("operation_type")); + let draft = handle_get_draft_policy( + &state, + with_user(Request::new(GetDraftPolicyRequest { + name: sandbox_name, + workspace: "default".to_string(), + ..Default::default() + })), + ) + .await + .unwrap() + .into_inner(); + assert!(draft.chunks.is_empty()); + } + #[tokio::test] - async fn mechanistic_proposal_with_empty_delta_also_auto_approves() { + async fn changed_policy_inputs_refresh_token_and_require_fresh_review() { use openshell_core::proto::{ - FilesystemPolicy, NetworkBinary, NetworkEndpoint, SandboxPhase, SandboxPolicy, + NetworkBinary, NetworkEndpoint, NetworkPolicyRule, SandboxPhase, SandboxPolicy, SandboxSpec, }; let state = test_server_state().await; - let sandbox_name = "mechanistic-clean".to_string(); + let sandbox_name = "stale-review-token".to_string(); + let sandbox_id = "sb-stale-review-token"; let mut sandbox = Sandbox { metadata: Some(openshell_core::proto::datamodel::v1::ObjectMeta { - id: "sb-mechanistic-clean".to_string(), + id: sandbox_id.to_string(), name: sandbox_name.clone(), created_at_ms: 1_000_000, - labels: std::collections::HashMap::new(), - resource_version: 0, - annotations: HashMap::new(), workspace: "default".to_string(), - deletion_timestamp_ms: 0, + ..Default::default() }), spec: Some(SandboxSpec { - policy: Some(SandboxPolicy { - version: 1, - filesystem: Some(FilesystemPolicy { - read_write: vec!["/sandbox".to_string()], - ..Default::default() - }), - ..Default::default() - }), - // No providers → no credential in scope for the proposed host. + policy: Some(SandboxPolicy::default()), ..Default::default() }), ..Default::default() }; sandbox.set_phase(SandboxPhase::Ready as i32); state.store.put_message(&sandbox).await.unwrap(); - // Opt into auto mode via the settings model to test the - // proposer-agnostic gate. - seed_sandbox_approval_mode(&state, &sandbox_name, "auto").await; - let proposed_rule = NetworkPolicyRule { - name: "anon_l4".to_string(), + let submit = handle_submit_policy_analysis( + &state, + with_user(Request::new(SubmitPolicyAnalysisRequest { + name: sandbox_name.clone(), + analysis_mode: "agent_authored".to_string(), + proposed_chunks: vec![PolicyChunk { + rule_name: "example".to_string(), + proposed_rule: Some(NetworkPolicyRule { + name: "example".to_string(), + endpoints: vec![NetworkEndpoint { + host: "example.com".to_string(), + port: 443, + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: "/usr/bin/curl".to_string(), + ..Default::default() + }], + }), + ..Default::default() + }], + ..Default::default() + })), + ) + .await + .unwrap() + .into_inner(); + let chunk_id = &submit.accepted_chunk_ids[0]; + let before = state + .store + .get_draft_chunk(chunk_id) + .await + .unwrap() + .unwrap(); + assert!(!before.review_token.is_empty()); + + // The cached verdict is deliberately replaced with a sentinel while + // retaining its token. With unchanged inputs, evaluation must carry + // that value through instead of invoking the prover again. + let mut cached = before.clone(); + cached.validation_result = "prover: cached sentinel".to_string(); + assert!( + state + .store + .update_draft_chunk_evaluation(&cached) + .await + .unwrap() + ); + let unchanged = require_current_proposal_evaluation( + &state, + "default", + &sandbox, + &cached, + Some(&cached.review_token), + ) + .await + .unwrap(); + assert_eq!(unchanged.validation_result, "prover: cached sentinel"); + + let mut changed_base = SandboxPolicy::default(); + changed_base.network_policies.insert( + "unrelated".to_string(), + NetworkPolicyRule { + name: "unrelated".to_string(), + endpoints: vec![NetworkEndpoint { + host: "unrelated.example".to_string(), + port: 443, + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: "/usr/bin/wget".to_string(), + ..Default::default() + }], + }, + ); + let changed_hash = deterministic_policy_hash(&changed_base); + state + .store + .put_policy_revision( + "stale-token-policy", + sandbox_id, + "default", + 1, + &changed_base.encode_to_vec(), + &changed_hash, + ) + .await + .unwrap(); + + let error = handle_approve_draft_chunk( + &state, + with_user(Request::new(ApproveDraftChunkRequest { + name: sandbox_name, + chunk_id: chunk_id.clone(), + workspace: "default".to_string(), + review_token: before.review_token.clone(), + })), + ) + .await + .expect_err("changed base policy must invalidate the reviewed token"); + assert_eq!(error.code(), Code::FailedPrecondition); + assert!(error.message().contains("inputs changed")); + let refreshed = state + .store + .get_draft_chunk(chunk_id) + .await + .unwrap() + .unwrap(); + assert_eq!(refreshed.status, "pending"); + assert_ne!(refreshed.review_token, before.review_token); + assert_eq!( + state + .store + .get_latest_policy(sandbox_id) + .await + .unwrap() + .unwrap() + .policy_hash, + changed_hash + ); + } + + #[tokio::test] + async fn policy_change_reconciles_pending_chunk_already_covered_by_live_policy() { + use openshell_core::proto::{NetworkBinary, NetworkEndpoint, NetworkPolicyRule}; + + let state = test_server_state().await; + let sandbox_id = "sb-covered-pending"; + let sandbox_name = "covered-pending"; + state + .store + .put_message(&test_sandbox( + sandbox_id, + sandbox_name, + ProtoSandboxPolicy::default(), + Vec::new(), + )) + .await + .unwrap(); + let rule = NetworkPolicyRule { + name: "example".to_string(), endpoints: vec![NetworkEndpoint { host: "example.com".to_string(), port: 443, @@ -11204,42 +13023,42 @@ mod tests { ..Default::default() }], }; - - handle_submit_policy_analysis( + let submit = handle_submit_policy_analysis( &state, with_user(Request::new(SubmitPolicyAnalysisRequest { - name: sandbox_name.clone(), - analysis_mode: "mechanistic".to_string(), + name: sandbox_name.to_string(), + analysis_mode: "agent_authored".to_string(), proposed_chunks: vec![PolicyChunk { - rule_name: "anon_l4".to_string(), - proposed_rule: Some(proposed_rule), - rationale: "Allow /usr/bin/curl to connect to example.com:443.".to_string(), + rule_name: "example".to_string(), + proposed_rule: Some(rule.clone()), ..Default::default() }], ..Default::default() })), ) .await - .unwrap(); - - let draft = handle_get_draft_policy( - &state, - with_user(Request::new(GetDraftPolicyRequest { - name: sandbox_name, - status_filter: String::new(), - workspace: "default".to_string(), - })), - ) - .await .unwrap() .into_inner(); - let verdict = &draft.chunks[0].validation_result; - assert_eq!(verdict, "prover: no new findings"); + let chunk_id = &submit.accepted_chunk_ids[0]; + + let mut live = ProtoSandboxPolicy::default(); + live.network_policies.insert("example".to_string(), rule); assert_eq!( - draft.chunks[0].status, "approved", - "empty-delta mechanistic proposal under auto mode must auto-approve \ - (proposer-agnostic); got status: {}", - draft.chunks[0].status + reconcile_pending_chunks_covered_by_policy(&state, sandbox_id, &live, 7) + .await + .unwrap(), + 1 + ); + let reconciled = state + .store + .get_draft_chunk(chunk_id) + .await + .unwrap() + .unwrap(); + assert_eq!(reconciled.status, "rejected"); + assert_eq!( + reconciled.rejection_reason, + "covered by active policy revision 7" ); } @@ -11867,6 +13686,7 @@ mod tests { endpoints: vec![NetworkEndpoint { host: "api.github.com".to_string(), port: 443, + allow_uninspected_credentials: true, ..Default::default() }], binaries: vec![NetworkBinary { @@ -11955,6 +13775,7 @@ mod tests { last_seen_ms: 0, validation_result: String::new(), rejection_reason: String::new(), + ..Default::default() }; state .store @@ -11968,6 +13789,7 @@ mod tests { name: sandbox_name.to_string(), chunk_id: chunk.id.clone(), workspace: "default".to_string(), + ..Default::default() })), ) .await @@ -12046,6 +13868,7 @@ mod tests { endpoints: vec![NetworkEndpoint { host: "api.github.com".to_string(), port: 443, + allow_uninspected_credentials: true, ..Default::default() }], binaries: vec![NetworkBinary { @@ -12379,7 +14202,10 @@ mod tests { host: "api.github.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + // Match the provider-owned endpoint contract so this test + // exercises prover composition rather than a deterministic + // application failure. + enforcement: "audit".to_string(), rules: vec![L7Rule { allow: Some(L7Allow { method: "PUT".to_string(), @@ -13047,6 +14873,7 @@ mod tests { last_seen_ms: 1_000, validation_result: String::new(), rejection_reason: String::new(), + ..Default::default() } } @@ -13210,6 +15037,13 @@ mod tests { .unwrap() .into_inner(); let chunk_id = submit.accepted_chunk_ids[0].clone(); + let review_token = state + .store + .get_draft_chunk(&chunk_id) + .await + .unwrap() + .unwrap() + .review_token; handle_reject_draft_chunk( &state, @@ -13229,6 +15063,7 @@ mod tests { name: sandbox_name.clone(), chunk_id: chunk_id.clone(), workspace: "default".to_string(), + review_token, }), ) .await @@ -13369,6 +15204,7 @@ mod tests { .unwrap() .into_inner(); let chunk_id = draft_policy.chunks[0].id.clone(); + let review_token = draft_policy.chunks[0].review_token.clone(); let other_name = sandbox_b.object_name().to_string(); let approve_err = handle_approve_draft_chunk( @@ -13377,6 +15213,7 @@ mod tests { name: other_name.clone(), chunk_id: chunk_id.clone(), workspace: "default".to_string(), + review_token: String::new(), }), ) .await @@ -13415,6 +15252,7 @@ mod tests { name: sandbox_a.object_name().to_string(), chunk_id: chunk_id.clone(), workspace: "default".to_string(), + review_token, }), ) .await @@ -13642,6 +15480,7 @@ mod tests { last_seen_ms: 0, validation_result: String::new(), rejection_reason: String::new(), + ..Default::default() }; let (version, _) = @@ -13740,6 +15579,7 @@ mod tests { last_seen_ms: 0, validation_result: String::new(), rejection_reason: String::new(), + ..Default::default() }; let (version, _) = merge_chunk_into_policy(&store, sandbox_id, "default", &chunk, &[]) @@ -13842,6 +15682,7 @@ mod tests { last_seen_ms: 0, validation_result: String::new(), rejection_reason: String::new(), + ..Default::default() }; let (version, _) = merge_chunk_into_policy(&store, sandbox_id, "default", &chunk, &[]) @@ -13935,6 +15776,7 @@ mod tests { provider_layers: &[], credential_binding: None, }, + None, None ), apply_merge_operations_with_retry( @@ -13947,6 +15789,7 @@ mod tests { provider_layers: &[], credential_binding: None, }, + None, None ), ); @@ -14725,6 +16568,138 @@ mod tests { ); } + #[test] + fn review_token_and_policy_hash_are_stable_across_nested_proto_map_order() { + use openshell_core::proto::{GraphqlOperation, L7Allow, L7QueryMatcher}; + + fn matcher(value: &str) -> L7QueryMatcher { + L7QueryMatcher { + glob: value.to_string(), + any: Vec::new(), + } + } + + fn rule(reverse: bool) -> NetworkPolicyRule { + let mut query = HashMap::new(); + let mut params = HashMap::new(); + let mut persisted = HashMap::new(); + let entries = if reverse { + [("zeta", "two"), ("alpha", "one")] + } else { + [("alpha", "one"), ("zeta", "two")] + }; + for (key, value) in entries { + query.insert(key.to_string(), matcher(value)); + params.insert(key.to_string(), matcher(value)); + persisted.insert( + key.to_string(), + GraphqlOperation { + operation_type: "query".to_string(), + operation_name: value.to_string(), + fields: vec![value.to_string()], + }, + ); + } + NetworkPolicyRule { + name: "mapped".to_string(), + endpoints: vec![NetworkEndpoint { + host: "api.example.com".to_string(), + port: 443, + ports: vec![443], + protocol: "graphql".to_string(), + rules: vec![L7Rule { + allow: Some(L7Allow { + method: "POST".to_string(), + path: "/graphql".to_string(), + query, + params, + operation_type: "query".to_string(), + ..Default::default() + }), + }], + graphql_persisted_queries: persisted, + ..Default::default() + }], + binaries: vec![NetworkBinary { + path: "/usr/bin/curl".to_string(), + ..Default::default() + }], + } + } + + let left_rule = rule(false); + let right_rule = rule(true); + assert_eq!(left_rule, right_rule); + let left_policy = ProtoSandboxPolicy { + network_policies: HashMap::from([("mapped".to_string(), left_rule.clone())]), + ..Default::default() + }; + let right_policy = ProtoSandboxPolicy { + network_policies: HashMap::from([("mapped".to_string(), right_rule.clone())]), + ..Default::default() + }; + + assert_eq!( + deterministic_policy_hash(&left_policy), + deterministic_policy_hash(&right_policy) + ); + assert_eq!( + compute_proposal_review_token( + "mapped", + &left_rule, + &left_policy, + &left_policy, + &CredentialSet::default(), + ), + compute_proposal_review_token( + "mapped", + &right_rule, + &right_policy, + &right_policy, + &CredentialSet::default(), + ) + ); + assert_eq!( + compute_failed_proposal_evaluation_hash("mapped", &left_rule, &left_policy, "failed",), + compute_failed_proposal_evaluation_hash("mapped", &right_rule, &right_policy, "failed",) + ); + + let mut changed_rule = right_rule; + changed_rule.endpoints[0].rules[0] + .allow + .as_mut() + .unwrap() + .query + .get_mut("alpha") + .unwrap() + .glob = "changed".to_string(); + let changed_policy = ProtoSandboxPolicy { + network_policies: HashMap::from([("mapped".to_string(), changed_rule.clone())]), + ..Default::default() + }; + assert_ne!( + deterministic_policy_hash(&left_policy), + deterministic_policy_hash(&changed_policy), + "map values remain decision-relevant after canonical ordering" + ); + assert_ne!( + compute_proposal_review_token( + "mapped", + &left_rule, + &left_policy, + &left_policy, + &CredentialSet::default(), + ), + compute_proposal_review_token( + "mapped", + &changed_rule, + &changed_policy, + &changed_policy, + &CredentialSet::default(), + ) + ); + } + #[test] fn config_revision_changes_when_policy_source_changes() { let policy = ProtoSandboxPolicy::default(); diff --git a/crates/openshell-server/src/persistence/postgres.rs b/crates/openshell-server/src/persistence/postgres.rs index 73039acf05..8bab0ada96 100644 --- a/crates/openshell-server/src/persistence/postgres.rs +++ b/crates/openshell-server/src/persistence/postgres.rs @@ -1179,6 +1179,28 @@ WHERE object_type = $1 AND id = $2 AND status = 'pending' Ok(result.rows_affected() > 0) } + pub async fn update_draft_chunk_evaluation( + &self, + chunk: &DraftChunkRecord, + ) -> PersistenceResult { + let payload = draft_chunk_payload_from_record(chunk)?; + let result = sqlx::query( + r#" +UPDATE "objects" +SET "payload" = $3, "updated_at_ms" = $4 +WHERE "object_type" = $1 AND "id" = $2 AND "status" IN ('pending', 'rejected') +"#, + ) + .bind(DRAFT_CHUNK_OBJECT_TYPE) + .bind(&chunk.id) + .bind(payload) + .bind(chunk.last_seen_ms) + .execute(&self.pool) + .await + .map_err(|e| map_db_error(&e))?; + Ok(result.rows_affected() > 0) + } + pub async fn delete_draft_chunks( &self, sandbox_id: &str, diff --git a/crates/openshell-server/src/persistence/sqlite.rs b/crates/openshell-server/src/persistence/sqlite.rs index b7bff9c2d2..658530f753 100644 --- a/crates/openshell-server/src/persistence/sqlite.rs +++ b/crates/openshell-server/src/persistence/sqlite.rs @@ -1240,6 +1240,28 @@ WHERE "object_type" = ?1 AND "id" = ?2 AND "status" = 'pending' Ok(result.rows_affected() > 0) } + pub async fn update_draft_chunk_evaluation( + &self, + chunk: &DraftChunkRecord, + ) -> PersistenceResult { + let payload = draft_chunk_payload_from_record(chunk)?; + let result = sqlx::query( + r#" +UPDATE "objects" +SET "payload" = ?3, "updated_at_ms" = ?4 +WHERE "object_type" = ?1 AND "id" = ?2 AND "status" IN ('pending', 'rejected') +"#, + ) + .bind(DRAFT_CHUNK_OBJECT_TYPE) + .bind(&chunk.id) + .bind(payload) + .bind(chunk.last_seen_ms) + .execute(&self.pool) + .await + .map_err(|e| map_db_error(&e))?; + Ok(result.rows_affected() > 0) + } + pub async fn delete_draft_chunks( &self, sandbox_id: &str, diff --git a/crates/openshell-server/src/policy_store.rs b/crates/openshell-server/src/policy_store.rs index 1a49e35a1f..bd044c8712 100644 --- a/crates/openshell-server/src/policy_store.rs +++ b/crates/openshell-server/src/policy_store.rs @@ -197,10 +197,12 @@ pub trait PolicyStoreExt { rejection_reason: &str, ) -> PersistenceResult; - async fn update_draft_chunk_rule( + /// Replace the policy-dependent evaluation payload for a pending chunk. + /// Status and observation counters remain owned by their dedicated + /// columns and are not changed by this update. + async fn update_draft_chunk_evaluation( &self, - id: &str, - proposed_rule: &[u8], + chunk: &DraftChunkRecord, ) -> PersistenceResult; async fn delete_draft_chunks(&self, sandbox_id: &str, status: &str) -> PersistenceResult; @@ -394,14 +396,13 @@ impl PolicyStoreExt for Store { } } - async fn update_draft_chunk_rule( + async fn update_draft_chunk_evaluation( &self, - id: &str, - proposed_rule: &[u8], + chunk: &DraftChunkRecord, ) -> PersistenceResult { match self { - Self::Postgres(store) => store.update_draft_chunk_rule(id, proposed_rule).await, - Self::Sqlite(store) => store.update_draft_chunk_rule(id, proposed_rule).await, + Self::Postgres(store) => store.update_draft_chunk_evaluation(chunk).await, + Self::Sqlite(store) => store.update_draft_chunk_evaluation(chunk).await, } } @@ -497,6 +498,12 @@ pub fn draft_chunk_payload_from_record(chunk: &DraftChunkRecord) -> PersistenceR draft_version: chunk.draft_version, validation_result: chunk.validation_result.clone(), rejection_reason: chunk.rejection_reason.clone(), + application_error: chunk.application_error.clone(), + review_token: chunk.review_token.clone(), + current_effective_policy_hash: chunk.current_effective_policy_hash.clone(), + candidate_effective_policy_hash: chunk.candidate_effective_policy_hash.clone(), + current_effective_policy: chunk.current_effective_policy.clone(), + candidate_effective_policy: chunk.candidate_effective_policy.clone(), } .encode_to_vec()) } @@ -536,5 +543,11 @@ pub fn draft_chunk_record_from_parts( last_seen_ms: updated_at_ms, validation_result: wrapper.validation_result, rejection_reason: wrapper.rejection_reason, + application_error: wrapper.application_error, + review_token: wrapper.review_token, + current_effective_policy_hash: wrapper.current_effective_policy_hash, + candidate_effective_policy_hash: wrapper.candidate_effective_policy_hash, + current_effective_policy: wrapper.current_effective_policy, + candidate_effective_policy: wrapper.candidate_effective_policy, }) } diff --git a/crates/openshell-supervisor-network/src/policy_local.rs b/crates/openshell-supervisor-network/src/policy_local.rs index 8951d6936e..c542a058be 100644 --- a/crates/openshell-supervisor-network/src/policy_local.rs +++ b/crates/openshell-supervisor-network/src/policy_local.rs @@ -817,7 +817,8 @@ async fn fetch_chunk_or_404( /// next on the redraft loop — identity (`chunk_id`, `status`), the proposal /// it submitted (`rule_name`, `binary`), the two feedback signals /// (`rejection_reason` from the reviewer, `validation_result` from the -/// gateway prover), and (on /wait) `policy_reloaded` so the agent can tell +/// gateway prover, and `application_error` from complete candidate preflight), +/// plus the review token/candidate hashes and (on /wait) `policy_reloaded` so the agent can tell /// "approved AND the new rule is loaded — safe to retry" from "approved /// but the supervisor hasn't reloaded yet — re-issue /wait or surface to /// user". Display-only proto fields (`hit_count`, `confidence`, `stage`, @@ -834,6 +835,10 @@ fn chunk_state_payload( "binary": chunk.binary, "rejection_reason": chunk.rejection_reason, "validation_result": chunk.validation_result, + "application_error": chunk.application_error, + "review_token": chunk.review_token, + "current_effective_policy_hash": chunk.current_effective_policy_hash, + "candidate_effective_policy_hash": chunk.candidate_effective_policy_hash, }); if timed_out { payload["timed_out"] = serde_json::json!(true); @@ -1052,6 +1057,7 @@ fn policy_chunk_from_add_rule( binary, validation_result: String::new(), rejection_reason: String::new(), + ..Default::default() }) } @@ -1794,6 +1800,10 @@ mod tests { binary: "/usr/bin/curl".to_string(), rejection_reason: "scope too broad".to_string(), validation_result: "no exfil paths".to_string(), + application_error: "candidate invalid: malformed GraphQL operation".to_string(), + review_token: "review-v1".to_string(), + current_effective_policy_hash: "current-hash".to_string(), + candidate_effective_policy_hash: "candidate-hash".to_string(), ..Default::default() }; let pending = chunk_state_payload(&chunk, false, false); @@ -1801,6 +1811,13 @@ mod tests { assert_eq!(pending["status"], "rejected"); assert_eq!(pending["rejection_reason"], "scope too broad"); assert_eq!(pending["validation_result"], "no exfil paths"); + assert_eq!( + pending["application_error"], + "candidate invalid: malformed GraphQL operation" + ); + assert_eq!(pending["review_token"], "review-v1"); + assert_eq!(pending["current_effective_policy_hash"], "current-hash"); + assert_eq!(pending["candidate_effective_policy_hash"], "candidate-hash"); // timed_out and policy_reloaded only appear when relevant. assert!(pending.get("timed_out").is_none()); assert!( diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 1f610015b4..e90cee8b38 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -1862,8 +1862,8 @@ fn spawn_draft_approve(app: &App, tx: mpsc::UnboundedSender) { None => return, }; let abs = app.draft_scroll + app.draft_selected; - let chunk_id = match app.draft_chunks.get(abs) { - Some(c) => c.id.clone(), + let (chunk_id, review_token) = match app.draft_chunks.get(abs) { + Some(c) => (c.id.clone(), c.review_token.clone()), None => return, }; let rule_name = app @@ -1877,6 +1877,7 @@ fn spawn_draft_approve(app: &App, tx: mpsc::UnboundedSender) { name, chunk_id, workspace, + review_token, }; match tokio::time::timeout(Duration::from_secs(5), client.approve_draft_chunk(req)).await { Ok(Ok(resp)) => { @@ -1949,7 +1950,7 @@ fn spawn_draft_reject(app: &App, tx: mpsc::UnboundedSender) { /// modal count display but is not iterated for per-chunk approval. fn spawn_draft_approve_all( app: &App, - _snapshot: Vec, + snapshot: Vec, tx: mpsc::UnboundedSender, ) { let mut client = app.client.clone(); @@ -1960,10 +1961,18 @@ fn spawn_draft_approve_all( let workspace = app.selected_sandbox_workspace(); tokio::spawn(async move { + let approvals = snapshot + .into_iter() + .map(|chunk| openshell_core::proto::DraftChunkApproval { + chunk_id: chunk.id, + review_token: chunk.review_token, + }) + .collect(); let req = openshell_core::proto::ApproveAllDraftChunksRequest { name, include_security_flagged: false, workspace, + approvals, }; match tokio::time::timeout( Duration::from_secs(30), @@ -1973,17 +1982,7 @@ fn spawn_draft_approve_all( { Ok(Ok(resp)) => { let inner = resp.into_inner(); - let msg = if inner.chunks_skipped > 0 { - format!( - "Approved {} chunks, skipped {} security-flagged -> policy v{}", - inner.chunks_approved, inner.chunks_skipped, inner.policy_version - ) - } else { - format!( - "Approved {} chunks -> policy v{}", - inner.chunks_approved, inner.policy_version - ) - }; + let msg = format_draft_approve_all_result(&inner); let _ = tx.send(Event::DraftActionResult(Ok(msg))); } Ok(Err(e)) => { @@ -1998,6 +1997,22 @@ fn spawn_draft_approve_all( }); } +fn format_draft_approve_all_result( + result: &openshell_core::proto::ApproveAllDraftChunksResponse, +) -> String { + if result.chunks_skipped > 0 { + format!( + "Approved {} chunks, skipped {}; review remaining pending chunks -> policy v{}", + result.chunks_approved, result.chunks_skipped, result.policy_version + ) + } else { + format!( + "Approved {} chunks -> policy v{}", + result.chunks_approved, result.policy_version + ) + } +} + // --------------------------------------------------------------------------- // Data refresh // --------------------------------------------------------------------------- @@ -2719,6 +2734,29 @@ fn format_age(epoch_ms: i64) -> String { } } +#[cfg(test)] +mod draft_approve_all_message_tests { + use super::*; + + #[test] + fn skipped_chunks_are_not_assumed_to_be_security_flagged() { + let message = format_draft_approve_all_result( + &openshell_core::proto::ApproveAllDraftChunksResponse { + policy_version: 7, + chunks_approved: 2, + chunks_skipped: 1, + ..Default::default() + }, + ); + + assert_eq!( + message, + "Approved 2 chunks, skipped 1; review remaining pending chunks -> policy v7" + ); + assert!(!message.contains("security-flagged")); + } +} + #[cfg(test)] mod phase_label_tests { use super::*; diff --git a/crates/openshell-tui/src/ui/sandbox_draft.rs b/crates/openshell-tui/src/ui/sandbox_draft.rs index 5fd11cb997..cda60d7742 100644 --- a/crates/openshell-tui/src/ui/sandbox_draft.rs +++ b/crates/openshell-tui/src/ui/sandbox_draft.rs @@ -474,6 +474,14 @@ struct ApprovalAnnotation { } fn approval_annotation(chunk: &PolicyChunk) -> Option { + let application_error = chunk.application_error.trim(); + if !application_error.is_empty() { + return Some(ApprovalAnnotation { + kind: ApprovalAnnotationKind::RequiresReview, + short_label: "application blocked".to_string(), + detail_label: format!("candidate cannot be applied: {application_error}"), + }); + } let validation = chunk.validation_result.trim(); if validation.is_empty() { return None; diff --git a/docs/sandboxes/policy-advisor.mdx b/docs/sandboxes/policy-advisor.mdx index c1059dee78..bc09468cdb 100644 --- a/docs/sandboxes/policy-advisor.mdx +++ b/docs/sandboxes/policy-advisor.mdx @@ -102,9 +102,23 @@ The loop has seven steps: 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 stores accepted proposals as pending draft chunks for the sandbox and runs the [policy prover](#what-auto-approval-checks) against the proposed delta. -6. Before auto-approval, the gateway reloads the current stored rule, recalculates its prover verdict, and regenerates its security notes. Under `auto` mode, it approves the proposal only when both the prover delta and security notes are empty. Any finding or security note keeps the proposal pending. Under `manual` mode, every accepted proposal lands in the draft inbox for a developer to approve or reject. -7. 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. +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. @@ -179,7 +193,7 @@ The policy prover runs against mechanistic and agent-authored proposals alike an Findings are categorical. There is no severity tier. The reviewer reads the category and the structured evidence to decide. -Before auto-approval, the gateway reloads the draft chunk, recomputes its prover verdict, and regenerates security notes from its current rule. These checks apply after edits and deduplicated resubmissions: an edited stored rule controls the decision even when the duplicate incoming proposal has an empty prover delta. The recalculated verdict is used for this decision but is not written back, so a later `validation_result` read can still show the submit-time verdict. Failures leave the chunk pending. 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. Draft reads and bulk approval also regenerate notes from the stored rule. Any prover finding or security note keeps the chunk pending. +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 [Providers v2](/sandboxes/providers-v2) are part of the effective policy the prover reasons over. @@ -193,7 +207,7 @@ openshell rule get --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, and endpoint summary. For L7 proposals, the endpoint summary includes the protocol, method, and path: +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/**] @@ -205,6 +219,8 @@ Approve only when the structured rule matches the access you intend to grant: openshell rule approve --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 diff --git a/e2e/policy-advisor/README.md b/e2e/policy-advisor/README.md index 2c2c96f195..71c3f9d1dd 100644 --- a/e2e/policy-advisor/README.md +++ b/e2e/policy-advisor/README.md @@ -67,3 +67,11 @@ Or manually against a running gateway with `agent_policy_proposals_enabled=true` ```bash OPENSHELL_BIN=target/debug/openshell bash e2e/policy-advisor/mechanistic-smoke.sh ``` + +The #2821 regression additionally verifies that a denial on an existing +inspected endpoint becomes a binary expansion, auto-approves, hot-reloads, and +does not downgrade the endpoint to L4: + +```bash +mise run e2e:mechanistic-existing-endpoint +``` diff --git a/e2e/policy-advisor/existing-endpoint-auto-approve.sh b/e2e/policy-advisor/existing-endpoint-auto-approve.sh new file mode 100755 index 0000000000..c80c6bbdc4 --- /dev/null +++ b/e2e/policy-advisor/existing-endpoint-auto-approve.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Regression for #2821: a curl denial on cargo's inspected endpoint must +# become a compatible binary expansion, auto-approve, hot-reload, and retain +# the REST/read-only endpoint contract. + +set -euo pipefail +export NO_COLOR=1 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +OPENSHELL_BIN="${OPENSHELL_BIN:-${REPO_ROOT}/target/debug/openshell}" +RUN_ID="${RUN_ID:-$(date +%H%M%S)}" +SANDBOX="${SANDBOX:-advisor-2821-${RUN_ID}}" +FLUSH_WAIT="${FLUSH_WAIT:-45}" +TMP_DIR="$(mktemp -d)" + +strip_ansi() { + sed $'s/\033\\[[0-9;]*m//g' +} + +cleanup() { + "$OPENSHELL_BIN" sandbox delete "$SANDBOX" >/dev/null 2>&1 || true + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +cat > "${TMP_DIR}/policy.yaml" <<'EOF' +version: 1 +network_policies: + cargo_registry: + name: cargo-registry + endpoints: + - host: index.crates.io + port: 443 + protocol: rest + enforcement: enforce + access: read-only + binaries: + - path: /usr/bin/cargo +EOF + +"$OPENSHELL_BIN" sandbox create \ + --name "$SANDBOX" \ + --policy "${TMP_DIR}/policy.yaml" \ + --approval-mode auto \ + --no-auto-providers \ + --no-tty \ + --detach \ + -- sh -c "exec sleep infinity" >/dev/null + +set +e +DENY_OUTPUT="$($OPENSHELL_BIN sandbox exec --name "$SANDBOX" -- \ + /usr/bin/curl -fsS --max-time 10 https://index.crates.io/config.json 2>&1)" +DENY_STATUS=$? +set -e +if [[ "$DENY_STATUS" -eq 0 ]]; then + echo "expected the first curl request to be denied" >&2 + exit 1 +fi +printf '%s\n' "$DENY_OUTPUT" + +RULE_OUTPUT="" +for _attempt in $(seq 1 "$((FLUSH_WAIT / 5))"); do + RULE_OUTPUT="$($OPENSHELL_BIN rule get "$SANDBOX" 2>&1 | strip_ansi)" + grep -q "Status: approved" <<<"$RULE_OUTPUT" && break + sleep 5 +done + +printf '%s\n' "$RULE_OUTPUT" +grep -q "Status: approved" <<<"$RULE_OUTPUT" +grep -q "Rule: cargo_registry" <<<"$RULE_OUTPUT" +grep -q "Prover: prover: no new findings" <<<"$RULE_OUTPUT" +if grep -q "Application:" <<<"$RULE_OUTPUT"; then + echo "auto-approved chunk unexpectedly retained an application error" >&2 + exit 1 +fi + +POLICY_OUTPUT="$($OPENSHELL_BIN policy get "$SANDBOX" --full 2>&1 | strip_ansi)" +grep -q "protocol: rest" <<<"$POLICY_OUTPUT" +grep -q "access: read-only" <<<"$POLICY_OUTPUT" +grep -q "/usr/bin/cargo" <<<"$POLICY_OUTPUT" +grep -q "/usr/bin/curl" <<<"$POLICY_OUTPUT" + +for _attempt in $(seq 1 15); do + if "$OPENSHELL_BIN" sandbox exec --name "$SANDBOX" -- \ + /usr/bin/curl -fsS --max-time 15 https://index.crates.io/config.json \ + >/dev/null 2>&1; then + echo "#2821 existing-endpoint auto-approval regression passed" + exit 0 + fi + sleep 2 +done + +echo "approved policy did not hot-reload for curl" >&2 +exit 1 diff --git a/proto/openshell.proto b/proto/openshell.proto index 2dc70eec01..9f58103a5d 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -2352,6 +2352,20 @@ message PolicyChunk { // back to the in-sandbox agent so it can revise the proposal. // Empty for non-rejected chunks. string rejection_reason = 18; + // Gateway-side merge/application preflight failure. Kept separate from + // prover output and operator rejection so clients can explain why a + // prover-clean proposal is not currently applicable. + string application_error = 19; + // Opaque digest binding review to the exact live inputs and complete + // effective candidate evaluated by the gateway. + string review_token = 20; + // Deterministic hashes for compact candidate display and diagnostics. + string current_effective_policy_hash = 21; + string candidate_effective_policy_hash = 22; + // Complete effective policies used for review. These contain policy + // configuration only; credential secret values are never materialized. + openshell.sandbox.v1.SandboxPolicy current_effective_policy = 23; + openshell.sandbox.v1.SandboxPolicy candidate_effective_policy = 24; } // Notification that the draft policy was updated. @@ -2430,6 +2444,9 @@ message ApproveDraftChunkRequest { string chunk_id = 2; // Workspace scope. Empty defaults to "default". string workspace = 3; + // Token returned with the reviewed PolicyChunk. Approval fails with + // FAILED_PRECONDITION if live decision inputs no longer match it. + string review_token = 4; } message ApproveDraftChunkResponse { @@ -2454,6 +2471,11 @@ message RejectDraftChunkRequest { message RejectDraftChunkResponse {} // Approve all pending chunks. +message DraftChunkApproval { + string chunk_id = 1; + string review_token = 2; +} + message ApproveAllDraftChunksRequest { // Sandbox name. string name = 1; @@ -2461,6 +2483,9 @@ message ApproveAllDraftChunksRequest { bool include_security_flagged = 2; // Workspace scope. Empty defaults to "default". string workspace = 3; + // Exact reviewed chunks and tokens. The server validates them against one + // live snapshot, stages compatible operations in order, and writes once. + repeated DraftChunkApproval approvals = 4; } message ApproveAllDraftChunksResponse { @@ -2470,7 +2495,8 @@ message ApproveAllDraftChunksResponse { string policy_hash = 2; // Number of chunks approved. uint32 chunks_approved = 3; - // Number of chunks skipped (security-flagged). + // Number of chunks skipped for any reason, including security flags, + // stale or invalid candidates, and conflicts within the staged batch. uint32 chunks_skipped = 4; } @@ -2585,6 +2611,12 @@ message DraftChunkPayload { // Operator-supplied free-form rejection text; empty for non-rejected // chunks. Mirrors PolicyChunk.rejection_reason. string rejection_reason = 12; + string application_error = 13; + string review_token = 14; + string current_effective_policy_hash = 15; + string candidate_effective_policy_hash = 16; + openshell.sandbox.v1.SandboxPolicy current_effective_policy = 17; + openshell.sandbox.v1.SandboxPolicy candidate_effective_policy = 18; } // Internal stored policy revision row materialized from the generic objects table. @@ -2624,6 +2656,12 @@ message StoredDraftChunk { string validation_result = 18; // Operator-supplied free-form rejection text. See PolicyChunk. string rejection_reason = 19; + string application_error = 20; + string review_token = 21; + string current_effective_policy_hash = 22; + string candidate_effective_policy_hash = 23; + openshell.sandbox.v1.SandboxPolicy current_effective_policy = 24; + openshell.sandbox.v1.SandboxPolicy candidate_effective_policy = 25; } // --------------------------------------------------------------------------- diff --git a/sdk/go/openshell/v1/fake/policy.go b/sdk/go/openshell/v1/fake/policy.go index 367ebf2226..7360af1b03 100644 --- a/sdk/go/openshell/v1/fake/policy.go +++ b/sdk/go/openshell/v1/fake/policy.go @@ -71,7 +71,7 @@ func (c *fakePolicyClient) GetDraft(_ context.Context, _, _ string, _ ...v1.GetD } // ApproveDraftChunk returns Unimplemented. -func (c *fakePolicyClient) ApproveDraftChunk(_ context.Context, _, _, _ string) (*types.ApproveResult, error) { +func (c *fakePolicyClient) ApproveDraftChunk(_ context.Context, _, _, _, _ string) (*types.ApproveResult, error) { if c.closedFunc() { return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} } diff --git a/sdk/go/openshell/v1/fake/policy_test.go b/sdk/go/openshell/v1/fake/policy_test.go index 65dfe52904..f48398a137 100644 --- a/sdk/go/openshell/v1/fake/policy_test.go +++ b/sdk/go/openshell/v1/fake/policy_test.go @@ -24,7 +24,7 @@ func TestFakePolicy_GetDraft_ReturnsUnimplemented(t *testing.T) { func TestFakePolicy_ApproveDraftChunk_ReturnsUnimplemented(t *testing.T) { c := newFakePolicyClient(func() bool { return false }) - _, err := c.ApproveDraftChunk(context.Background(), "default", "sb-1", "chunk-1") + _, err := c.ApproveDraftChunk(context.Background(), "default", "sb-1", "chunk-1", "token-1") require.Error(t, err) assert.True(t, types.IsUnimplemented(err)) } @@ -279,7 +279,7 @@ func TestFakePolicy_GetDraft_ClosedReturnsUnavailable(t *testing.T) { func TestFakePolicy_ApproveDraftChunk_ClosedReturnsUnavailable(t *testing.T) { c := newFakePolicyClient(func() bool { return true }) - _, err := c.ApproveDraftChunk(context.Background(), "default", "sb-1", "chunk-1") + _, err := c.ApproveDraftChunk(context.Background(), "default", "sb-1", "chunk-1", "token-1") require.Error(t, err) assert.True(t, types.IsUnavailable(err)) } diff --git a/sdk/go/openshell/v1/internal/converter/policy.go b/sdk/go/openshell/v1/internal/converter/policy.go index 0b20a7ddbe..8d68ca2f57 100644 --- a/sdk/go/openshell/v1/internal/converter/policy.go +++ b/sdk/go/openshell/v1/internal/converter/policy.go @@ -54,24 +54,30 @@ func PolicyChunkFromProto(c *pb.PolicyChunk) *types.PolicyChunk { return nil } return &types.PolicyChunk{ - ID: c.GetId(), - Status: c.GetStatus(), - RuleName: c.GetRuleName(), - ProposedRule: NetworkPolicyRuleFromProto(c.GetProposedRule()), - Rationale: c.GetRationale(), - SecurityNotes: c.GetSecurityNotes(), - Confidence: c.GetConfidence(), - DenialSummaryIDs: CopyStringSlice(c.GetDenialSummaryIds()), - CreatedAt: TimeFromMillis(c.GetCreatedAtMs()), - DecidedAt: TimeFromMillis(c.GetDecidedAtMs()), - Stage: c.GetStage(), - SupersedesChunkID: c.GetSupersedesChunkId(), - HitCount: c.GetHitCount(), - FirstSeen: TimeFromMillis(c.GetFirstSeenMs()), - LastSeen: TimeFromMillis(c.GetLastSeenMs()), - Binary: c.GetBinary(), - ValidationResult: c.GetValidationResult(), - RejectionReason: c.GetRejectionReason(), + ID: c.GetId(), + Status: c.GetStatus(), + RuleName: c.GetRuleName(), + ProposedRule: NetworkPolicyRuleFromProto(c.GetProposedRule()), + Rationale: c.GetRationale(), + SecurityNotes: c.GetSecurityNotes(), + Confidence: c.GetConfidence(), + DenialSummaryIDs: CopyStringSlice(c.GetDenialSummaryIds()), + CreatedAt: TimeFromMillis(c.GetCreatedAtMs()), + DecidedAt: TimeFromMillis(c.GetDecidedAtMs()), + Stage: c.GetStage(), + SupersedesChunkID: c.GetSupersedesChunkId(), + HitCount: c.GetHitCount(), + FirstSeen: TimeFromMillis(c.GetFirstSeenMs()), + LastSeen: TimeFromMillis(c.GetLastSeenMs()), + Binary: c.GetBinary(), + ValidationResult: c.GetValidationResult(), + RejectionReason: c.GetRejectionReason(), + ApplicationError: c.GetApplicationError(), + ReviewToken: c.GetReviewToken(), + CurrentEffectivePolicyHash: c.GetCurrentEffectivePolicyHash(), + CandidateEffectivePolicyHash: c.GetCandidateEffectivePolicyHash(), + CurrentEffectivePolicy: SandboxPolicyFromProto(c.GetCurrentEffectivePolicy()), + CandidateEffectivePolicy: SandboxPolicyFromProto(c.GetCandidateEffectivePolicy()), } } diff --git a/sdk/go/openshell/v1/policy.go b/sdk/go/openshell/v1/policy.go index d1ebdaa264..cbe2e8621a 100644 --- a/sdk/go/openshell/v1/policy.go +++ b/sdk/go/openshell/v1/policy.go @@ -24,6 +24,9 @@ type ProcessPolicy = types.ProcessPolicy // PolicyChunk represents a single proposed policy change in the draft inbox. type PolicyChunk = types.PolicyChunk +// DraftChunkApproval binds a bulk approval to an evaluated proposal. +type DraftChunkApproval = types.DraftChunkApproval + // DraftPolicy contains the full draft policy state returned by GetDraft. type DraftPolicy = types.DraftPolicy @@ -72,6 +75,9 @@ type ApproveAllOption = types.ApproveAllOption // WithIncludeSecurityFlagged includes security-flagged chunks in bulk approval. var WithIncludeSecurityFlagged = types.WithIncludeSecurityFlagged +// WithDraftApprovals supplies token-bound chunks for bulk approval. +var WithDraftApprovals = types.WithDraftApprovals + // GetStatusOption configures a GetStatus call. type GetStatusOption = types.GetStatusOption @@ -99,7 +105,7 @@ var WithStatusGlobal = types.WithStatusGlobal // approvals, and revision history. type PolicyInterface interface { GetDraft(ctx context.Context, workspace, sandboxName string, opts ...GetDraftOption) (*DraftPolicy, error) - ApproveDraftChunk(ctx context.Context, workspace, sandboxName, chunkID string) (*ApproveResult, error) + ApproveDraftChunk(ctx context.Context, workspace, sandboxName, chunkID, reviewToken string) (*ApproveResult, error) RejectDraftChunk(ctx context.Context, workspace, sandboxName, chunkID, reason string) error ApproveAllDraftChunks(ctx context.Context, workspace, sandboxName string, opts ...ApproveAllOption) (*ApproveAllResult, error) ClearDraftChunks(ctx context.Context, workspace, sandboxName string) (*ClearResult, error) diff --git a/sdk/go/openshell/v1/policy_client.go b/sdk/go/openshell/v1/policy_client.go index fceeb52a60..f002caf9d9 100644 --- a/sdk/go/openshell/v1/policy_client.go +++ b/sdk/go/openshell/v1/policy_client.go @@ -33,11 +33,12 @@ func (p *policyClient) GetDraft(ctx context.Context, workspace, sandboxName stri return converter.DraftPolicyFromProto(resp), nil } -func (p *policyClient) ApproveDraftChunk(ctx context.Context, workspace, sandboxName, chunkID string) (*ApproveResult, error) { +func (p *policyClient) ApproveDraftChunk(ctx context.Context, workspace, sandboxName, chunkID, reviewToken string) (*ApproveResult, error) { resp, err := p.client.ApproveDraftChunk(ctx, &pb.ApproveDraftChunkRequest{ - Name: sandboxName, - ChunkId: chunkID, - Workspace: workspace, + Name: sandboxName, + ChunkId: chunkID, + Workspace: workspace, + ReviewToken: reviewToken, }) if err != nil { return nil, converter.FromGRPCError(err) @@ -60,10 +61,18 @@ func (p *policyClient) RejectDraftChunk(ctx context.Context, workspace, sandboxN func (p *policyClient) ApproveAllDraftChunks(ctx context.Context, workspace, sandboxName string, opts ...ApproveAllOption) (*ApproveAllResult, error) { cfg := types.ApplyApproveAllOptions(opts) + approvals := make([]*pb.DraftChunkApproval, 0, len(cfg.Approvals())) + for _, approval := range cfg.Approvals() { + approvals = append(approvals, &pb.DraftChunkApproval{ + ChunkId: approval.ChunkID, + ReviewToken: approval.ReviewToken, + }) + } resp, err := p.client.ApproveAllDraftChunks(ctx, &pb.ApproveAllDraftChunksRequest{ Name: sandboxName, IncludeSecurityFlagged: cfg.IncludeSecurityFlagged(), Workspace: workspace, + Approvals: approvals, }) if err != nil { return nil, converter.FromGRPCError(err) diff --git a/sdk/go/openshell/v1/policy_client_test.go b/sdk/go/openshell/v1/policy_client_test.go index a517cd2288..d572ca8e33 100644 --- a/sdk/go/openshell/v1/policy_client_test.go +++ b/sdk/go/openshell/v1/policy_client_test.go @@ -314,7 +314,7 @@ func TestPolicyApproveDraftChunk(t *testing.T) { client, cleanup := setupPolicyTest(t, mock) defer cleanup() - result, err := client.ApproveDraftChunk(context.Background(), "default", "my-sandbox", "chunk-1") + result, err := client.ApproveDraftChunk(context.Background(), "default", "my-sandbox", "chunk-1", "token-1") require.NoError(t, err) require.NotNil(t, result) @@ -323,6 +323,7 @@ func TestPolicyApproveDraftChunk(t *testing.T) { mock.mu.Lock() assert.Equal(t, "my-sandbox", mock.lastApproveReq.GetName()) assert.Equal(t, "chunk-1", mock.lastApproveReq.GetChunkId()) + assert.Equal(t, "token-1", mock.lastApproveReq.GetReviewToken()) mock.mu.Unlock() // Verify response mapping. @@ -337,7 +338,7 @@ func TestPolicyApproveDraftChunk_Error(t *testing.T) { client, cleanup := setupPolicyTest(t, mock) defer cleanup() - result, err := client.ApproveDraftChunk(context.Background(), "default", "sb1", "bad-chunk") + result, err := client.ApproveDraftChunk(context.Background(), "default", "sb1", "bad-chunk", "token-bad") assert.Nil(t, result) require.Error(t, err) diff --git a/sdk/go/openshell/v1/types/policy.go b/sdk/go/openshell/v1/types/policy.go index 8713fce04c..9b6082b9ec 100644 --- a/sdk/go/openshell/v1/types/policy.go +++ b/sdk/go/openshell/v1/types/policy.go @@ -78,6 +78,20 @@ type PolicyChunk struct { ValidationResult string // RejectionReason is the operator-supplied text accompanying a rejection. RejectionReason string + // ApplicationError is the complete-candidate preflight/application failure. + ApplicationError string + // ReviewToken binds an approval to the exact evaluated candidate. + ReviewToken string + CurrentEffectivePolicyHash string + CandidateEffectivePolicyHash string + CurrentEffectivePolicy *SandboxPolicy + CandidateEffectivePolicy *SandboxPolicy +} + +// DraftChunkApproval binds one bulk approval decision to a reviewed chunk. +type DraftChunkApproval struct { + ChunkID string + ReviewToken string } // DraftPolicy contains the full draft policy state returned by GetDraft. @@ -266,6 +280,14 @@ func (c *getDraftConfig) StatusFilter() string { // approveAllConfig holds configuration for ApproveAllDraftChunks calls. type approveAllConfig struct { includeSecurityFlagged bool + approvals []DraftChunkApproval +} + +// WithDraftApprovals supplies the exact reviewed chunk tokens for bulk approval. +func WithDraftApprovals(approvals ...DraftChunkApproval) ApproveAllOption { + return func(c *approveAllConfig) { + c.approvals = append([]DraftChunkApproval(nil), approvals...) + } } // ApproveAllOption configures an ApproveAllDraftChunks call. @@ -292,6 +314,11 @@ func (c *approveAllConfig) IncludeSecurityFlagged() bool { return c.includeSecurityFlagged } +// Approvals returns a copy of the configured token-bound approvals. +func (c *approveAllConfig) Approvals() []DraftChunkApproval { + return append([]DraftChunkApproval(nil), c.approvals...) +} + // getStatusConfig holds configuration for GetStatus calls. type getStatusConfig struct { version uint32 diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 38e54f419e..7bdf99b333 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -10542,8 +10542,22 @@ type PolicyChunk struct { // back to the in-sandbox agent so it can revise the proposal. // Empty for non-rejected chunks. RejectionReason string `protobuf:"bytes,18,opt,name=rejection_reason,json=rejectionReason,proto3" json:"rejection_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Gateway-side merge/application preflight failure. Kept separate from + // prover output and operator rejection so clients can explain why a + // prover-clean proposal is not currently applicable. + ApplicationError string `protobuf:"bytes,19,opt,name=application_error,json=applicationError,proto3" json:"application_error,omitempty"` + // Opaque digest binding review to the exact live inputs and complete + // effective candidate evaluated by the gateway. + ReviewToken string `protobuf:"bytes,20,opt,name=review_token,json=reviewToken,proto3" json:"review_token,omitempty"` + // Deterministic hashes for compact candidate display and diagnostics. + CurrentEffectivePolicyHash string `protobuf:"bytes,21,opt,name=current_effective_policy_hash,json=currentEffectivePolicyHash,proto3" json:"current_effective_policy_hash,omitempty"` + CandidateEffectivePolicyHash string `protobuf:"bytes,22,opt,name=candidate_effective_policy_hash,json=candidateEffectivePolicyHash,proto3" json:"candidate_effective_policy_hash,omitempty"` + // Complete effective policies used for review. These contain policy + // configuration only; credential secret values are never materialized. + CurrentEffectivePolicy *sandboxv1.SandboxPolicy `protobuf:"bytes,23,opt,name=current_effective_policy,json=currentEffectivePolicy,proto3" json:"current_effective_policy,omitempty"` + CandidateEffectivePolicy *sandboxv1.SandboxPolicy `protobuf:"bytes,24,opt,name=candidate_effective_policy,json=candidateEffectivePolicy,proto3" json:"candidate_effective_policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PolicyChunk) Reset() { @@ -10702,6 +10716,48 @@ func (x *PolicyChunk) GetRejectionReason() string { return "" } +func (x *PolicyChunk) GetApplicationError() string { + if x != nil { + return x.ApplicationError + } + return "" +} + +func (x *PolicyChunk) GetReviewToken() string { + if x != nil { + return x.ReviewToken + } + return "" +} + +func (x *PolicyChunk) GetCurrentEffectivePolicyHash() string { + if x != nil { + return x.CurrentEffectivePolicyHash + } + return "" +} + +func (x *PolicyChunk) GetCandidateEffectivePolicyHash() string { + if x != nil { + return x.CandidateEffectivePolicyHash + } + return "" +} + +func (x *PolicyChunk) GetCurrentEffectivePolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.CurrentEffectivePolicy + } + return nil +} + +func (x *PolicyChunk) GetCandidateEffectivePolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.CandidateEffectivePolicy + } + return nil +} + // Notification that the draft policy was updated. type DraftPolicyUpdate struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -11090,7 +11146,10 @@ type ApproveDraftChunkRequest struct { // Chunk ID to approve. ChunkId string `protobuf:"bytes,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + // Token returned with the reviewed PolicyChunk. Approval fails with + // FAILED_PRECONDITION if live decision inputs no longer match it. + ReviewToken string `protobuf:"bytes,4,opt,name=review_token,json=reviewToken,proto3" json:"review_token,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -11146,6 +11205,13 @@ func (x *ApproveDraftChunkRequest) GetWorkspace() string { return "" } +func (x *ApproveDraftChunkRequest) GetReviewToken() string { + if x != nil { + return x.ReviewToken + } + return "" +} + type ApproveDraftChunkResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // New policy version after merge. @@ -11310,6 +11376,58 @@ func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { } // Approve all pending chunks. +type DraftChunkApproval struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChunkId string `protobuf:"bytes,1,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` + ReviewToken string `protobuf:"bytes,2,opt,name=review_token,json=reviewToken,proto3" json:"review_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DraftChunkApproval) Reset() { + *x = DraftChunkApproval{} + mi := &file_openshell_proto_msgTypes[157] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DraftChunkApproval) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DraftChunkApproval) ProtoMessage() {} + +func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[157] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DraftChunkApproval.ProtoReflect.Descriptor instead. +func (*DraftChunkApproval) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{157} +} + +func (x *DraftChunkApproval) GetChunkId() string { + if x != nil { + return x.ChunkId + } + return "" +} + +func (x *DraftChunkApproval) GetReviewToken() string { + if x != nil { + return x.ReviewToken + } + return "" +} + type ApproveAllDraftChunksRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Sandbox name. @@ -11317,14 +11435,17 @@ type ApproveAllDraftChunksRequest struct { // Include chunks with security_notes (default false: skips them). IncludeSecurityFlagged bool `protobuf:"varint,2,opt,name=include_security_flagged,json=includeSecurityFlagged,proto3" json:"include_security_flagged,omitempty"` // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + // Exact reviewed chunks and tokens. The server validates them against one + // live snapshot, stages compatible operations in order, and writes once. + Approvals []*DraftChunkApproval `protobuf:"bytes,4,rep,name=approvals,proto3" json:"approvals,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11336,7 +11457,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11349,7 +11470,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{157} + return file_openshell_proto_rawDescGZIP(), []int{158} } func (x *ApproveAllDraftChunksRequest) GetName() string { @@ -11373,6 +11494,13 @@ func (x *ApproveAllDraftChunksRequest) GetWorkspace() string { return "" } +func (x *ApproveAllDraftChunksRequest) GetApprovals() []*DraftChunkApproval { + if x != nil { + return x.Approvals + } + return nil +} + type ApproveAllDraftChunksResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // New policy version after merge. @@ -11381,7 +11509,8 @@ type ApproveAllDraftChunksResponse struct { PolicyHash string `protobuf:"bytes,2,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` // Number of chunks approved. ChunksApproved uint32 `protobuf:"varint,3,opt,name=chunks_approved,json=chunksApproved,proto3" json:"chunks_approved,omitempty"` - // Number of chunks skipped (security-flagged). + // Number of chunks skipped for any reason, including security flags, + // stale or invalid candidates, and conflicts within the staged batch. ChunksSkipped uint32 `protobuf:"varint,4,opt,name=chunks_skipped,json=chunksSkipped,proto3" json:"chunks_skipped,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -11389,7 +11518,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11401,7 +11530,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11414,7 +11543,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{158} + return file_openshell_proto_rawDescGZIP(), []int{159} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -11462,7 +11591,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11474,7 +11603,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11487,7 +11616,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{159} + return file_openshell_proto_rawDescGZIP(), []int{160} } func (x *EditDraftChunkRequest) GetName() string { @@ -11526,7 +11655,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11538,7 +11667,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11551,7 +11680,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{160} + return file_openshell_proto_rawDescGZIP(), []int{161} } // Reverse an approval (remove merged rule from active policy). @@ -11569,7 +11698,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11581,7 +11710,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11594,7 +11723,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{161} + return file_openshell_proto_rawDescGZIP(), []int{162} } func (x *UndoDraftChunkRequest) GetName() string { @@ -11630,7 +11759,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11642,7 +11771,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11655,7 +11784,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{162} + return file_openshell_proto_rawDescGZIP(), []int{163} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -11685,7 +11814,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11697,7 +11826,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11710,7 +11839,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{163} + return file_openshell_proto_rawDescGZIP(), []int{164} } func (x *ClearDraftChunksRequest) GetName() string { @@ -11737,7 +11866,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11749,7 +11878,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11762,7 +11891,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{164} + return file_openshell_proto_rawDescGZIP(), []int{165} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -11785,7 +11914,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11797,7 +11926,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11810,7 +11939,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{165} + return file_openshell_proto_rawDescGZIP(), []int{166} } func (x *GetDraftHistoryRequest) GetName() string { @@ -11844,7 +11973,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11856,7 +11985,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11869,7 +11998,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{166} + return file_openshell_proto_rawDescGZIP(), []int{167} } func (x *DraftHistoryEntry) GetTimestampMs() int64 { @@ -11910,7 +12039,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11922,7 +12051,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11935,7 +12064,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{167} + return file_openshell_proto_rawDescGZIP(), []int{168} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -11964,7 +12093,7 @@ type PolicyRevisionPayload struct { func (x *PolicyRevisionPayload) Reset() { *x = PolicyRevisionPayload{} - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11976,7 +12105,7 @@ func (x *PolicyRevisionPayload) String() string { func (*PolicyRevisionPayload) ProtoMessage() {} func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11989,7 +12118,7 @@ func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyRevisionPayload.ProtoReflect.Descriptor instead. func (*PolicyRevisionPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{168} + return file_openshell_proto_rawDescGZIP(), []int{169} } func (x *PolicyRevisionPayload) GetPolicy() *sandboxv1.SandboxPolicy { @@ -12055,14 +12184,20 @@ type DraftChunkPayload struct { ValidationResult string `protobuf:"bytes,11,opt,name=validation_result,json=validationResult,proto3" json:"validation_result,omitempty"` // Operator-supplied free-form rejection text; empty for non-rejected // chunks. Mirrors PolicyChunk.rejection_reason. - RejectionReason string `protobuf:"bytes,12,opt,name=rejection_reason,json=rejectionReason,proto3" json:"rejection_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + RejectionReason string `protobuf:"bytes,12,opt,name=rejection_reason,json=rejectionReason,proto3" json:"rejection_reason,omitempty"` + ApplicationError string `protobuf:"bytes,13,opt,name=application_error,json=applicationError,proto3" json:"application_error,omitempty"` + ReviewToken string `protobuf:"bytes,14,opt,name=review_token,json=reviewToken,proto3" json:"review_token,omitempty"` + CurrentEffectivePolicyHash string `protobuf:"bytes,15,opt,name=current_effective_policy_hash,json=currentEffectivePolicyHash,proto3" json:"current_effective_policy_hash,omitempty"` + CandidateEffectivePolicyHash string `protobuf:"bytes,16,opt,name=candidate_effective_policy_hash,json=candidateEffectivePolicyHash,proto3" json:"candidate_effective_policy_hash,omitempty"` + CurrentEffectivePolicy *sandboxv1.SandboxPolicy `protobuf:"bytes,17,opt,name=current_effective_policy,json=currentEffectivePolicy,proto3" json:"current_effective_policy,omitempty"` + CandidateEffectivePolicy *sandboxv1.SandboxPolicy `protobuf:"bytes,18,opt,name=candidate_effective_policy,json=candidateEffectivePolicy,proto3" json:"candidate_effective_policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DraftChunkPayload) Reset() { *x = DraftChunkPayload{} - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12074,7 +12209,7 @@ func (x *DraftChunkPayload) String() string { func (*DraftChunkPayload) ProtoMessage() {} func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12087,7 +12222,7 @@ func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkPayload.ProtoReflect.Descriptor instead. func (*DraftChunkPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{169} + return file_openshell_proto_rawDescGZIP(), []int{170} } func (x *DraftChunkPayload) GetRuleName() string { @@ -12174,6 +12309,48 @@ func (x *DraftChunkPayload) GetRejectionReason() string { return "" } +func (x *DraftChunkPayload) GetApplicationError() string { + if x != nil { + return x.ApplicationError + } + return "" +} + +func (x *DraftChunkPayload) GetReviewToken() string { + if x != nil { + return x.ReviewToken + } + return "" +} + +func (x *DraftChunkPayload) GetCurrentEffectivePolicyHash() string { + if x != nil { + return x.CurrentEffectivePolicyHash + } + return "" +} + +func (x *DraftChunkPayload) GetCandidateEffectivePolicyHash() string { + if x != nil { + return x.CandidateEffectivePolicyHash + } + return "" +} + +func (x *DraftChunkPayload) GetCurrentEffectivePolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.CurrentEffectivePolicy + } + return nil +} + +func (x *DraftChunkPayload) GetCandidateEffectivePolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.CandidateEffectivePolicy + } + return nil +} + // Internal stored policy revision row materialized from the generic objects table. type StoredPolicyRevision struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -12193,7 +12370,7 @@ type StoredPolicyRevision struct { func (x *StoredPolicyRevision) Reset() { *x = StoredPolicyRevision{} - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12205,7 +12382,7 @@ func (x *StoredPolicyRevision) String() string { func (*StoredPolicyRevision) ProtoMessage() {} func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12218,7 +12395,7 @@ func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredPolicyRevision.ProtoReflect.Descriptor instead. func (*StoredPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{170} + return file_openshell_proto_rawDescGZIP(), []int{171} } func (x *StoredPolicyRevision) GetId() string { @@ -12314,14 +12491,20 @@ type StoredDraftChunk struct { // Gateway prover verdict; empty until the prover runs. See PolicyChunk. ValidationResult string `protobuf:"bytes,18,opt,name=validation_result,json=validationResult,proto3" json:"validation_result,omitempty"` // Operator-supplied free-form rejection text. See PolicyChunk. - RejectionReason string `protobuf:"bytes,19,opt,name=rejection_reason,json=rejectionReason,proto3" json:"rejection_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + RejectionReason string `protobuf:"bytes,19,opt,name=rejection_reason,json=rejectionReason,proto3" json:"rejection_reason,omitempty"` + ApplicationError string `protobuf:"bytes,20,opt,name=application_error,json=applicationError,proto3" json:"application_error,omitempty"` + ReviewToken string `protobuf:"bytes,21,opt,name=review_token,json=reviewToken,proto3" json:"review_token,omitempty"` + CurrentEffectivePolicyHash string `protobuf:"bytes,22,opt,name=current_effective_policy_hash,json=currentEffectivePolicyHash,proto3" json:"current_effective_policy_hash,omitempty"` + CandidateEffectivePolicyHash string `protobuf:"bytes,23,opt,name=candidate_effective_policy_hash,json=candidateEffectivePolicyHash,proto3" json:"candidate_effective_policy_hash,omitempty"` + CurrentEffectivePolicy *sandboxv1.SandboxPolicy `protobuf:"bytes,24,opt,name=current_effective_policy,json=currentEffectivePolicy,proto3" json:"current_effective_policy,omitempty"` + CandidateEffectivePolicy *sandboxv1.SandboxPolicy `protobuf:"bytes,25,opt,name=candidate_effective_policy,json=candidateEffectivePolicy,proto3" json:"candidate_effective_policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StoredDraftChunk) Reset() { *x = StoredDraftChunk{} - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12333,7 +12516,7 @@ func (x *StoredDraftChunk) String() string { func (*StoredDraftChunk) ProtoMessage() {} func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12346,7 +12529,7 @@ func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredDraftChunk.ProtoReflect.Descriptor instead. func (*StoredDraftChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{171} + return file_openshell_proto_rawDescGZIP(), []int{172} } func (x *StoredDraftChunk) GetId() string { @@ -12482,6 +12665,48 @@ func (x *StoredDraftChunk) GetRejectionReason() string { return "" } +func (x *StoredDraftChunk) GetApplicationError() string { + if x != nil { + return x.ApplicationError + } + return "" +} + +func (x *StoredDraftChunk) GetReviewToken() string { + if x != nil { + return x.ReviewToken + } + return "" +} + +func (x *StoredDraftChunk) GetCurrentEffectivePolicyHash() string { + if x != nil { + return x.CurrentEffectivePolicyHash + } + return "" +} + +func (x *StoredDraftChunk) GetCandidateEffectivePolicyHash() string { + if x != nil { + return x.CandidateEffectivePolicyHash + } + return "" +} + +func (x *StoredDraftChunk) GetCurrentEffectivePolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.CurrentEffectivePolicy + } + return nil +} + +func (x *StoredDraftChunk) GetCandidateEffectivePolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.CandidateEffectivePolicy + } + return nil +} + // Create workspace request. type CreateWorkspaceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -12495,7 +12720,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12507,7 +12732,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12520,7 +12745,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{172} + return file_openshell_proto_rawDescGZIP(), []int{173} } func (x *CreateWorkspaceRequest) GetName() string { @@ -12547,7 +12772,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12559,7 +12784,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12572,7 +12797,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{173} + return file_openshell_proto_rawDescGZIP(), []int{174} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -12593,7 +12818,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12605,7 +12830,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12618,7 +12843,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{174} + return file_openshell_proto_rawDescGZIP(), []int{175} } func (x *GetWorkspaceRequest) GetName() string { @@ -12638,7 +12863,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12650,7 +12875,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12663,7 +12888,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{175} + return file_openshell_proto_rawDescGZIP(), []int{176} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -12686,7 +12911,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12698,7 +12923,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12711,7 +12936,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{176} + return file_openshell_proto_rawDescGZIP(), []int{177} } func (x *ListWorkspacesRequest) GetLimit() uint32 { @@ -12745,7 +12970,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12757,7 +12982,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12770,7 +12995,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{177} + return file_openshell_proto_rawDescGZIP(), []int{178} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -12791,7 +13016,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12803,7 +13028,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12816,7 +13041,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{178} + return file_openshell_proto_rawDescGZIP(), []int{179} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -12836,7 +13061,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12848,7 +13073,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12861,7 +13086,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{179} + return file_openshell_proto_rawDescGZIP(), []int{180} } func (x *DeleteWorkspaceResponse) GetDeleted() bool { @@ -12885,7 +13110,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12897,7 +13122,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12910,7 +13135,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{180} + return file_openshell_proto_rawDescGZIP(), []int{181} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -12949,7 +13174,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12961,7 +13186,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12974,7 +13199,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{181} + return file_openshell_proto_rawDescGZIP(), []int{182} } func (x *AddWorkspaceMemberRequest) GetWorkspace() string { @@ -13008,7 +13233,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13020,7 +13245,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13033,7 +13258,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{182} + return file_openshell_proto_rawDescGZIP(), []int{183} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -13056,7 +13281,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13068,7 +13293,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13081,7 +13306,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{183} + return file_openshell_proto_rawDescGZIP(), []int{184} } func (x *RemoveWorkspaceMemberRequest) GetWorkspace() string { @@ -13108,7 +13333,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13120,7 +13345,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[185] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13133,7 +13358,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{184} + return file_openshell_proto_rawDescGZIP(), []int{185} } func (x *RemoveWorkspaceMemberResponse) GetRemoved() bool { @@ -13156,7 +13381,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13168,7 +13393,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[186] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13181,7 +13406,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{185} + return file_openshell_proto_rawDescGZIP(), []int{186} } func (x *ListWorkspaceMembersRequest) GetWorkspace() string { @@ -13215,7 +13440,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13227,7 +13452,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[187] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13240,7 +13465,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{186} + return file_openshell_proto_rawDescGZIP(), []int{187} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -13268,7 +13493,7 @@ type ExtensionServiceCredential struct { func (x *ExtensionServiceCredential) Reset() { *x = ExtensionServiceCredential{} - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13280,7 +13505,7 @@ func (x *ExtensionServiceCredential) String() string { func (*ExtensionServiceCredential) ProtoMessage() {} func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[188] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13293,7 +13518,7 @@ func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtensionServiceCredential.ProtoReflect.Descriptor instead. func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{187} + return file_openshell_proto_rawDescGZIP(), []int{188} } func (x *ExtensionServiceCredential) GetServiceName() string { @@ -14106,7 +14331,7 @@ const file_openshell_proto_rawDesc = "" + "\x16NetworkActivitySummary\x124\n" + "\x16network_activity_count\x18\x01 \x01(\rR\x14networkActivityCount\x12.\n" + "\x13denied_action_count\x18\x02 \x01(\rR\x11deniedActionCount\x12H\n" + - "\x10denials_by_group\x18\x03 \x03(\v2\x1e.openshell.v1.DenialGroupCountR\x0edenialsByGroup\"\x94\x05\n" + + "\x10denials_by_group\x18\x03 \x03(\v2\x1e.openshell.v1.DenialGroupCountR\x0edenialsByGroup\"\xb0\b\n" + "\vPolicyChunk\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + "\x06status\x18\x02 \x01(\tR\x06status\x12\x1b\n" + @@ -14129,7 +14354,13 @@ const file_openshell_proto_rawDesc = "" + "lastSeenMs\x12\x16\n" + "\x06binary\x18\x10 \x01(\tR\x06binary\x12+\n" + "\x11validation_result\x18\x11 \x01(\tR\x10validationResult\x12)\n" + - "\x10rejection_reason\x18\x12 \x01(\tR\x0frejectionReason\"\x96\x01\n" + + "\x10rejection_reason\x18\x12 \x01(\tR\x0frejectionReason\x12+\n" + + "\x11application_error\x18\x13 \x01(\tR\x10applicationError\x12!\n" + + "\freview_token\x18\x14 \x01(\tR\vreviewToken\x12A\n" + + "\x1dcurrent_effective_policy_hash\x18\x15 \x01(\tR\x1acurrentEffectivePolicyHash\x12E\n" + + "\x1fcandidate_effective_policy_hash\x18\x16 \x01(\tR\x1ccandidateEffectivePolicyHash\x12]\n" + + "\x18current_effective_policy\x18\x17 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x16currentEffectivePolicy\x12a\n" + + "\x1acandidate_effective_policy\x18\x18 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x18candidateEffectivePolicy\"\x96\x01\n" + "\x11DraftPolicyUpdate\x12#\n" + "\rdraft_version\x18\x01 \x01(\x04R\fdraftVersion\x12\x1d\n" + "\n" + @@ -14156,11 +14387,12 @@ const file_openshell_proto_rawDesc = "" + "\x06chunks\x18\x01 \x03(\v2\x19.openshell.v1.PolicyChunkR\x06chunks\x12'\n" + "\x0frolling_summary\x18\x02 \x01(\tR\x0erollingSummary\x12#\n" + "\rdraft_version\x18\x03 \x01(\x04R\fdraftVersion\x12-\n" + - "\x13last_analyzed_at_ms\x18\x04 \x01(\x03R\x10lastAnalyzedAtMs\"g\n" + + "\x13last_analyzed_at_ms\x18\x04 \x01(\x03R\x10lastAnalyzedAtMs\"\x8a\x01\n" + "\x18ApproveDraftChunkRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\"c\n" + + "\tworkspace\x18\x03 \x01(\tR\tworkspace\x12!\n" + + "\freview_token\x18\x04 \x01(\tR\vreviewToken\"c\n" + "\x19ApproveDraftChunkResponse\x12%\n" + "\x0epolicy_version\x18\x01 \x01(\rR\rpolicyVersion\x12\x1f\n" + "\vpolicy_hash\x18\x02 \x01(\tR\n" + @@ -14170,11 +14402,15 @@ const file_openshell_proto_rawDesc = "" + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12\x16\n" + "\x06reason\x18\x03 \x01(\tR\x06reason\x12\x1c\n" + "\tworkspace\x18\x04 \x01(\tR\tworkspace\"\x1a\n" + - "\x18RejectDraftChunkResponse\"\x8a\x01\n" + + "\x18RejectDraftChunkResponse\"R\n" + + "\x12DraftChunkApproval\x12\x19\n" + + "\bchunk_id\x18\x01 \x01(\tR\achunkId\x12!\n" + + "\freview_token\x18\x02 \x01(\tR\vreviewToken\"\xca\x01\n" + "\x1cApproveAllDraftChunksRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x128\n" + "\x18include_security_flagged\x18\x02 \x01(\bR\x16includeSecurityFlagged\x12\x1c\n" + - "\tworkspace\x18\x03 \x01(\tR\tworkspace\"\xb7\x01\n" + + "\tworkspace\x18\x03 \x01(\tR\tworkspace\x12>\n" + + "\tapprovals\x18\x04 \x03(\v2 .openshell.v1.DraftChunkApprovalR\tapprovals\"\xb7\x01\n" + "\x1dApproveAllDraftChunksResponse\x12%\n" + "\x0epolicy_version\x18\x01 \x01(\rR\rpolicyVersion\x12\x1f\n" + "\vpolicy_hash\x18\x02 \x01(\tR\n" + @@ -14223,7 +14459,7 @@ const file_openshell_proto_rawDesc = "" + "provenance\x1a=\n" + "\x0fProvenanceEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc4\x03\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe0\x06\n" + "\x11DraftChunkPayload\x12\x1b\n" + "\trule_name\x18\x01 \x01(\tR\bruleName\x12L\n" + "\rproposed_rule\x18\x02 \x01(\v2'.openshell.sandbox.v1.NetworkPolicyRuleR\fproposedRule\x12\x1c\n" + @@ -14239,7 +14475,13 @@ const file_openshell_proto_rawDesc = "" + "\rdraft_version\x18\n" + " \x01(\x03R\fdraftVersion\x12+\n" + "\x11validation_result\x18\v \x01(\tR\x10validationResult\x12)\n" + - "\x10rejection_reason\x18\f \x01(\tR\x0frejectionReason\"\xe1\x03\n" + + "\x10rejection_reason\x18\f \x01(\tR\x0frejectionReason\x12+\n" + + "\x11application_error\x18\r \x01(\tR\x10applicationError\x12!\n" + + "\freview_token\x18\x0e \x01(\tR\vreviewToken\x12A\n" + + "\x1dcurrent_effective_policy_hash\x18\x0f \x01(\tR\x1acurrentEffectivePolicyHash\x12E\n" + + "\x1fcandidate_effective_policy_hash\x18\x10 \x01(\tR\x1ccandidateEffectivePolicyHash\x12]\n" + + "\x18current_effective_policy\x18\x11 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x16currentEffectivePolicy\x12a\n" + + "\x1acandidate_effective_policy\x18\x12 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x18candidateEffectivePolicy\"\xe1\x03\n" + "\x14StoredPolicyRevision\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + "\n" + @@ -14262,7 +14504,7 @@ const file_openshell_proto_rawDesc = "" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\r\n" + "\v_load_errorB\x0f\n" + - "\r_loaded_at_ms\"\xff\x04\n" + + "\r_loaded_at_ms\"\x9b\b\n" + "\x10StoredDraftChunk\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + "\n" + @@ -14287,7 +14529,13 @@ const file_openshell_proto_rawDesc = "" + "\flast_seen_ms\x18\x11 \x01(\x03R\n" + "lastSeenMs\x12+\n" + "\x11validation_result\x18\x12 \x01(\tR\x10validationResult\x12)\n" + - "\x10rejection_reason\x18\x13 \x01(\tR\x0frejectionReasonB\x10\n" + + "\x10rejection_reason\x18\x13 \x01(\tR\x0frejectionReason\x12+\n" + + "\x11application_error\x18\x14 \x01(\tR\x10applicationError\x12!\n" + + "\freview_token\x18\x15 \x01(\tR\vreviewToken\x12A\n" + + "\x1dcurrent_effective_policy_hash\x18\x16 \x01(\tR\x1acurrentEffectivePolicyHash\x12E\n" + + "\x1fcandidate_effective_policy_hash\x18\x17 \x01(\tR\x1ccandidateEffectivePolicyHash\x12]\n" + + "\x18current_effective_policy\x18\x18 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x16currentEffectivePolicy\x12a\n" + + "\x1acandidate_effective_policy\x18\x19 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x18candidateEffectivePolicyB\x10\n" + "\x0e_decided_at_ms\"\xb1\x01\n" + "\x16CreateWorkspaceRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12H\n" + @@ -14532,7 +14780,7 @@ func file_openshell_proto_rawDescGZIP() []byte { } var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 213) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 214) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase (ProviderCredentialRefreshStrategy)(0), // 1: openshell.v1.ProviderCredentialRefreshStrategy @@ -14697,113 +14945,114 @@ var file_openshell_proto_goTypes = []any{ (*ApproveDraftChunkResponse)(nil), // 160: openshell.v1.ApproveDraftChunkResponse (*RejectDraftChunkRequest)(nil), // 161: openshell.v1.RejectDraftChunkRequest (*RejectDraftChunkResponse)(nil), // 162: openshell.v1.RejectDraftChunkResponse - (*ApproveAllDraftChunksRequest)(nil), // 163: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 164: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 165: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 166: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 167: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 168: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 169: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 170: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 171: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 172: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 173: openshell.v1.GetDraftHistoryResponse - (*PolicyRevisionPayload)(nil), // 174: openshell.v1.PolicyRevisionPayload - (*DraftChunkPayload)(nil), // 175: openshell.v1.DraftChunkPayload - (*StoredPolicyRevision)(nil), // 176: openshell.v1.StoredPolicyRevision - (*StoredDraftChunk)(nil), // 177: openshell.v1.StoredDraftChunk - (*CreateWorkspaceRequest)(nil), // 178: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 179: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 180: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 181: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 182: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 183: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 184: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 185: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 186: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 187: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 188: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 189: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 190: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 191: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 192: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 193: openshell.v1.ExtensionServiceCredential - nil, // 194: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 195: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 196: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 197: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 198: openshell.v1.PlatformEvent.MetadataEntry - nil, // 199: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 200: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 201: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 202: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 203: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - nil, // 204: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - nil, // 205: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - nil, // 206: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - nil, // 207: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 208: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 209: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 210: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - nil, // 211: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 212: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 213: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 214: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 215: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 216: openshell.v1.PolicyRevisionPayload.ProvenanceEntry - nil, // 217: openshell.v1.StoredPolicyRevision.ProvenanceEntry - nil, // 218: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*datamodelv1.ObjectMeta)(nil), // 219: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 220: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 221: google.protobuf.Struct - (*datamodelv1.Provider)(nil), // 222: openshell.datamodel.v1.Provider - (*datamodelv1.CredentialHandle)(nil), // 223: openshell.datamodel.v1.CredentialHandle - (*sandboxv1.NetworkEndpoint)(nil), // 224: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 225: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 226: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 227: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 228: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 229: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 230: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 231: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 232: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 233: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 234: openshell.sandbox.v1.GetGatewayConfigResponse + (*DraftChunkApproval)(nil), // 163: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 164: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 165: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 166: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 167: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 168: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 169: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 170: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 171: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 172: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 173: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 174: openshell.v1.GetDraftHistoryResponse + (*PolicyRevisionPayload)(nil), // 175: openshell.v1.PolicyRevisionPayload + (*DraftChunkPayload)(nil), // 176: openshell.v1.DraftChunkPayload + (*StoredPolicyRevision)(nil), // 177: openshell.v1.StoredPolicyRevision + (*StoredDraftChunk)(nil), // 178: openshell.v1.StoredDraftChunk + (*CreateWorkspaceRequest)(nil), // 179: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 180: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 181: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 182: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 183: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 184: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 185: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 186: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 187: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 188: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 189: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 190: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 191: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 192: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 193: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 194: openshell.v1.ExtensionServiceCredential + nil, // 195: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 196: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 197: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 198: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 199: openshell.v1.PlatformEvent.MetadataEntry + nil, // 200: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 201: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 202: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 203: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 204: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + nil, // 205: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + nil, // 206: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + nil, // 207: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + nil, // 208: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 209: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 210: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 211: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + nil, // 212: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 213: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 214: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 215: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 216: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 217: openshell.v1.PolicyRevisionPayload.ProvenanceEntry + nil, // 218: openshell.v1.StoredPolicyRevision.ProvenanceEntry + nil, // 219: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*datamodelv1.ObjectMeta)(nil), // 220: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 221: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 222: google.protobuf.Struct + (*datamodelv1.Provider)(nil), // 223: openshell.datamodel.v1.Provider + (*datamodelv1.CredentialHandle)(nil), // 224: openshell.datamodel.v1.CredentialHandle + (*sandboxv1.NetworkEndpoint)(nil), // 225: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 226: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 227: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 228: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 229: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 230: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 231: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 232: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 233: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 234: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 235: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 193, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 194, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential 4, // 1: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus 4, // 2: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus 16, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo 17, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 219, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 220, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 19, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec 23, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 194, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 195, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry 22, // 9: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 220, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 221, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy 20, // 11: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements 21, // 12: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 195, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 196, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 197, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 221, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 221, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 196, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 197, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 198, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 222, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 222, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct 24, // 18: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 0, // 19: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 198, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 199, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry 19, // 21: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 199, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 200, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 200, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 201, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry 18, // 24: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox 18, // 25: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 222, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 223, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider 18, // 27: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox 18, // 28: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox 50, // 29: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 219, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 220, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 49, // 31: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 201, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 202, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry 54, // 33: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout 55, // 34: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr 56, // 35: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit @@ -14812,18 +15061,18 @@ var file_openshell_proto_depIdxs = []int32{ 58, // 38: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit 53, // 39: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest 61, // 40: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 219, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 220, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 18, // 42: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox 65, // 43: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine 25, // 44: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent 66, // 45: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning 154, // 46: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 202, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 222, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 222, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 203, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 222, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 222, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 203, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 223, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 223, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 204, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + 223, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 223, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider 96, // 53: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile 78, // 54: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride 83, // 55: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh @@ -14832,25 +15081,25 @@ var file_openshell_proto_depIdxs = []int32{ 81, // 58: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial 82, // 59: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput 1, // 60: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 219, // 61: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 220, // 61: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 1, // 62: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 204, // 63: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - 205, // 64: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - 206, // 65: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + 205, // 63: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + 206, // 64: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + 207, // 65: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry 87, // 66: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion - 223, // 67: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle + 224, // 67: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle 84, // 68: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus 1, // 69: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 207, // 70: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 208, // 70: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry 84, // 71: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus 84, // 72: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus 2, // 73: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory 80, // 74: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 224, // 75: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 225, // 76: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 225, // 75: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 226, // 76: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary 85, // 77: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 208, // 78: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 219, // 79: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 209, // 78: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 220, // 79: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 96, // 80: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile 96, // 81: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile 96, // 82: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile @@ -14863,30 +15112,30 @@ var file_openshell_proto_depIdxs = []int32{ 76, // 89: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem 77, // 90: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic 110, // 91: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 209, // 92: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 210, // 93: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 211, // 94: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 212, // 95: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 220, // 96: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 226, // 97: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 210, // 92: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 211, // 93: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + 212, // 94: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 213, // 95: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 221, // 96: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 227, // 97: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue 114, // 98: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 213, // 99: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 214, // 99: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry 115, // 100: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule 116, // 101: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint 117, // 102: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule 118, // 103: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules 119, // 104: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules 120, // 105: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 227, // 106: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 228, // 107: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 229, // 108: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 214, // 109: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 228, // 106: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 229, // 107: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 230, // 108: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 215, // 109: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry 128, // 110: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision 128, // 111: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision 3, // 112: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus 3, // 113: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 220, // 114: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 215, // 115: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 221, // 114: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 216, // 115: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry 65, // 116: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine 65, // 117: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine 135, // 118: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello @@ -14903,168 +15152,175 @@ var file_openshell_proto_depIdxs = []int32{ 145, // 129: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit 149, // 130: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample 151, // 131: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 227, // 132: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 150, // 133: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 153, // 134: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 152, // 135: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 153, // 136: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 227, // 137: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 172, // 138: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 220, // 139: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 216, // 140: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry - 227, // 141: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 217, // 142: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry - 218, // 143: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 230, // 144: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 230, // 145: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 230, // 146: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 219, // 147: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 5, // 148: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 5, // 149: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 186, // 150: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 186, // 151: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 223, // 152: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle - 80, // 153: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 111, // 154: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 10, // 155: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 12, // 156: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 14, // 157: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 26, // 158: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 27, // 159: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 28, // 160: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 29, // 161: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 30, // 162: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 31, // 163: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 32, // 164: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 33, // 165: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 34, // 166: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 41, // 167: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 43, // 168: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 44, // 169: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 45, // 170: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 47, // 171: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 51, // 172: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 53, // 173: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 59, // 174: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 60, // 175: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 67, // 176: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 68, // 177: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 69, // 178: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 74, // 179: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 75, // 180: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 100, // 181: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 102, // 182: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 104, // 183: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 70, // 184: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 88, // 185: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 90, // 186: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 92, // 187: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 94, // 188: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 71, // 189: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 107, // 190: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 231, // 191: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 232, // 192: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 113, // 193: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 122, // 194: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 124, // 195: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 126, // 196: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 109, // 197: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 129, // 198: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 130, // 199: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 133, // 200: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 140, // 201: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 146, // 202: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 63, // 203: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 155, // 204: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 157, // 205: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 159, // 206: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 161, // 207: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 163, // 208: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 165, // 209: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 167, // 210: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 169, // 211: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 171, // 212: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 6, // 213: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 8, // 214: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 178, // 215: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 180, // 216: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 182, // 217: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 184, // 218: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 187, // 219: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 189, // 220: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 191, // 221: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 11, // 222: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 13, // 223: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 15, // 224: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 35, // 225: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 35, // 226: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 36, // 227: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 37, // 228: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 38, // 229: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 39, // 230: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 40, // 231: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 35, // 232: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 35, // 233: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 42, // 234: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 50, // 235: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 50, // 236: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 46, // 237: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 48, // 238: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 52, // 239: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 57, // 240: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 59, // 241: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 57, // 242: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 72, // 243: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 72, // 244: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 73, // 245: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 99, // 246: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 98, // 247: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 101, // 248: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 103, // 249: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 105, // 250: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 72, // 251: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 89, // 252: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 91, // 253: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 93, // 254: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 95, // 255: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 106, // 256: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 108, // 257: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 233, // 258: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 234, // 259: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 121, // 260: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 123, // 261: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 125, // 262: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 127, // 263: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 112, // 264: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 132, // 265: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 131, // 266: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 134, // 267: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 141, // 268: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 146, // 269: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 64, // 270: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 156, // 271: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 158, // 272: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 160, // 273: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 162, // 274: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 164, // 275: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 166, // 276: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 168, // 277: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 170, // 278: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 173, // 279: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 7, // 280: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 9, // 281: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 179, // 282: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 181, // 283: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 183, // 284: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 185, // 285: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 188, // 286: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 190, // 287: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 192, // 288: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 222, // [222:289] is the sub-list for method output_type - 155, // [155:222] is the sub-list for method input_type - 155, // [155:155] is the sub-list for extension type_name - 155, // [155:155] is the sub-list for extension extendee - 0, // [0:155] is the sub-list for field type_name + 228, // 132: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 221, // 133: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 221, // 134: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 150, // 135: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 153, // 136: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 152, // 137: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 153, // 138: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 163, // 139: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 228, // 140: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 173, // 141: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 221, // 142: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 217, // 143: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry + 228, // 144: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 221, // 145: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 221, // 146: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 218, // 147: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry + 221, // 148: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 221, // 149: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 219, // 150: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 231, // 151: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 231, // 152: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 231, // 153: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 220, // 154: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 5, // 155: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 5, // 156: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 187, // 157: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 187, // 158: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 224, // 159: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle + 80, // 160: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 111, // 161: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 10, // 162: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 12, // 163: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 14, // 164: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 26, // 165: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 27, // 166: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 28, // 167: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 29, // 168: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 30, // 169: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 31, // 170: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 32, // 171: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 33, // 172: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 34, // 173: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 41, // 174: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 43, // 175: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 44, // 176: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 45, // 177: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 47, // 178: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 51, // 179: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 53, // 180: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 59, // 181: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 60, // 182: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 67, // 183: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 68, // 184: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 69, // 185: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 74, // 186: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 75, // 187: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 100, // 188: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 102, // 189: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 104, // 190: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 70, // 191: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 88, // 192: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 90, // 193: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 92, // 194: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 94, // 195: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 71, // 196: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 107, // 197: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 232, // 198: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 233, // 199: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 113, // 200: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 122, // 201: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 124, // 202: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 126, // 203: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 109, // 204: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 129, // 205: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 130, // 206: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 133, // 207: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 140, // 208: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 146, // 209: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 63, // 210: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 155, // 211: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 157, // 212: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 159, // 213: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 161, // 214: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 164, // 215: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 166, // 216: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 168, // 217: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 170, // 218: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 172, // 219: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 6, // 220: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 8, // 221: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 179, // 222: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 181, // 223: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 183, // 224: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 185, // 225: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 188, // 226: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 190, // 227: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 192, // 228: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 11, // 229: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 13, // 230: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 15, // 231: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 35, // 232: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 35, // 233: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 36, // 234: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 37, // 235: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 38, // 236: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 39, // 237: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 40, // 238: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 35, // 239: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 35, // 240: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 42, // 241: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 50, // 242: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 50, // 243: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 46, // 244: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 48, // 245: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 52, // 246: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 57, // 247: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 59, // 248: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 57, // 249: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 72, // 250: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 72, // 251: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 73, // 252: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 99, // 253: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 98, // 254: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 101, // 255: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 103, // 256: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 105, // 257: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 72, // 258: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 89, // 259: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 91, // 260: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 93, // 261: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 95, // 262: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 106, // 263: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 108, // 264: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 234, // 265: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 235, // 266: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 121, // 267: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 123, // 268: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 125, // 269: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 127, // 270: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 112, // 271: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 132, // 272: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 131, // 273: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 134, // 274: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 141, // 275: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 146, // 276: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 64, // 277: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 156, // 278: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 158, // 279: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 160, // 280: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 162, // 281: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 165, // 282: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 167, // 283: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 169, // 284: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 171, // 285: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 174, // 286: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 7, // 287: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 9, // 288: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 180, // 289: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 182, // 290: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 184, // 291: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 186, // 292: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 189, // 293: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 191, // 294: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 193, // 295: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 229, // [229:296] is the sub-list for method output_type + 162, // [162:229] is the sub-list for method input_type + 162, // [162:162] is the sub-list for extension type_name + 162, // [162:162] is the sub-list for extension extendee + 0, // [0:162] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -15130,15 +15386,15 @@ func file_openshell_proto_init() { (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[170].OneofWrappers = []any{} file_openshell_proto_msgTypes[171].OneofWrappers = []any{} + file_openshell_proto_msgTypes[172].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), NumEnums: 6, - NumMessages: 213, + NumMessages: 214, NumExtensions: 0, NumServices: 1, }, diff --git a/tasks/test.toml b/tasks/test.toml index 9f47d0dcda..a796ea67b4 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -219,6 +219,13 @@ run = [ "e2e/with-docker-gateway.sh bash -lc 'target/debug/openshell settings set --global --key agent_policy_proposals_enabled --value true --yes && OPENSHELL_BIN=$PWD/target/debug/openshell bash e2e/policy-advisor/mechanistic-smoke.sh'", ] +["e2e:mechanistic-existing-endpoint"] +description = "Run #2821 existing inspected-endpoint auto-approval regression" +run = [ + "cargo build -p openshell-cli", + "e2e/with-docker-gateway.sh bash -lc 'target/debug/openshell settings set --global --key agent_policy_proposals_enabled --value true --yes && OPENSHELL_BIN=$PWD/target/debug/openshell bash e2e/policy-advisor/existing-endpoint-auto-approve.sh'", +] + ["e2e:docker:gpu"] description = "Run GPU e2e against a standalone gateway with the Docker compute driver" env = { OPENSHELL_E2E_DOCKER_GPU = "1", OPENSHELL_E2E_DOCKER_TEST = "gpu", OPENSHELL_E2E_DOCKER_FEATURES = "e2e-docker-gpu" }