diff --git a/CHANGELOG.md b/CHANGELOG.md index 4564d5d2..852c33b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 metadata now lives in `commands/registry.rs`, slash parsing in `commands/parse.rs`, and handlers under group-owned command areas, preserving the existing dispatch surface while reducing future `commands/mod.rs` churn. +- **Approval-rule source metadata (#1186/#2971).** Runtime API + `approval.required` events now include optional `matched_rule` metadata when + an execution-policy rule caused the prompt. Thanks @greyfreedom for the PR + and @Ram9199 for the audit-semantics discussion. ### Fixed diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 900e16b2..75382f9d 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1156,6 +1156,7 @@ impl Runtime { let reason = decision.reason().to_string(); let maybe_approval_frame = approval_request_frame( &decision.requirement, + decision.matched_rule.as_deref(), call_id, approval_id.clone(), response_id.clone(), @@ -1578,6 +1579,7 @@ fn to_persisted_source(source: &codewhale_protocol::SessionSource) -> SessionSou fn approval_request_frame( requirement: &ExecApprovalRequirement, + matched_rule: Option<&str>, call_id: String, approval_id: String, turn_id: String, @@ -1620,6 +1622,7 @@ fn approval_request_frame( command, cwd, reason: reason.clone(), + matched_rule: matched_rule.map(|rule| rule.to_string().into_boxed_str()), network_approval_context: None, proposed_execpolicy_amendment: proposed_execpolicy_amendment .as_ref() @@ -1886,6 +1889,36 @@ mod tests { assert_eq!(permission_path_for_call(&call), None); } + #[test] + fn approval_request_frame_includes_matched_rule() { + let requirement = ExecApprovalRequirement::NeedsApproval { + reason: "Typed ask rule 'tool=exec_shell command=cargo test' requires approval." + .to_string(), + proposed_execpolicy_amendment: None, + proposed_network_policy_amendments: Vec::new(), + }; + + let frame = approval_request_frame( + &requirement, + Some("tool=exec_shell command=cargo test"), + "call-1".to_string(), + "approval-1".to_string(), + "turn-1".to_string(), + "cargo test --workspace".to_string(), + "/repo".to_string(), + ) + .expect("approval frame"); + + let EventFrame::ExecApprovalRequest { request } = frame else { + panic!("expected exec approval request frame"); + }; + assert_eq!( + request.matched_rule.as_deref(), + Some("tool=exec_shell command=cargo test") + ); + assert_eq!(request.reason, requirement.reason()); + } + #[test] fn enqueue_creates_queued_job_with_zero_progress() { let mut jm = JobManager::default(); diff --git a/crates/hooks/src/lib.rs b/crates/hooks/src/lib.rs index 60ca14f1..d28525be 100644 --- a/crates/hooks/src/lib.rs +++ b/crates/hooks/src/lib.rs @@ -15,6 +15,7 @@ use tokio::io::AsyncWriteExt; /// serialised with a `"type"` discriminator using `snake_case` naming (e.g. /// `"response_start"`, `"tool_lifecycle"`), making it easy to consume from /// JSON-based log files or webhook receivers. +#[allow(clippy::large_enum_variant)] // Keep the public HookEvent shape stable for 0.8.x. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum HookEvent { diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 02f697fd..2e940e19 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -472,6 +472,9 @@ pub struct ExecApprovalRequestEvent { pub cwd: String, /// Human-readable reason why approval is needed. pub reason: String, + /// Policy rule that matched this approval request, when available. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub matched_rule: Option>, /// Network context if the approval involves network access. #[serde(skip_serializing_if = "Option::is_none")] pub network_approval_context: Option, diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 87138b1e..bfbdd648 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -28,6 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 metadata now lives in `commands/registry.rs`, slash parsing in `commands/parse.rs`, and handlers under group-owned command areas, preserving the existing dispatch surface while reducing future `commands/mod.rs` churn. +- **Approval-rule source metadata (#1186/#2971).** Runtime API + `approval.required` events now include optional `matched_rule` metadata when + an execution-policy rule caused the prompt. Thanks @greyfreedom for the PR + and @Ram9199 for the audit-semantics discussion. ### Fixed diff --git a/docs/RUNTIME_API.md b/docs/RUNTIME_API.md index 454cb189..59c4d301 100644 --- a/docs/RUNTIME_API.md +++ b/docs/RUNTIME_API.md @@ -407,6 +407,10 @@ Common event names: `thread.started`, `thread.forked`, `turn.started`, `item.failed`, `item.interrupted`, `approval.required`, `approval.decided`, `approval.timeout`, `sandbox.denied`, `coherence.state`. +`approval.required` events may include a `matched_rule` string when an +execution-policy rule caused the prompt. This field is explanatory metadata for +clients and does not grant or persist permissions. + ## Security boundary - **Localhost by default**. The server binds to `127.0.0.1` by default.