Repository navigation
Expand file tree
/
Copy pathkernel_pipeline.rs
More file actions
97 lines (95 loc) · 3.72 KB
/
Copy pathkernel_pipeline.rs
File metadata and controls
97 lines (95 loc) · 3.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//! Admission ordering and recorder evidence through the canonical entrypoint.
use crate::auth::{AuthContext, AuthKind};
use crate::tool_runtime::kernel::{
HostFileImportTrust, ToolCallContext, ToolCallErrorStatus, ToolCallRequest, ToolTransport,
};
use crate::tool_runtime::sessions::{SessionCreateOptions, SessionGuards};
use crate::tool_runtime::{SessionMode, ToolRuntime};
use serde_json::json;
#[tokio::test]
async fn scope_denial_preserves_each_transports_recording_and_error_precedence() {
for (transport, record_denial) in [(ToolTransport::Mcp, false), (ToolTransport::Api, true)] {
let runtime = ToolRuntime::new_for_tests();
let auth = AuthContext {
user_id: Some("recorder-owner".into()),
scopes: vec!["runtime:read".into()],
..AuthContext::new(AuthKind::OAuth2Token)
};
let fingerprint =
crate::tool_runtime::workflow_session_authority_fingerprint(Some(&auth)).unwrap();
let session = runtime
.sessions
.start_session_with_options(
SessionCreateOptions::new(
None,
None,
SessionMode::Normal,
SessionGuards::default(),
)
.with_owner_authority_fingerprint(Some(fingerprint)),
)
.unwrap();
let outcome = runtime
.call_tool_with_context(
ToolCallRequest {
tool_name: "read_files".into(),
// Deliberately invalid too: scope denial must win on both paths.
arguments: json!({}),
},
ToolCallContext {
transport,
session_id: Some(&session.session_id),
auth: Some(&auth),
window: None,
record_oauth_scope_denials: record_denial,
host_file_import_trust: HostFileImportTrust::Untrusted,
},
)
.await;
assert!(matches!(
outcome.error_status,
Some(ToolCallErrorStatus::InsufficientScope { .. })
));
assert!(outcome.result.is_none());
let summary = runtime
.sessions
.summary(&session.session_id, Some(10))
.unwrap();
assert_eq!(summary.counts.tool_calls, usize::from(record_denial));
if record_denial {
assert_eq!(summary.counts.failed, 1);
assert_eq!(summary.events.len(), 2);
assert_eq!(
summary.events[1].error_kind.as_deref(),
Some("insufficient_scope")
);
} else {
assert!(summary.events.is_empty());
}
}
}
#[tokio::test]
async fn missing_app_capability_is_rejected_before_recorder_lookup_or_business_parsing() {
let runtime = ToolRuntime::new_for_tests();
let outcome = runtime
.call_tool_with_context(
ToolCallRequest {
tool_name: "get_work_result_state".into(),
arguments: json!({}),
},
ToolCallContext {
transport: ToolTransport::Api,
session_id: Some("~s999999"),
auth: None,
window: None,
record_oauth_scope_denials: true,
host_file_import_trust: HostFileImportTrust::Untrusted,
},
)
.await;
assert_eq!(outcome.error_status, Some(ToolCallErrorStatus::InvalidArguments {
message: "Work Result App operations are available only on Stateless MCP 2026 requests with Work Result App capability".into(),
}));
assert!(outcome.result.is_none());
assert!(outcome.canonical_audit_output.is_none());
}