feat(runtime): expose matched approval rule metadata

Harvests the explainability slice from PR #2971 without changing the public HookEvent constructor shape. Runtime API approval.required frames now carry matched_rule metadata when an execpolicy rule caused the prompt.

Co-authored-by: greyfreedom <11493871+greyfreedom@users.noreply.github.com>
This commit is contained in:
Hunter B
2026-06-12 01:46:41 -07:00
parent c16a32150e
commit 10e41b1153
6 changed files with 49 additions and 0 deletions
+4
View File
@@ -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
+33
View File
@@ -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();
+1
View File
@@ -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 {
+3
View File
@@ -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<Box<str>>,
/// Network context if the approval involves network access.
#[serde(skip_serializing_if = "Option::is_none")]
pub network_approval_context: Option<NetworkApprovalContext>,
+4
View File
@@ -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
+4
View File
@@ -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.