1use std::net::SocketAddr;
2#[cfg(unix)]
3use std::os::unix::fs::PermissionsExt;
4use std::path::PathBuf;
5
6use axum::extract::{Path, Query, Request, State};
7use axum::http::{header, HeaderValue, StatusCode};
8use axum::middleware::{self, Next};
9use axum::response::{IntoResponse, Response};
10use axum::routing::{delete, get, post};
11use axum::{Json, Router};
12use serde::{Deserialize, Serialize};
13use tower_http::cors::{AllowOrigin, CorsLayer};
14
15use crate::ai::agent::{Agent, LlmPlanner};
16use crate::ai::Orchestrator;
17use crate::core::framework::SharedFramework;
18use crate::core::governance::{
19 audit_to_csv, role_allows, ApprovalRequest, ApprovalStatus, PolicyPack, Role,
20};
21use crate::core::job::Job;
22use crate::core::safety::{
23 Charter, DecisionRecord, Evidence, MemoryEntry, PolicyContext, PolicyDecision, PolicyEngine,
24 PolicyRule, PolicySet, Preflight, ReasoningTrace, RiskLevel,
25};
26use crate::core::sdk::{ActionOutcome, GovernAction, GovernResult, RecordResult};
27use crate::core::session::{Session, SessionId, SessionKind};
28use crate::core::workspace::WorkspaceSnapshot;
29
30#[derive(Serialize)]
31struct ModuleItem {
32 name: String,
33 kind: String,
34 description: String,
35}
36
37#[derive(Serialize)]
38struct ModuleDetail {
39 name: String,
40 kind: String,
41 description: String,
42 author: String,
43 options: serde_json::Value,
44 charter_accepted: bool,
45}
46
47#[derive(Deserialize)]
48struct SetPayload {
49 key: String,
50 value: String,
51}
52
53#[derive(Deserialize, Default)]
54struct RunPayload {
55 target: String,
56 #[serde(default)]
57 approved: bool,
58 #[serde(default)]
59 sandbox: bool,
60 #[serde(default)]
61 engine: Option<String>,
62 #[serde(default)]
63 options: std::collections::HashMap<String, serde_json::Value>,
64}
65
66#[derive(Serialize)]
67struct RunResponse {
68 job_id: u64,
69 session_id: Option<String>,
70 success: bool,
71 data: serde_json::Value,
72 preflight: Option<PreflightReport>,
73 error: Option<String>,
74}
75
76#[derive(Serialize)]
77struct PreflightReport {
78 in_scope: bool,
79 charter_accepted: bool,
80 risk: String,
81 destructive: bool,
82 approved: bool,
83 passed: bool,
84 reason: Option<String>,
85}
86
87#[derive(Deserialize)]
88struct CharterPayload {
89 engagement: String,
90}
91
92#[derive(Serialize)]
93struct CharterStatus {
94 accepted: bool,
95 engagement: String,
96}
97
98#[derive(Deserialize)]
99struct ScopePayload {
100 target: String,
101}
102
103#[derive(Serialize)]
104struct SessionItem {
105 id: u64,
106 kind: String,
107 target: String,
108 module: String,
109 elapsed_secs: u64,
110}
111
112#[derive(Deserialize)]
113struct AgentRunPayload {
114 target: String,
115 model: Option<String>,
116 #[serde(default)]
117 approve: bool,
118}
119
120#[derive(Deserialize)]
121struct OrchestratePayload {
122 targets: Vec<String>,
123 model: Option<String>,
124 #[serde(default)]
125 approve: bool,
126}
127
128#[derive(Serialize)]
129struct AgentRunResponse {
130 summary: String,
131 actions_taken: Vec<String>,
132 sessions_opened: Vec<u64>,
133 job_ids: Vec<u64>,
134 report: String,
135}
136
137#[derive(Serialize)]
138struct JobItem {
139 id: u64,
140 module: String,
141 target: String,
142 status: String,
143 elapsed_secs: u64,
144}
145
146#[derive(Deserialize)]
147struct PolicyQuery {
148 module: Option<String>,
149 target: Option<String>,
150}
151
152#[derive(Deserialize)]
153struct AuditQuery {
154 n: Option<usize>,
155 format: Option<String>,
156}
157
158#[derive(Deserialize)]
159struct EvidenceQuery {
160 n: Option<usize>,
161 #[serde(default)]
162 min_confidence: Option<f64>,
163 kind: Option<String>,
164}
165
166#[derive(Deserialize)]
167struct SetModePayload {
168 mode: String,
169}
170
171#[derive(Clone)]
172pub struct AuthState {
173 pub token: Option<String>,
174}
175
176fn home_dir() -> PathBuf {
177 std::env::var("HOME")
178 .map(PathBuf::from)
179 .unwrap_or_else(|_| PathBuf::from("."))
180}
181
182fn random_token() -> String {
183 let mut buf = [0u8; 24];
184 if let Ok(bytes) = std::fs::read("/dev/urandom") {
185 let n = bytes.len().min(buf.len());
186 buf[..n].copy_from_slice(&bytes[..n]);
187 }
188 hex::encode(buf)
189}
190
191pub fn resolve_auth(no_auth: bool, explicit: Option<String>) -> AuthState {
192 if no_auth {
193 return AuthState { token: None };
194 }
195 if let Some(t) = explicit {
196 return AuthState { token: Some(t) };
197 }
198 let path = home_dir().join(".icebox").join("auth.token");
199 if let Ok(t) = std::fs::read_to_string(&path) {
200 let t = t.trim().to_string();
201 if !t.is_empty() {
202 return AuthState { token: Some(t) };
203 }
204 }
205 let t = random_token();
206 if let Some(parent) = path.parent() {
207 let _ = std::fs::create_dir_all(parent);
208 }
209 if std::fs::write(&path, &t).is_ok() {
210 #[cfg(unix)]
211 let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
212 }
213 AuthState { token: Some(t) }
214}
215
216async fn require_auth(req: Request, next: Next) -> Result<Response, StatusCode> {
217 let token = req.extensions().get::<AuthState>().cloned();
218 if let Some(AuthState {
219 token: Some(expected),
220 }) = token
221 {
222 let ok = req
223 .headers()
224 .get(header::AUTHORIZATION)
225 .and_then(|v| v.to_str().ok())
226 .map(|v| v.trim_start_matches("Bearer ") == expected)
227 .unwrap_or(false);
228 if !ok {
229 return Err(StatusCode::UNAUTHORIZED);
230 }
231 }
232 Ok(next.run(req).await)
233}
234
235pub async fn serve(fw: SharedFramework, addr: SocketAddr, auth: AuthState) -> anyhow::Result<()> {
236 let auth_layer = middleware::from_fn(move |mut req: Request, next: Next| {
237 let auth = auth.clone();
238 async move {
239 req.extensions_mut().insert(auth);
240 require_auth(req, next).await
241 }
242 });
243 let cors = CorsLayer::new().allow_origin(AllowOrigin::list([
244 HeaderValue::from_static("http://127.0.0.1:8443"),
245 HeaderValue::from_static("http://localhost:8443"),
246 ]));
247 let app = Router::new()
248 .route("/api/v1/modules", get(list_modules))
249 .route("/api/v1/modules/{name}", get(get_module))
250 .route("/api/v1/modules/{name}/set", post(set_option))
251 .route("/api/v1/modules/{name}/run", post(run_module))
252 .route("/api/v1/sessions", get(list_sessions))
253 .route("/api/v1/sessions/{id}/close", post(close_session))
254 .route("/api/v1/jobs", get(list_jobs))
255 .route("/api/v1/agent/run", post(run_agent))
256 .route("/api/v1/orchestrate", post(run_orchestrate))
257 .route("/api/v1/charter", get(get_charter).post(accept_charter))
258 .route("/api/v1/scope", get(get_scope).post(add_scope))
259 .route("/api/v1/policy", get(evaluate_policy))
260 .route(
261 "/api/v1/policy/rules",
262 get(list_policy_rules)
263 .post(add_policy_rule)
264 .put(replace_policy_rules),
265 )
266 .route("/api/v1/policy/rules/{index}", delete(delete_policy_rule))
267 .route("/api/v1/workspace/save", post(save_workspace))
268 .route("/api/v1/workspace/load", post(load_workspace))
269 .route("/api/v1/audit", get(list_audit))
270 .route("/api/v1/audit/export", get(export_audit))
271 .route(
272 "/api/v1/policy/packs",
273 get(list_policy_packs).post(add_policy_pack),
274 )
275 .route("/api/v1/policy/pack/{name}/apply", post(apply_policy_pack))
276 .route(
277 "/api/v1/approvals",
278 get(list_approvals).post(request_approval),
279 )
280 .route("/api/v1/approvals/{id}/approve", post(approve_approval))
281 .route("/api/v1/approvals/{id}/deny", post(deny_approval))
282 .route("/api/v1/role", get(get_role).post(set_role))
283 .route("/api/v1/evidence", get(list_evidence))
284 .route("/api/v1/traces", get(list_traces))
285 .route("/api/v1/memory", get(list_memory))
286 .route("/api/v1/mode", get(get_mode).post(set_mode))
287 .route("/api/v1/proxy/bind", post(bind_proxy))
288 .route("/api/v1/proxy/unbind", post(unbind_proxy))
289 .route("/api/v1/govern", post(govern_handler))
290 .route("/api/v1/govern/record", post(record_handler))
291 .layer(auth_layer)
292 .layer(cors)
293 .with_state(fw);
294
295 let listener = tokio::net::TcpListener::bind(addr).await?;
296 eprintln!("REST API listening on {}", listener.local_addr()?);
297 axum::serve(listener, app).await?;
298 Ok(())
299}
300
301fn to_preflight_report(pf: &Preflight, policy: &dyn PolicyEngine) -> PreflightReport {
302 let check = pf.check(policy);
303 PreflightReport {
304 in_scope: pf.in_scope,
305 charter_accepted: pf.charter_accepted,
306 risk: pf.risk.as_str().into(),
307 destructive: pf.destructive,
308 approved: pf.approved,
309 passed: check.is_ok(),
310 reason: check.err().map(|e| e.to_string()),
311 }
312}
313
314async fn list_modules(State(_fw): State<SharedFramework>) -> Json<Vec<ModuleItem>> {
315 let items = crate::modules::discover()
316 .into_iter()
317 .map(|e| {
318 let i = (e.info)();
319 ModuleItem {
320 name: i.name,
321 kind: i.kind.as_str().into(),
322 description: i.description,
323 }
324 })
325 .collect();
326 Json(items)
327}
328
329async fn get_module(
330 State(fw): State<SharedFramework>,
331 Path(name): Path<String>,
332) -> Json<Option<ModuleDetail>> {
333 let fw = fw.lock().await;
334 let loaded = match crate::modules::load(&name) {
335 Some(l) => l,
336 None => return Json(None),
337 };
338 let pf = fw
339 .executor
340 .preflight(&loaded, "", None, false, PolicyContext::Rest)
341 .await;
342 Json(Some(ModuleDetail {
343 name: loaded.info.name.clone(),
344 kind: loaded.info.kind.as_str().into(),
345 description: loaded.info.description.clone(),
346 author: loaded.info.author.clone(),
347 options: loaded.module.options_json(),
348 charter_accepted: pf.charter_accepted,
349 }))
350}
351
352async fn set_option(
353 State(fw): State<SharedFramework>,
354 Path(name): Path<String>,
355 Json(payload): Json<SetPayload>,
356) -> Json<Result<String, String>> {
357 if !role_allows(fw.lock().await.operator_role, Role::Operator) {
358 return Json(Err("forbidden: operator role required".into()));
359 }
360 let Some(mut loaded) = crate::modules::load(&name) else {
361 return Json(Err("module not found".into()));
362 };
363 match loaded.module.set_option(&payload.key, &payload.value) {
364 Ok(_) => Json(Ok(format!("{}={}", payload.key, payload.value))),
365 Err(e) => Json(Err(e.to_string())),
366 }
367}
368
369async fn set_mode(
370 State(fw): State<SharedFramework>,
371 Json(payload): Json<SetModePayload>,
372) -> Result<Json<String>, StatusCode> {
373 let mut fw = fw.lock().await;
374 if !role_allows(fw.operator_role, Role::Operator) {
375 return Err(StatusCode::FORBIDDEN);
376 }
377
378 let mode_rules: Vec<PolicyRule> = match payload.mode.to_lowercase().as_str() {
379 "fridge" => vec![PolicyRule::MaxRisk(RiskLevel::Low)],
380 "freezer" => vec![
381 PolicyRule::MaxRisk(RiskLevel::High),
382 PolicyRule::DenyIfCvssAbove(7.0),
383 ],
384 "deep_freezer" | "deep freezer" | "deep-freezer" => vec![
385 PolicyRule::MaxRisk(RiskLevel::Critical),
386 PolicyRule::RequireApprovalIf {
387 cvss_above: Some(0.0),
388 epss_above: None,
389 kev: false,
390 },
391 ],
392 _ => return Err(StatusCode::BAD_REQUEST),
393 };
394
395 let kept: Vec<PolicyRule> = fw
396 .executor
397 .policy_set
398 .rules
399 .iter()
400 .filter(|r| {
401 !matches!(
402 r,
403 PolicyRule::MaxRisk(_)
404 | PolicyRule::DenyIfCvssAbove(_)
405 | PolicyRule::RequireApprovalIf { .. }
406 )
407 })
408 .cloned()
409 .collect();
410 let mut rules = kept;
411 rules.extend(mode_rules);
412 fw.executor.policy_set.rules = rules;
413 fw.executor.policy_set.version += 1;
414
415 Ok(Json(format!("mode set to {}", payload.mode)))
416}
417
418async fn get_mode(State(fw): State<SharedFramework>) -> Json<serde_json::Value> {
419 let fw = fw.lock().await;
420
421 let max_risk = fw.executor.policy_set.max_risk(RiskLevel::High);
423 let has_cvss = fw.executor.policy_set.has_cvss_rules();
424
425 let mode = if max_risk == RiskLevel::Critical {
426 "deep_freezer"
427 } else if max_risk == RiskLevel::High && has_cvss {
428 "freezer"
429 } else if max_risk == RiskLevel::Low {
430 "fridge"
431 } else {
432 "custom"
433 };
434
435 Json(serde_json::json!({
436 "mode": mode
437 }))
438}
439
440async fn run_module(
441 State(fw): State<SharedFramework>,
442 Path(name): Path<String>,
443 Json(payload): Json<RunPayload>,
444) -> Json<RunResponse> {
445 let Some(mut loaded) = crate::modules::load(&name) else {
446 return Json(RunResponse {
447 job_id: 0,
448 session_id: None,
449 success: false,
450 data: serde_json::Value::Null,
451 preflight: None,
452 error: Some("module not found".into()),
453 });
454 };
455
456 let mut fw = fw.lock().await;
457 if !role_allows(fw.operator_role, Role::Operator) {
458 return Json(RunResponse {
459 job_id: 0,
460 session_id: None,
461 success: false,
462 data: serde_json::Value::Null,
463 preflight: None,
464 error: Some("forbidden: operator role required".into()),
465 });
466 }
467 for (k, v) in &payload.options {
468 let s = match v {
469 serde_json::Value::String(s) => s.clone(),
470 other => other.to_string(),
471 };
472 let _ = loaded.module.set_option(k, &s);
473 }
474 let pf = fw
475 .executor
476 .preflight(
477 &loaded,
478 &payload.target,
479 None,
480 payload.approved,
481 PolicyContext::Rest,
482 )
483 .await;
484 let policy = fw.executor.policy(PolicyContext::Rest);
485 let report = to_preflight_report(&pf, &policy);
486 if let Err(ref e) = pf.check(&policy) {
487 return Json(RunResponse {
488 job_id: 0,
489 session_id: None,
490 success: false,
491 data: serde_json::Value::Null,
492 preflight: Some(report),
493 error: Some(e.to_string()),
494 });
495 }
496
497 let job = Job::new(&loaded.info.name, &payload.target);
498 let job_id = job.id;
499 fw.jobs.register(job);
500
501 let engine = if let Some(e) = payload.engine {
502 match e.to_lowercase().as_str() {
503 "docker" => Some(crate::core::sandbox::SandboxEngineType::Docker),
504 "firecracker" => {
505 return Json(RunResponse {
506 job_id: 0,
507 session_id: None,
508 success: false,
509 data: serde_json::Value::Null,
510 preflight: None,
511 error: Some(
512 "Firecracker is not supported for module execution; use docker".into(),
513 ),
514 })
515 }
516 _ => {
517 return Json(RunResponse {
518 job_id: 0,
519 session_id: None,
520 success: false,
521 data: serde_json::Value::Null,
522 preflight: None,
523 error: Some(format!("unknown engine {e}")),
524 })
525 }
526 }
527 } else {
528 None
529 };
530
531 let result = fw
532 .executor
533 .execute(
534 &mut loaded,
535 &payload.target,
536 None,
537 payload.approved,
538 PolicyContext::Rest,
539 Some(job_id.as_u64()),
540 payload.sandbox,
541 engine,
542 )
543 .await;
544
545 match result {
546 Ok(r) => {
547 fw.jobs.complete(job_id, r.clone());
548 let mut sid = None;
549 if let Some(ref s) = r.session_id {
550 let kind = if s.starts_with("session:") {
551 SessionKind::Shell
552 } else {
553 SessionKind::Unknown
554 };
555 let new_sid =
556 fw.sessions
557 .register(Session::new(kind, &payload.target, &loaded.info.name));
558 sid = Some(new_sid.to_string());
559 }
560 Json(RunResponse {
561 job_id: job_id.as_u64(),
562 session_id: sid,
563 success: r.success,
564 data: r.data.clone(),
565 preflight: Some(report),
566 error: r.error.clone(),
567 })
568 }
569 Err(e) => {
570 fw.jobs.cancel(job_id);
571 Json(RunResponse {
572 job_id: job_id.as_u64(),
573 session_id: None,
574 success: false,
575 data: serde_json::Value::Null,
576 preflight: Some(report),
577 error: Some(e.to_string()),
578 })
579 }
580 }
581}
582
583async fn run_agent(
584 State(fw): State<SharedFramework>,
585 Json(payload): Json<AgentRunPayload>,
586) -> Json<AgentRunResponse> {
587 let model = payload.model.unwrap_or_else(|| "llama3.2".into());
588 if !role_allows(fw.lock().await.operator_role, Role::Operator) {
589 return Json(AgentRunResponse {
590 summary: "forbidden: operator role required".into(),
591 actions_taken: vec![],
592 sessions_opened: vec![],
593 job_ids: vec![],
594 report: String::new(),
595 });
596 }
597 let planner = Box::new(LlmPlanner::new(&model));
598 let mut agent = Agent::new(planner, fw, payload.target.clone(), RiskLevel::High);
599 agent.set_approved(true);
600 if payload.approve {
601 agent.set_plan_approver(Box::new(crate::ai::agent::AlwaysApprove));
602 } else {
603 agent.set_plan_approver(Box::new(crate::ai::agent::DenyPlan));
604 }
605 match agent.run().await {
606 Ok(cr) => Json(AgentRunResponse {
607 summary: cr.summary,
608 actions_taken: cr.actions_taken,
609 sessions_opened: cr.sessions_opened.iter().map(|s| s.as_u64()).collect(),
610 job_ids: cr.job_ids.iter().map(|j| j.as_u64()).collect(),
611 report: cr.report,
612 }),
613 Err(e) => Json(AgentRunResponse {
614 summary: format!("error: {e}"),
615 actions_taken: vec![],
616 sessions_opened: vec![],
617 job_ids: vec![],
618 report: String::new(),
619 }),
620 }
621}
622
623async fn run_orchestrate(
624 State(fw): State<SharedFramework>,
625 Json(payload): Json<OrchestratePayload>,
626) -> Json<crate::ai::CampaignReport> {
627 let model = payload.model.unwrap_or_else(|| "llama3.2".into());
628 if !role_allows(fw.lock().await.operator_role, Role::Operator) {
629 return Json(crate::ai::CampaignReport {
630 targets: payload.targets.clone(),
631 summaries: vec!["forbidden: operator role required".into()],
632 ok: 0,
633 failed: 0,
634 total_jobs: 0,
635 total_sessions: 0,
636 total_decisions: 0,
637 total_evidence: 0,
638 total_traces: 0,
639 });
640 }
641 let mut orch = Orchestrator::new(fw, RiskLevel::High);
642 orch.set_approved(payload.approve);
643 Json(
644 orch.run(&payload.targets, move || {
645 Box::new(LlmPlanner::new(model.clone()))
646 })
647 .await,
648 )
649}
650
651async fn list_sessions(State(fw): State<SharedFramework>) -> Json<Vec<SessionItem>> {
652 let fw = fw.lock().await;
653 Json(
654 fw.sessions
655 .list()
656 .into_iter()
657 .map(|s| SessionItem {
658 id: s.id.as_u64(),
659 kind: s.kind.as_str().into(),
660 target: s.target.clone(),
661 module: s.module_name.clone(),
662 elapsed_secs: s.elapsed().as_secs(),
663 })
664 .collect(),
665 )
666}
667
668async fn close_session(State(fw): State<SharedFramework>, Path(id): Path<u64>) -> Json<bool> {
669 let mut fw = fw.lock().await;
670 Json(fw.sessions.close(SessionId(id)))
671}
672
673async fn list_jobs(State(fw): State<SharedFramework>) -> Json<Vec<JobItem>> {
674 let fw = fw.lock().await;
675 Json(
676 fw.jobs
677 .list_recent(50)
678 .into_iter()
679 .map(|j| JobItem {
680 id: j.id.as_u64(),
681 module: j.module_name.clone(),
682 target: j.target.clone(),
683 status: j.status.as_str().into(),
684 elapsed_secs: j.elapsed().as_secs(),
685 })
686 .collect(),
687 )
688}
689
690async fn get_charter(State(fw): State<SharedFramework>) -> Json<CharterStatus> {
691 let fw = fw.lock().await;
692 Json(CharterStatus {
693 accepted: fw.executor.charter.accepted,
694 engagement: fw.executor.charter.engagement.clone(),
695 })
696}
697
698async fn accept_charter(
699 State(fw): State<SharedFramework>,
700 Json(payload): Json<CharterPayload>,
701) -> Json<String> {
702 let mut fw = fw.lock().await;
703 if !role_allows(fw.operator_role, Role::Operator) {
704 return Json("forbidden: operator role required".into());
705 }
706 fw.executor.charter = Charter::accept(payload.engagement.clone(), vec![]);
707 Json("accepted".into())
708}
709
710async fn get_scope(State(fw): State<SharedFramework>) -> Json<Vec<String>> {
711 let fw = fw.lock().await;
712 Json(fw.executor.scope.allow.clone())
713}
714
715async fn add_scope(
716 State(fw): State<SharedFramework>,
717 Json(payload): Json<ScopePayload>,
718) -> Json<String> {
719 let mut fw = fw.lock().await;
720 if !role_allows(fw.operator_role, Role::Operator) {
721 return Json("forbidden: operator role required".into());
722 }
723 fw.executor.scope.allow.push(payload.target);
724 Json("added".into())
725}
726
727async fn evaluate_policy(
728 State(fw): State<SharedFramework>,
729 Query(params): Query<PolicyQuery>,
730) -> Json<serde_json::Value> {
731 let fw = fw.lock().await;
732 let module = params.module.clone().unwrap_or_default();
733 if module.is_empty() {
734 return Json(
735 serde_json::to_value(&fw.executor.policy_set).unwrap_or(serde_json::Value::Null),
736 );
737 }
738 let Some(loaded) = crate::modules::load(&module) else {
739 return Json(serde_json::json!({
740 "module": module,
741 "target": params.target.unwrap_or_default(),
742 "decision": "deny",
743 "reason": "module not found",
744 "in_scope": false,
745 "charter_accepted": fw.executor.charter.accepted,
746 }));
747 };
748 let target = params.target.unwrap_or_default();
749 let pf = fw
750 .executor
751 .preflight(&loaded, &target, None, false, PolicyContext::Rest)
752 .await;
753 let policy = fw.executor.policy(PolicyContext::Rest);
754 let decision = policy.evaluate(&pf.to_request());
755 Json(serde_json::json!({
756 "module": loaded.info.name,
757 "target": target,
758 "capabilities": pf.capabilities.iter().map(|c| c.as_str()).collect::<Vec<_>>(),
759 "intents": pf.capabilities.iter().map(|c| c.intent().as_str()).collect::<Vec<_>>(),
760 "impact": pf.risk.as_str(),
761 "decision": match &decision {
762 PolicyDecision::Allow => "allow",
763 PolicyDecision::RequireApproval(_) => "require_approval",
764 PolicyDecision::Deny(_) => "deny",
765 },
766 "reason": decision.reason().map(|s| s.to_string()),
767 "in_scope": pf.in_scope,
768 "charter_accepted": pf.charter_accepted,
769 }))
770}
771
772async fn list_audit(
773 State(fw): State<SharedFramework>,
774 Query(params): Query<AuditQuery>,
775) -> Json<Vec<DecisionRecord>> {
776 let fw = fw.lock().await;
777 Json(fw.executor.recent_decisions(params.n.unwrap_or(20)))
778}
779
780async fn list_evidence(
781 State(fw): State<SharedFramework>,
782 Query(params): Query<EvidenceQuery>,
783) -> Json<Vec<Evidence>> {
784 let fw = fw.lock().await;
785 let mut ev: Vec<Evidence> = fw.executor.recent_evidence(params.n.unwrap_or(200));
786 if let Some(min) = params.min_confidence {
787 ev.retain(|e| e.confidence >= min);
788 }
789 if let Some(kind) = ¶ms.kind {
790 ev.retain(|e| e.kind.as_deref() == Some(kind.as_str()));
791 }
792 Json(ev)
793}
794
795async fn list_policy_rules(State(fw): State<SharedFramework>) -> Json<PolicySet> {
796 let fw = fw.lock().await;
797 Json(fw.executor.policy_set.clone())
798}
799
800async fn add_policy_rule(
801 State(fw): State<SharedFramework>,
802 Json(rule): Json<PolicyRule>,
803) -> Json<PolicySet> {
804 let mut fw = fw.lock().await;
805 if !role_allows(fw.operator_role, Role::Operator) {
806 return Json(fw.executor.policy_set.clone());
807 }
808 fw.executor.policy_set.add_rule(rule);
809 Json(fw.executor.policy_set.clone())
810}
811
812async fn replace_policy_rules(
813 State(fw): State<SharedFramework>,
814 Json(rules): Json<Vec<PolicyRule>>,
815) -> Json<PolicySet> {
816 let mut fw = fw.lock().await;
817 if !role_allows(fw.operator_role, Role::Operator) {
818 return Json(fw.executor.policy_set.clone());
819 }
820 fw.executor.policy_set.set_rules(rules);
821 Json(fw.executor.policy_set.clone())
822}
823
824async fn delete_policy_rule(
825 State(fw): State<SharedFramework>,
826 Path(index): Path<usize>,
827) -> Result<Json<PolicySet>, StatusCode> {
828 let mut fw = fw.lock().await;
829 if !role_allows(fw.operator_role, Role::Operator) {
830 return Err(StatusCode::FORBIDDEN);
831 }
832 match fw.executor.policy_set.remove_rule(index) {
833 Some(_) => Ok(Json(fw.executor.policy_set.clone())),
834 None => Err(StatusCode::NOT_FOUND),
835 }
836}
837
838#[derive(Deserialize)]
839struct WorkspacePath {
840 path: String,
841}
842
843async fn save_workspace(
844 State(fw): State<SharedFramework>,
845 Json(p): Json<WorkspacePath>,
846) -> Result<Json<String>, StatusCode> {
847 let fw = fw.lock().await;
848 if !role_allows(fw.operator_role, Role::Operator) {
849 return Err(StatusCode::FORBIDDEN);
850 }
851 let snap = WorkspaceSnapshot::from_framework(&fw);
852 snap.save_to_file(&p.path).map_err(|e| {
853 eprintln!("workspace save error: {e}");
854 StatusCode::INTERNAL_SERVER_ERROR
855 })?;
856 Ok(Json(format!("saved to {}", p.path)))
857}
858
859async fn load_workspace(
860 State(fw): State<SharedFramework>,
861 Json(p): Json<WorkspacePath>,
862) -> Result<Json<String>, StatusCode> {
863 let snap = WorkspaceSnapshot::load_from_file(&p.path).map_err(|e| {
864 eprintln!("workspace load error: {e}");
865 StatusCode::BAD_REQUEST
866 })?;
867 let mut fw = fw.lock().await;
868 if !role_allows(fw.operator_role, Role::Operator) {
869 return Err(StatusCode::FORBIDDEN);
870 }
871 snap.apply_to_framework(&mut fw);
872 Ok(Json(format!("loaded from {}", p.path)))
873}
874
875async fn export_audit(
876 State(fw): State<SharedFramework>,
877 Query(params): Query<AuditQuery>,
878) -> Response {
879 let fw = fw.lock().await;
880 let records = fw.executor.recent_decisions(params.n.unwrap_or(200));
881 if params.format.as_deref() == Some("csv") {
882 ([(header::CONTENT_TYPE, "text/csv")], audit_to_csv(&records)).into_response()
883 } else {
884 Json(records).into_response()
885 }
886}
887
888async fn list_policy_packs(State(fw): State<SharedFramework>) -> Json<Vec<PolicyPack>> {
889 let fw = fw.lock().await;
890 Json(fw.policy_packs.values().cloned().collect())
891}
892
893async fn add_policy_pack(
894 State(fw): State<SharedFramework>,
895 Json(pack): Json<PolicyPack>,
896) -> Result<Json<PolicyPack>, StatusCode> {
897 let mut fw = fw.lock().await;
898 if !role_allows(fw.operator_role, Role::Admin) {
899 return Err(StatusCode::FORBIDDEN);
900 }
901 fw.policy_packs.insert(pack.name.clone(), pack.clone());
902 Ok(Json(pack))
903}
904
905async fn apply_policy_pack(
906 State(fw): State<SharedFramework>,
907 Path(name): Path<String>,
908) -> Result<Json<PolicySet>, StatusCode> {
909 let mut fw = fw.lock().await;
910 if !role_allows(fw.operator_role, Role::Admin) {
911 return Err(StatusCode::FORBIDDEN);
912 }
913 let pack = match fw.policy_packs.get(&name) {
914 Some(p) => p.clone(),
915 None => return Err(StatusCode::NOT_FOUND),
916 };
917 fw.executor.policy_set.set_rules(pack.rules.clone());
918 Ok(Json(fw.executor.policy_set.clone()))
919}
920
921async fn list_approvals(State(fw): State<SharedFramework>) -> Json<Vec<ApprovalRequest>> {
922 let fw = fw.lock().await;
923 Json(fw.approval_queue.list())
924}
925
926#[derive(Deserialize)]
927struct ApprovalRequestInput {
928 module: String,
929 target: String,
930 reason: String,
931 #[serde(default)]
932 options: std::collections::HashMap<String, String>,
933}
934
935async fn request_approval(
936 State(fw): State<SharedFramework>,
937 Json(input): Json<ApprovalRequestInput>,
938) -> Json<ApprovalRequest> {
939 let mut fw = fw.lock().await;
940 let id = fw
941 .approval_queue
942 .request(input.module, input.target, input.reason, input.options);
943 Json(
944 fw.approval_queue
945 .get(id)
946 .cloned()
947 .expect("Approval queue missing requested id"),
948 )
949}
950
951async fn approve_approval(
952 State(fw): State<SharedFramework>,
953 Path(id): Path<u64>,
954) -> Result<Json<String>, StatusCode> {
955 let mut fw = fw.lock().await;
956 if !role_allows(fw.operator_role, Role::Operator) {
957 return Err(StatusCode::FORBIDDEN);
958 }
959 let req = match fw.approval_queue.get(id) {
960 Some(r) if r.status == ApprovalStatus::Pending => r.clone(),
961 _ => return Err(StatusCode::NOT_FOUND),
962 };
963 fw.approval_queue.approve(id);
964 let mut loaded = match crate::modules::load(&req.module) {
965 Some(l) => l,
966 None => return Err(StatusCode::BAD_REQUEST),
967 };
968 for (k, v) in &req.options {
969 let _ = loaded.module.set_option(k, v);
970 }
971 match fw
972 .executor
973 .execute(
974 &mut loaded,
975 &req.target,
976 None,
977 true,
978 PolicyContext::Rest,
979 None,
980 false,
981 None,
982 )
983 .await
984 {
985 Ok(_) => Ok(Json(format!("request {id} approved and executed"))),
986 Err(e) => Ok(Json(format!(
987 "request {id} approved but execution failed: {e}"
988 ))),
989 }
990}
991
992async fn deny_approval(
993 State(fw): State<SharedFramework>,
994 Path(id): Path<u64>,
995) -> Result<Json<String>, StatusCode> {
996 let mut fw = fw.lock().await;
997 if !role_allows(fw.operator_role, Role::Operator) {
998 return Err(StatusCode::FORBIDDEN);
999 }
1000 if fw.approval_queue.deny(id) {
1001 Ok(Json(format!("request {id} denied")))
1002 } else {
1003 Err(StatusCode::NOT_FOUND)
1004 }
1005}
1006
1007async fn get_role(State(fw): State<SharedFramework>) -> Json<Role> {
1008 let fw = fw.lock().await;
1009 Json(fw.operator_role)
1010}
1011
1012#[derive(Deserialize)]
1013struct RolePayload {
1014 role: Role,
1015}
1016
1017async fn set_role(
1018 State(fw): State<SharedFramework>,
1019 Json(payload): Json<RolePayload>,
1020) -> Result<Json<Role>, StatusCode> {
1021 let mut fw = fw.lock().await;
1022 if !role_allows(fw.operator_role, Role::Admin) {
1023 return Err(StatusCode::FORBIDDEN);
1024 }
1025 fw.operator_role = payload.role;
1026 Ok(Json(fw.operator_role))
1027}
1028
1029async fn list_traces(State(fw): State<SharedFramework>) -> Json<Vec<ReasoningTrace>> {
1030 let fw = fw.lock().await;
1031 Json(fw.executor.recent_traces(50))
1032}
1033
1034async fn list_memory(State(fw): State<SharedFramework>) -> Json<Vec<MemoryEntry>> {
1035 let fw = fw.lock().await;
1036 Json(fw.executor.recent_memories(50))
1037}
1038
1039#[derive(Deserialize)]
1040struct ProxyBindPayload {
1041 target: String,
1042 port: u16,
1043}
1044
1045#[derive(Serialize)]
1046struct ProxyBindResponse {
1047 local_port: u16,
1048 preflight: Option<PreflightReport>,
1049 error: Option<String>,
1050}
1051
1052async fn bind_proxy(
1053 State(fw): State<SharedFramework>,
1054 Json(payload): Json<ProxyBindPayload>,
1055) -> Json<ProxyBindResponse> {
1056 let mut fw = fw.lock().await;
1057 if !role_allows(fw.operator_role, Role::Operator) {
1058 return Json(ProxyBindResponse {
1059 local_port: 0,
1060 preflight: None,
1061 error: Some("forbidden: operator role required".into()),
1062 });
1063 }
1064
1065 struct VirtualModule;
1067 #[async_trait::async_trait]
1068 impl crate::core::module::Module for VirtualModule {
1069 async fn run(
1070 &self,
1071 ) -> Result<crate::core::module::ModuleResult, crate::core::module::ModuleError> {
1072 Ok(crate::core::module::ModuleResult::default())
1073 }
1074 }
1075
1076 let proxy_module = crate::core::module::LoadedModule {
1077 info: crate::core::module::ModuleInfo {
1078 name: "proxy".into(),
1079 description: "Internal REST Proxy".into(),
1080 author: "System".into(),
1081 kind: crate::core::module::ModuleKind::Auxiliary,
1082 capabilities: vec![],
1083 impact: Some(crate::core::safety::RiskLevel::Low),
1084 intent: Some(crate::core::module::Intent::Read),
1085 sandbox_image: None,
1086 cve: None,
1087 },
1088 module: Box::new(VirtualModule),
1089 };
1090 let pf = fw
1091 .executor
1092 .preflight(
1093 &proxy_module,
1094 &payload.target,
1095 None,
1096 false,
1097 PolicyContext::Rest,
1098 )
1099 .await;
1100 let policy = fw.executor.policy(PolicyContext::Rest);
1101 let report = to_preflight_report(&pf, &policy);
1102
1103 if let Err(e) = pf.check(&policy) {
1104 return Json(ProxyBindResponse {
1105 local_port: 0,
1106 preflight: Some(report),
1107 error: Some(e.to_string()),
1108 });
1109 }
1110
1111 use crate::core::proxy::NetworkIsolator;
1112
1113 #[cfg(target_os = "linux")]
1114 let isolator: Box<dyn NetworkIsolator> =
1115 Box::new(crate::core::proxy::netns::LinuxNetnsIsolator {
1116 namespace_name: format!("icebox-netns-{}", std::process::id()),
1117 });
1118
1119 #[cfg(not(target_os = "linux"))]
1120 let isolator: Box<dyn NetworkIsolator> = Box::new(crate::core::proxy::tcp::TcpProxyIsolator);
1121
1122 let _ = isolator.setup().await; let proxy = isolator.spawn_proxy(&payload.target, payload.port).await;
1125 match proxy {
1126 Ok((listener, handle)) => {
1127 let port = listener.local_addr.port();
1128 crate::core::proxy::bind_proxy(&payload.target, listener.local_addr);
1129 fw.proxies
1130 .insert(port, (payload.target.clone(), isolator, handle));
1131 Json(ProxyBindResponse {
1132 local_port: port,
1133 preflight: Some(report),
1134 error: None,
1135 })
1136 }
1137 Err(e) => Json(ProxyBindResponse {
1138 local_port: 0,
1139 preflight: Some(report),
1140 error: Some(format!("Failed to bind proxy: {e}")),
1141 }),
1142 }
1143}
1144
1145#[derive(Deserialize)]
1146struct ProxyUnbindPayload {
1147 local_port: u16,
1148}
1149
1150#[derive(Serialize)]
1151struct ProxyUnbindResponse {
1152 ok: bool,
1153 error: Option<String>,
1154}
1155
1156async fn unbind_proxy(
1157 State(fw): State<SharedFramework>,
1158 Json(payload): Json<ProxyUnbindPayload>,
1159) -> Json<ProxyUnbindResponse> {
1160 let mut fw = fw.lock().await;
1161 if !role_allows(fw.operator_role, Role::Operator) {
1162 return Json(ProxyUnbindResponse {
1163 ok: false,
1164 error: Some("forbidden: operator role required".into()),
1165 });
1166 }
1167 if let Some((target, isolator, handle)) = fw.proxies.remove(&payload.local_port) {
1168 crate::core::proxy::unbind_proxy(&target);
1169 let _ = isolator.teardown().await;
1170 handle.abort();
1171 }
1172 Json(ProxyUnbindResponse {
1173 ok: true,
1174 error: None,
1175 })
1176}
1177
1178async fn govern_handler(
1179 State(fw): State<SharedFramework>,
1180 Json(payload): Json<GovernAction>,
1181) -> Json<GovernResult> {
1182 let mut guard = fw.lock().await;
1183 let result = guard.executor.govern_action(&payload, PolicyContext::Rest);
1184 Json(result)
1185}
1186
1187async fn record_handler(
1188 State(fw): State<SharedFramework>,
1189 Json(payload): Json<(GovernAction, ActionOutcome)>,
1190) -> Json<RecordResult> {
1191 let mut guard = fw.lock().await;
1192 let result = guard.executor.record_action(&payload.0, payload.1);
1193 Json(result)
1194}