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:
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user