Skip to main content

icebox/interfaces/
rest.rs

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    // Infer mode based on the current rules
422    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) = &params.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    // Basic preflight - virtual module check
1066    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; // Ignore errors, it logs internally
1123
1124    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}