-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlease_violation.rs
More file actions
153 lines (137 loc) · 5.34 KB
/
Copy pathlease_violation.rs
File metadata and controls
153 lines (137 loc) · 5.34 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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! ARCP v1.1 §9.5 — lease violation handling.
//!
//! A lease grants time-limited access to a named resource set. If an agent
//! attempts to access a resource that is NOT covered by its current lease,
//! the runtime rejects the action with `LEASE_VIOLATION` (or revokes the
//! lease and emits `lease.revoked` with reason `violated`).
//!
//! This example demonstrates three scenarios:
//! 1. Normal use — agent stays within its granted resource set.
//! 2. Over-reach — agent attempts a resource outside the lease scope;
//! the runtime emits `lease.revoked` with `reason: "violated"`.
//! 3. Expired lease — the agent holds an expired lease and attempts
//! a resource access; the runtime rejects with `LEASE_EXPIRED`.
//!
//! Run with:
//! `cargo run --example lease_violation`
#![allow(
clippy::todo,
clippy::unimplemented,
clippy::panic,
clippy::unwrap_used,
clippy::expect_used,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::doc_markdown,
clippy::needless_pass_by_value,
clippy::too_many_arguments,
clippy::unused_async,
clippy::diverging_sub_expression,
clippy::no_effect_underscore_binding,
clippy::let_unit_value,
clippy::used_underscore_binding,
clippy::let_underscore_untyped,
clippy::struct_field_names,
clippy::manual_let_else,
clippy::map_unwrap_or,
clippy::redundant_pub_crate,
dead_code,
unreachable_code,
unused_assignments,
unused_mut,
unused_imports,
unused_variables
)]
use std::env;
use arcp::error::ARCPError;
use arcp::transport::MemoryTransport;
use arcp::ARCPClient;
use serde_json::json;
type Client = ARCPClient<MemoryTransport>;
/// Submit a job requesting a `net.fetch` lease scoped to `allowed_urls`.
/// Returns `(job_id, lease_id)`.
async fn submit_fetcher(
_client: &Client,
_allowed_urls: &[&str],
) -> Result<(String, String), ARCPError> {
// client.request(envelope("tool.invoke", {
// tool: "url-fetcher",
// arguments: {},
// lease_request: {resources: {"net.fetch": allowed_urls}},
// })) -> (job_id from job.accepted, lease_id from lease.granted)
todo!()
}
/// Tell the running agent (via the runtime) to fetch `url`.
/// Returns `Ok` when the fetch is permitted, `Err(LeaseViolation)` when
/// the runtime rejects the resource as out-of-scope.
async fn agent_fetch(_client: &Client, _job_id: &str, _url: &str) -> Result<String, ARCPError> {
// client.request(envelope("tool.invoke", {
// target_job_id: job_id,
// tool: "url-fetcher.fetch",
// arguments: {url: url},
// }))
todo!()
}
async fn await_revoked_event(_client: &Client, _lease_id: &str) -> Result<String, ARCPError> {
// for await env in client.events():
// if env.type == "lease.revoked" and env.payload.lease_id == lease_id:
// return env.payload.reason
todo!()
}
async fn scenario_normal(client: &Client) -> Result<(), ARCPError> {
let allowed = &["https://example.com/**"];
let (job_id, lease_id) = submit_fetcher(client, allowed).await?;
println!("[normal] job={job_id} lease={lease_id}");
let body = agent_fetch(client, &job_id, "https://example.com/index.html").await?;
println!("[normal] fetch succeeded, len={}", body.len());
Ok(())
}
async fn scenario_over_reach(client: &Client) -> Result<(), ARCPError> {
let allowed = &["https://example.com/**"];
let (job_id, lease_id) = submit_fetcher(client, allowed).await?;
println!("[over-reach] job={job_id} lease={lease_id}");
// Attempt a URL that is outside the granted scope.
let result = agent_fetch(client, &job_id, "https://evil.example.net/data").await;
match result {
Err(ARCPError::LeaseSubsetViolation { .. }) => {
let reason = await_revoked_event(client, &lease_id).await?;
println!("[over-reach] lease revoked with reason={reason} — expected");
}
other => {
return Err(ARCPError::Unknown {
detail: format!("expected LeaseSubsetViolation but got {other:?}"),
});
}
}
Ok(())
}
async fn scenario_expired_lease(client: &Client) -> Result<(), ARCPError> {
// Request a lease with a 0-second TTL so it expires immediately.
let allowed = &["https://example.com/**"];
let (job_id, lease_id) = submit_fetcher(client, allowed).await?;
// Force expiry by waiting briefly; in tests a clock injection is used.
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let result = agent_fetch(client, &job_id, "https://example.com/data").await;
match result {
Err(ARCPError::LeaseExpired { .. }) => {
println!("[expired] access after expiry rejected — expected");
}
other => eprintln!("[expired] unexpected outcome: {other:?}"),
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client: Client = todo!(); // transport, identity, auth elided
match env::args().nth(1).as_deref().unwrap_or("normal") {
"normal" => scenario_normal(&client).await?,
"over-reach" | "over_reach" => scenario_over_reach(&client).await?,
"expired" => scenario_expired_lease(&client).await?,
other => {
eprintln!("unknown scenario: {other} (normal|over-reach|expired)");
std::process::exit(2);
}
}
println!("done");
Ok(())
}