1use indexmap::IndexMap;
9use lex_ast::canonicalize_program;
10use lex_bytecode::{compile_program, vm::Vm, Value};
11use lex_runtime::{check_program as check_policy, DefaultHandler, Policy};
12use lex_store::Store;
13use lex_syntax::{load_program, load_program_from_str, Manifest};
14use lex_vcs::{MergeSession, MergeSessionId};
15use serde::{Deserialize, Serialize};
16use std::collections::{BTreeMap, BTreeSet, HashMap};
17use std::path::PathBuf;
18use std::sync::{Arc, Mutex};
19use std::time::{SystemTime, UNIX_EPOCH};
20use tiny_http::{Header, Method, Request, Response};
21
22pub struct State {
23 pub store: Mutex<Store>,
24 pub root: PathBuf,
29 pub sessions: Mutex<HashMap<MergeSessionId, ApiMergeSession>>,
36 pub policy_ceiling: Option<Policy>,
57}
58
59pub struct ApiMergeSession {
66 pub inner: MergeSession,
67 pub src_branch: String,
68 pub dst_branch: String,
69}
70
71impl State {
72 pub fn open(root: PathBuf) -> anyhow::Result<Self> {
73 Self::open_with_ceiling(root, None)
74 }
75
76 pub fn open_with_ceiling(
81 root: PathBuf,
82 policy_ceiling: Option<Policy>,
83 ) -> anyhow::Result<Self> {
84 Ok(Self {
85 store: Mutex::new(Store::open(&root)?),
86 root,
87 sessions: Mutex::new(HashMap::new()),
88 policy_ceiling,
89 })
90 }
91
92 pub fn new_with_tenant(tenant_id: &str, store_root: PathBuf) -> anyhow::Result<Self> {
102 validate_tenant_id(tenant_id)?;
103 Self::open(store_root.join(tenant_id))
104 }
105
106 pub fn new_with_tenant_and_ceiling(
111 tenant_id: &str,
112 store_root: PathBuf,
113 policy_ceiling: Option<Policy>,
114 ) -> anyhow::Result<Self> {
115 validate_tenant_id(tenant_id)?;
116 Self::open_with_ceiling(store_root.join(tenant_id), policy_ceiling)
117 }
118}
119
120fn clamp_policy(requested: Policy, ceiling: &Policy) -> Policy {
136 let allow_effects: BTreeSet<String> = requested
137 .allow_effects
138 .intersection(&ceiling.allow_effects)
139 .cloned()
140 .collect();
141 let budget = match (requested.budget, ceiling.budget) {
142 (Some(r), Some(c)) => Some(r.min(c)),
143 (None, Some(c)) => Some(c),
144 (Some(r), None) => Some(r),
145 (None, None) => None,
146 };
147 Policy {
148 allow_effects,
149 allow_fs_read: ceiling.allow_fs_read.clone(),
150 allow_fs_write: ceiling.allow_fs_write.clone(),
151 allow_net_host: ceiling.allow_net_host.clone(),
152 allow_proc: ceiling.allow_proc.clone(),
153 budget,
154 }
155}
156
157fn validate_tenant_id(tenant_id: &str) -> anyhow::Result<()> {
158 if tenant_id.is_empty() {
159 anyhow::bail!("tenant_id must not be empty");
160 }
161 if tenant_id.len() > 64 {
162 anyhow::bail!("tenant_id must be at most 64 bytes");
163 }
164 if !tenant_id
165 .bytes()
166 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
167 {
168 anyhow::bail!(
169 "tenant_id {tenant_id:?} contains characters outside [A-Za-z0-9_-]"
170 );
171 }
172 Ok(())
173}
174
175#[derive(Debug, Serialize, Deserialize)]
176struct ErrorEnvelope {
177 error: String,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 detail: Option<serde_json::Value>,
180}
181
182fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
183 let bytes = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
184 Response::from_data(bytes)
185 .with_status_code(status)
186 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
187}
188
189fn error_response(status: u16, msg: impl Into<String>) -> Response<std::io::Cursor<Vec<u8>>> {
190 json_response(status, &serde_json::to_value(ErrorEnvelope {
191 error: msg.into(), detail: None,
192 }).unwrap())
193}
194
195fn error_with_detail(status: u16, msg: impl Into<String>, detail: serde_json::Value)
196 -> Response<std::io::Cursor<Vec<u8>>>
197{
198 json_response(status, &serde_json::to_value(ErrorEnvelope {
199 error: msg.into(), detail: Some(detail),
200 }).unwrap())
201}
202
203fn write_error_response(prefix: &str, err: lex_store::StoreError)
209 -> Response<std::io::Cursor<Vec<u8>>>
210{
211 if let lex_store::StoreError::Contention { branch, attempts } = &err {
212 let body = serde_json::to_vec(&ErrorEnvelope {
213 error: format!("{prefix}: branch '{branch}' is contended (attempts={attempts})"),
214 detail: Some(serde_json::json!({
215 "kind": "contention",
216 "branch": branch,
217 "attempts": attempts,
218 })),
219 }).unwrap_or_else(|_| b"{}".to_vec());
220 return Response::from_data(body)
221 .with_status_code(503)
222 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
223 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"1"[..]).unwrap());
224 }
225 if let lex_store::StoreError::BudgetExceeded { session_id, cap, spent_after } = &err {
233 let body = serde_json::to_vec(&ErrorEnvelope {
234 error: format!(
235 "{prefix}: session `{session_id}` budget exceeded \
236 (spent_after={spent_after}, cap={cap})"
237 ),
238 detail: Some(serde_json::json!({
239 "kind": "budget_exceeded",
240 "session_id": session_id,
241 "cap": cap,
242 "spent_after": spent_after,
243 })),
244 }).unwrap_or_else(|_| b"{}".to_vec());
245 return Response::from_data(body)
246 .with_status_code(503)
247 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
248 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"0"[..]).unwrap());
249 }
250 error_response(500, format!("{prefix}: {err}"))
251}
252
253pub fn handle(state: Arc<State>, mut req: Request) -> std::io::Result<()> {
254 let method = req.method().clone();
255 let url = req.url().to_string();
256 let path = url.split('?').next().unwrap_or("").to_string();
257 let query = url.split_once('?').map(|(_, q)| q.to_string()).unwrap_or_default();
258
259 let x_lex_user = req.headers().iter()
264 .find(|h| h.field.equiv("x-lex-user"))
265 .map(|h| h.value.as_str().to_string());
266
267 if matches!(method, Method::Post) && path == "/v1/pkg/publish" {
269 let mut body_bytes: Vec<u8> = Vec::new();
270 let _ = req.as_reader().read_to_end(&mut body_bytes);
271 let resp = pkg_publish_handler(&state, &body_bytes);
272 return req.respond(resp);
273 }
274
275 let mut body = String::new();
276 let _ = req.as_reader().read_to_string(&mut body);
277
278 let resp = route(&state, &method, &path, &query, &body, x_lex_user.as_deref());
279 req.respond(resp)
280}
281
282pub fn handle_with_auth<F>(state: Arc<State>, req: Request, auth: F) -> std::io::Result<()>
286where
287 F: FnOnce(&str, &[Header]) -> bool,
288{
289 let path = req.url().split('?').next().unwrap_or("").to_string();
290 if !auth(&path, req.headers()) {
291 return req.respond(
292 Response::from_data(br#"{"error":"unauthorized"}"#.to_vec())
293 .with_status_code(401)
294 .with_header(
295 Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
296 ),
297 );
298 }
299 handle(state, req)
300}
301
302fn route(
303 state: &State,
304 method: &Method,
305 path: &str,
306 query: &str,
307 body: &str,
308 x_lex_user: Option<&str>,
309) -> Response<std::io::Cursor<Vec<u8>>> {
310 match (method, path) {
311 (Method::Get, "/") => crate::web::activity_handler(state),
313 (Method::Get, "/web/branches") => crate::web::branches_handler(state),
314 (Method::Get, "/web/trust") => crate::web::trust_handler(state),
315 (Method::Get, "/web/attention") => crate::web::attention_handler(state),
316 (Method::Get, p) if p.starts_with("/web/branch/") => {
317 let name = &p["/web/branch/".len()..];
318 crate::web::branch_handler(state, name)
319 }
320 (Method::Get, p) if p.starts_with("/web/stage/") => {
321 let id = &p["/web/stage/".len()..];
322 crate::web::stage_html_handler(state, id)
323 }
324 (Method::Post, p) if p.starts_with("/web/stage/") && (
329 p.ends_with("/pin") || p.ends_with("/defer")
330 || p.ends_with("/block") || p.ends_with("/unblock")
331 ) => {
332 let prefix_len = "/web/stage/".len();
333 let last_slash = p.rfind('/').unwrap_or(p.len());
334 let id = &p[prefix_len..last_slash];
335 let verb = &p[last_slash + 1..];
336 let decision = match verb {
337 "pin" => crate::web::WebStageDecision::Pin,
338 "defer" => crate::web::WebStageDecision::Defer,
339 "block" => crate::web::WebStageDecision::Block,
340 "unblock" => crate::web::WebStageDecision::Unblock,
341 _ => unreachable!("matched in outer guard"),
342 };
343 crate::web::stage_decision_handler(state, id, body, decision, x_lex_user)
344 }
345 (Method::Get, "/v1/health") => json_response(200, &serde_json::json!({"ok": true})),
347 (Method::Post, "/v1/parse") => parse_handler(body),
348 (Method::Post, "/v1/check") => check_handler(body),
349 (Method::Post, "/v1/publish") => publish_handler(state, body),
350 (Method::Post, "/v1/patch") => patch_handler(state, body),
351 (Method::Get, p) if p.starts_with("/v1/stage/") => {
352 let suffix = &p["/v1/stage/".len()..];
353 if let Some(id) = suffix.strip_suffix("/attestations") {
356 stage_attestations_handler(state, id)
357 } else {
358 stage_handler(state, suffix)
359 }
360 }
361 (Method::Post, "/v1/run") => run_handler(state, body, false),
362 (Method::Post, "/v1/replay") => run_handler(state, body, true),
363 (Method::Get, p) if p.starts_with("/v1/trace/") => {
364 let id = &p["/v1/trace/".len()..];
365 trace_handler(state, id)
366 }
367 (Method::Get, "/v1/diff") => diff_handler(state, query),
368 (Method::Post, "/v1/merge/start") => merge_start_handler(state, body),
369 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/resolve") => {
370 let id = &p["/v1/merge/".len()..p.len() - "/resolve".len()];
371 merge_resolve_handler(state, id, body)
372 }
373 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/commit") => {
374 let id = &p["/v1/merge/".len()..p.len() - "/commit".len()];
375 merge_commit_handler(state, id)
376 }
377 (Method::Post, "/v1/ops/batch") => ops_batch_handler(state, body),
379 (Method::Post, "/v1/attestations/batch") => attestations_batch_handler(state, body),
380 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
384 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
385 branch_head_handler(state, name)
386 }
387 (Method::Get, "/v1/ops/since") => ops_since_handler(state, query),
391 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
392 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
396 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
397 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
398 pkg_head_handler(state, name)
399 }
400 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
401 let name = &p["/v1/pkg/".len()..];
402 pkg_get_handler(state, name)
403 }
404 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
405 let name = &p["/v1/pkg/".len()..];
406 pkg_delete_handler(state, name)
407 }
408 _ => error_response(404, format!("unknown route: {method:?} {path}")),
409 }
410}
411
412#[derive(Deserialize)]
413struct ParseReq { source: String }
414
415fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
416 let req: ParseReq = match serde_json::from_str(body) {
417 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
418 };
419 match load_program_from_str(&req.source) {
420 Ok(prog) => {
421 let stages = canonicalize_program(&prog);
422 json_response(200, &serde_json::to_value(&stages).unwrap())
423 }
424 Err(e) => error_response(400, format!("syntax error: {e}")),
425 }
426}
427
428pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
429 let req: ParseReq = match serde_json::from_str(body) {
430 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
431 };
432 let prog = match load_program_from_str(&req.source) {
433 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
434 };
435 let stages = canonicalize_program(&prog);
436 match lex_types::check_program(&stages) {
437 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
438 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
439 }
440}
441
442#[derive(Deserialize)]
443struct PublishReq { source: String, #[serde(default)] activate: bool }
444
445pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
446 let req: PublishReq = match serde_json::from_str(body) {
447 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
448 };
449 let prog = match load_program_from_str(&req.source) {
450 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
451 };
452 let mut stages = canonicalize_program(&prog);
456 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
457 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
458 }
459
460 let store = state.store.lock().unwrap();
461 let branch = store.current_branch();
462
463 let old_head = match store.branch_head(&branch) {
465 Ok(h) => h,
466 Err(e) => return error_response(500, format!("branch_head: {e}")),
467 };
468 let old_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = old_head.values()
469 .filter_map(|stg| store.get_ast(stg).ok())
470 .filter_map(|s| match s {
471 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd)),
472 _ => None,
473 })
474 .collect();
475 let new_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = stages.iter()
476 .filter_map(|s| match s {
477 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
478 _ => None,
479 })
480 .collect();
481 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
482
483 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
485 {
486 let entry = new_imports.entry("<source>".into()).or_default();
487 for s in &stages {
488 if let lex_ast::Stage::Import(im) = s {
489 entry.insert(im.reference.clone());
490 }
491 }
492 }
493
494 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
495 Ok(outcome) => json_response(200, &serde_json::json!({
496 "ops": outcome.ops,
497 "head_op": outcome.head_op,
498 })),
499 Err(lex_store::StoreError::TypeError(errs)) => {
507 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
508 }
509 Err(e) => write_error_response("publish_program", e),
510 }
511}
512
513#[derive(Deserialize)]
514struct PatchReq {
515 stage_id: String,
516 patch: lex_ast::Patch,
517 #[serde(default)] activate: bool,
518}
519
520fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
523 let req: PatchReq = match serde_json::from_str(body) {
524 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
525 };
526 let store = state.store.lock().unwrap();
527
528 let original = match store.get_ast(&req.stage_id) {
530 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
531 };
532
533 let patched = match lex_ast::apply_patch(&original, &req.patch) {
535 Ok(s) => s,
536 Err(e) => return error_with_detail(422, "patch failed",
537 serde_json::to_value(&e).unwrap_or_default()),
538 };
539
540 let stages = vec![patched.clone()];
542 if let Err(errs) = lex_types::check_program(&stages) {
543 return error_with_detail(422, "type errors after patch",
544 serde_json::to_value(&errs).unwrap_or_default());
545 }
546
547 let branch = store.current_branch();
551
552 let sig = match lex_ast::sig_id(&patched) {
554 Some(s) => s,
555 None => return error_response(500, "patched stage has no sig_id"),
556 };
557
558 let new_id = match store.publish(&patched) {
559 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
560 };
561 if req.activate {
562 if let Err(e) = store.activate(&new_id) {
563 return error_response(500, format!("activate: {e}"));
564 }
565 }
566
567 let original_effects: std::collections::BTreeSet<String> = match &original {
569 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
570 _ => std::collections::BTreeSet::new(),
571 };
572 let patched_effects: std::collections::BTreeSet<String> = match &patched {
573 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
574 _ => std::collections::BTreeSet::new(),
575 };
576 let head_now = match store.get_branch(&branch) {
577 Ok(b) => b.and_then(|b| b.head_op),
578 Err(e) => return error_response(500, format!("get_branch: {e}")),
579 };
580 let kind = if original_effects != patched_effects {
581 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
588 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
589 lex_vcs::OperationKind::ChangeEffectSig {
590 sig_id: sig.clone(),
591 from_stage_id: req.stage_id.clone(),
592 to_stage_id: new_id.clone(),
593 from_effects: original_effects,
594 to_effects: patched_effects,
595 from_budget,
596 to_budget,
597 }
598 } else {
599 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
600 lex_vcs::OperationKind::ModifyBody {
601 sig_id: sig.clone(),
602 from_stage_id: req.stage_id.clone(),
603 to_stage_id: new_id.clone(),
604 from_budget: budget,
605 to_budget: budget,
606 }
607 };
608 let transition = lex_vcs::StageTransition::Replace {
609 sig_id: sig.clone(),
610 from: req.stage_id.clone(),
611 to: new_id.clone(),
612 };
613 let op = lex_vcs::Operation::new(
614 kind,
615 head_now.into_iter().collect::<Vec<_>>(),
616 );
617 let op_id = match store.apply_operation(&branch, op, transition) {
618 Ok(id) => id,
619 Err(e) => return write_error_response("apply_operation", e),
620 };
621
622 let status = format!("{:?}",
623 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
624 json_response(200, &serde_json::json!({
625 "old_stage_id": req.stage_id,
626 "new_stage_id": new_id,
627 "sig_id": sig,
628 "status": status,
629 "op_id": op_id,
630 }))
631}
632
633pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
634 let store = state.store.lock().unwrap();
635 let meta = match store.get_metadata(id) {
636 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
637 };
638 let ast = match store.get_ast(id) {
639 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
640 };
641 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
642 json_response(200, &serde_json::json!({
643 "metadata": meta,
644 "ast": ast,
645 "status": status,
646 }))
647}
648
649pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
658 let store = state.store.lock().unwrap();
659 if let Err(e) = store.get_metadata(id) {
660 return error_response(404, format!("{e}"));
661 }
662 let log = match store.attestation_log() {
663 Ok(l) => l,
664 Err(e) => return error_response(500, format!("attestation log: {e}")),
665 };
666 let mut listing = match log.list_for_stage(&id.to_string()) {
667 Ok(v) => v,
668 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
669 };
670 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
671 json_response(200, &serde_json::json!({"attestations": listing}))
672}
673
674#[derive(Deserialize, Default)]
675struct PolicyJson {
676 #[serde(default)] allow_effects: Vec<String>,
677 #[serde(default)] allow_fs_read: Vec<String>,
678 #[serde(default)] allow_fs_write: Vec<String>,
679 #[serde(default)] budget: Option<u64>,
680}
681
682impl PolicyJson {
683 fn into_policy(self) -> Policy {
684 Policy {
685 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
686 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
687 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
688 allow_net_host: Vec::new(),
689 allow_proc: Vec::new(),
690 budget: self.budget,
691 }
692 }
693}
694
695#[derive(Deserialize)]
696struct RunReq {
697 source: String,
698 #[serde(rename = "fn")] func: String,
699 #[serde(default)] args: Vec<serde_json::Value>,
700 #[serde(default)] policy: PolicyJson,
701 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
702}
703
704pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
705 let req: RunReq = match serde_json::from_str(body) {
706 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
707 };
708 let prog = match load_program_from_str(&req.source) {
709 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
710 };
711 let stages = canonicalize_program(&prog);
712 if let Err(errs) = lex_types::check_program(&stages) {
713 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
714 }
715 let bc = compile_program(&stages);
716 let mut policy = req.policy.into_policy();
717 if let Some(ceiling) = &state.policy_ceiling {
723 policy = clamp_policy(policy, ceiling);
724 }
725 if let Err(violations) = check_policy(&bc, &policy) {
726 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
727 }
728
729 let mut recorder = lex_trace::Recorder::new();
730 if with_overrides && !req.overrides.is_empty() {
731 recorder = recorder.with_overrides(req.overrides);
732 }
733 let handle = recorder.handle();
734 let handler = DefaultHandler::new(policy);
735 let mut vm = Vm::with_handler(&bc, Box::new(handler));
736 vm.set_tracer(Box::new(recorder));
737
738 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
739 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
740 let result = vm.call(&req.func, vargs);
741 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
742
743 let store = state.store.lock().unwrap();
744 let (root_out, root_err, status) = match &result {
745 Ok(v) => (Some(value_to_json(v)), None, 200u16),
746 Err(e) => (None, Some(format!("{e}")), 200u16),
747 };
748 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
749 root_out.clone(), root_err.clone(), started, ended);
750 let run_id = match store.save_trace(&tree) {
751 Ok(id) => id,
752 Err(e) => return error_response(500, format!("save_trace: {e}")),
753 };
754
755 let mut body = serde_json::json!({
756 "run_id": run_id,
757 "output": root_out,
758 });
759 if let Some(err) = root_err {
760 body["error"] = serde_json::Value::String(err);
761 }
762 json_response(status, &body)
763}
764
765fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
766 let store = state.store.lock().unwrap();
767 match store.load_trace(id) {
768 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
769 Err(e) => error_response(404, format!("{e}")),
770 }
771}
772
773fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
774 let mut a = None;
775 let mut b = None;
776 for kv in query.split('&') {
777 if let Some((k, v)) = kv.split_once('=') {
778 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
779 }
780 }
781 let (Some(a), Some(b)) = (a, b) else {
782 return error_response(400, "missing a or b query params");
783 };
784 let store = state.store.lock().unwrap();
785 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
786 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
787 match lex_trace::diff_runs(&ta, &tb) {
788 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
789 None => json_response(200, &serde_json::json!({"divergence": null})),
790 }
791}
792
793fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
794
795fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
796
797#[derive(Deserialize)]
798struct MergeStartReq {
799 src_branch: String,
800 dst_branch: String,
801}
802
803fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
814 let req: MergeStartReq = match serde_json::from_str(body) {
815 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
816 };
817 let store = state.store.lock().unwrap();
818 let src_head = match store.get_branch(&req.src_branch) {
819 Ok(Some(b)) => b.head_op,
820 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
821 Err(e) => return error_response(500, format!("src branch read: {e}")),
822 };
823 let dst_head = match store.get_branch(&req.dst_branch) {
824 Ok(Some(b)) => b.head_op,
825 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
826 Err(e) => return error_response(500, format!("dst branch read: {e}")),
827 };
828 let log = match lex_vcs::OpLog::open(store.root()) {
829 Ok(l) => l,
830 Err(e) => return error_response(500, format!("op log: {e}")),
831 };
832 let merge_id = mint_merge_id();
836 let session = match MergeSession::start(
837 merge_id.clone(),
838 &log,
839 src_head.as_ref(),
840 dst_head.as_ref(),
841 ) {
842 Ok(s) => s,
843 Err(e) => return error_response(500, format!("merge start: {e}")),
844 };
845 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
846 let auto_resolved_count = session.auto_resolved.len();
847 let body = serde_json::json!({
848 "merge_id": merge_id,
849 "src_head": session.src_head,
850 "dst_head": session.dst_head,
851 "lca": session.lca,
852 "conflicts": conflicts,
853 "auto_resolved_count": auto_resolved_count,
854 });
855 drop(conflicts);
856 drop(store);
857 let wrapped = ApiMergeSession {
858 inner: session,
859 src_branch: req.src_branch,
860 dst_branch: req.dst_branch,
861 };
862 state.sessions.lock().unwrap().insert(merge_id, wrapped);
863 json_response(200, &body)
864}
865
866#[derive(Deserialize)]
867struct MergeResolveReq {
868 resolutions: Vec<MergeResolveEntry>,
873}
874
875#[derive(Deserialize)]
876struct MergeResolveEntry {
877 conflict_id: String,
878 resolution: lex_vcs::Resolution,
879}
880
881fn merge_resolve_handler(
892 state: &State,
893 merge_id: &str,
894 body: &str,
895) -> Response<std::io::Cursor<Vec<u8>>> {
896 let req: MergeResolveReq = match serde_json::from_str(body) {
897 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
898 };
899 let mut sessions = state.sessions.lock().unwrap();
900 let Some(wrapped) = sessions.get_mut(merge_id) else {
901 return error_response(404, format!("unknown merge_id `{merge_id}`"));
902 };
903 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
904 .map(|e| (e.conflict_id, e.resolution))
905 .collect();
906 let verdicts = wrapped.inner.resolve(pairs);
907 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
908 let body = serde_json::json!({
909 "verdicts": verdicts,
910 "remaining_conflicts": remaining,
911 });
912 json_response(200, &body)
913}
914
915fn merge_commit_handler(
934 state: &State,
935 merge_id: &str,
936) -> Response<std::io::Cursor<Vec<u8>>> {
937 use std::collections::BTreeMap;
938 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
939 Some(w) => w,
940 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
941 };
942 let dst_branch = wrapped.dst_branch.clone();
943 let src_head = wrapped.inner.src_head.clone();
944 let dst_head = wrapped.inner.dst_head.clone();
945 let auto_resolved = wrapped.inner.auto_resolved.clone();
946
947 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
950
951 for outcome in &auto_resolved {
953 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
954 entries.insert(sig_id.clone(), stage_id.clone());
955 }
956 }
957
958 let resolved = match wrapped.inner.commit() {
960 Ok(r) => r,
961 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
962 return error_with_detail(
966 422,
967 "conflicts remaining",
968 serde_json::json!({"unresolved": ids}),
969 );
970 }
971 };
972
973 for (conflict_id, resolution) in resolved {
974 match resolution {
975 lex_vcs::Resolution::TakeOurs => {
976 }
978 lex_vcs::Resolution::TakeTheirs => {
979 match resolve_take_theirs(state, &src_head, &conflict_id) {
989 Ok(stage_id) => {
990 entries.insert(conflict_id.clone(), stage_id);
991 }
992 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
993 }
994 }
995 lex_vcs::Resolution::Custom { op } => {
996 match op.kind.merge_target() {
1005 Some((sig, stage)) => {
1006 if sig != conflict_id {
1007 return error_with_detail(
1008 422,
1009 "custom op targets a different sig than the conflict",
1010 serde_json::json!({
1011 "conflict_id": conflict_id,
1012 "op_targets": sig,
1013 }),
1014 );
1015 }
1016 entries.insert(conflict_id, stage);
1017 }
1018 None => {
1019 return error_with_detail(
1020 422,
1021 "custom op kind doesn't yield a single sig→stage delta",
1022 serde_json::json!({
1023 "conflict_id": conflict_id,
1024 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1025 }),
1026 );
1027 }
1028 }
1029 }
1030 lex_vcs::Resolution::Defer => {
1031 return error_response(500, "internal: Defer slipped past commit gate");
1033 }
1034 }
1035 }
1036
1037 let resolved_count = entries.len();
1038 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1039 if let Some(d) = dst_head { parents.push(d); }
1040 if let Some(s) = src_head { parents.push(s); }
1041 let op = lex_vcs::Operation::new(
1042 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1043 parents,
1044 );
1045 let transition = lex_vcs::StageTransition::Merge { entries };
1046 let store = state.store.lock().unwrap();
1047 match store.apply_operation(&dst_branch, op, transition) {
1048 Ok(new_head_op) => json_response(200, &serde_json::json!({
1049 "new_head_op": new_head_op,
1050 "dst_branch": dst_branch,
1051 })),
1052 Err(e) => write_error_response("apply merge op", e),
1053 }
1054}
1055
1056fn resolve_take_theirs(
1061 state: &State,
1062 src_head: &Option<lex_vcs::OpId>,
1063 sig: &lex_vcs::SigId,
1064) -> std::io::Result<Option<lex_vcs::StageId>> {
1065 let store = state.store.lock().unwrap();
1066 let log = lex_vcs::OpLog::open(store.root())?;
1067 let Some(head) = src_head.as_ref() else { return Ok(None); };
1068 let mut current: Option<lex_vcs::StageId> = None;
1071 for record in log.walk_forward(head, None)? {
1072 match &record.produces {
1073 lex_vcs::StageTransition::Create { sig_id, stage_id }
1074 if sig_id == sig => { current = Some(stage_id.clone()); }
1075 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1076 if sig_id == sig => { current = Some(to.clone()); }
1077 lex_vcs::StageTransition::Remove { sig_id, .. }
1078 if sig_id == sig => { current = None; }
1079 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1080 if from == sig || to == sig => {
1081 if from == sig { current = None; }
1082 if to == sig { current = Some(body_stage_id.clone()); }
1083 }
1084 lex_vcs::StageTransition::Merge { entries } => {
1085 if let Some(opt) = entries.get(sig) {
1086 current = opt.clone();
1087 }
1088 }
1089 _ => {}
1090 }
1091 }
1092 Ok(current)
1093}
1094
1095fn mint_merge_id() -> MergeSessionId {
1096 use std::sync::atomic::{AtomicU64, Ordering};
1097 static COUNTER: AtomicU64 = AtomicU64::new(0);
1098 let nanos = SystemTime::now()
1099 .duration_since(UNIX_EPOCH)
1100 .map(|d| d.as_nanos())
1101 .unwrap_or(0);
1102 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1103 format!("merge_{nanos:x}_{n:x}")
1104}
1105
1106pub(crate) fn ops_batch_handler(state: &State, body: &str)
1137 -> Response<std::io::Cursor<Vec<u8>>>
1138{
1139 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1140 Ok(r) => r,
1141 Err(e) => return error_response(400,
1142 format!("body must be a JSON array of OperationRecord: {e}")),
1143 };
1144 let store = state.store.lock().unwrap();
1145 let log = match lex_vcs::OpLog::open(store.root()) {
1146 Ok(l) => l,
1147 Err(e) => return error_response(500, format!("opening op log: {e}")),
1148 };
1149
1150 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1158 std::collections::BTreeSet::new();
1159 for rec in &records {
1160 let expected = rec.op.op_id();
1161 if expected != rec.op_id {
1162 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1163 "supplied": rec.op_id,
1164 "expected": expected,
1165 }));
1166 }
1167 for parent in &rec.op.parents {
1168 let known = match log.get(parent) {
1169 Ok(Some(_)) => true,
1170 Ok(None) => false,
1171 Err(e) => return error_response(500, format!("op log read: {e}")),
1172 };
1173 if !known && !batch_ids.contains(parent) {
1174 return error_with_detail(422, "MissingParent", serde_json::json!({
1175 "op_id": rec.op_id,
1176 "missing_parent": parent,
1177 }));
1178 }
1179 }
1180 batch_ids.insert(rec.op_id.clone());
1181 }
1182
1183 let mut added = 0usize;
1186 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1187 for rec in &records {
1188 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1189 match log.put(rec) {
1190 Ok(()) => {
1191 if !already_present {
1192 added += 1;
1193 added_ids.push(&rec.op_id);
1194 }
1195 }
1196 Err(e) => return error_response(500, format!("op log write: {e}")),
1197 }
1198 }
1199
1200 json_response(200, &serde_json::json!({
1201 "received": records.len(),
1202 "added": added,
1203 "skipped": records.len() - added,
1204 "added_ids": added_ids,
1205 }))
1206}
1207
1208pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1230 -> Response<std::io::Cursor<Vec<u8>>>
1231{
1232 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1233 Ok(a) => a,
1234 Err(e) => return error_response(400,
1235 format!("body must be a JSON array of Attestation: {e}")),
1236 };
1237 let store = state.store.lock().unwrap();
1238 let log = match store.attestation_log() {
1239 Ok(l) => l,
1240 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1241 };
1242 let op_log = match lex_vcs::OpLog::open(store.root()) {
1243 Ok(l) => l,
1244 Err(e) => return error_response(500, format!("opening op log: {e}")),
1245 };
1246
1247 for att in &attestations {
1249 let expected = lex_vcs::Attestation::with_timestamp(
1252 att.stage_id.clone(),
1253 att.op_id.clone(),
1254 att.intent_id.clone(),
1255 att.kind.clone(),
1256 att.result.clone(),
1257 att.produced_by.clone(),
1258 att.cost.clone(),
1259 att.timestamp,
1260 ).attestation_id;
1261 if expected != att.attestation_id {
1262 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1263 "supplied": att.attestation_id,
1264 "expected": expected,
1265 }));
1266 }
1267 if let Some(op_id) = &att.op_id {
1271 match op_log.get(op_id) {
1272 Ok(Some(_)) => {}
1273 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1274 "attestation_id": att.attestation_id,
1275 "op_id": op_id,
1276 })),
1277 Err(e) => return error_response(500, format!("op log read: {e}")),
1278 }
1279 }
1280 }
1281
1282 let mut added = 0usize;
1286 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1287 for att in &attestations {
1288 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1289 match log.put(att) {
1290 Ok(()) => {
1291 if !already_present {
1292 added += 1;
1293 added_ids.push(&att.attestation_id);
1294 }
1295 }
1296 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1297 }
1298 }
1299
1300 json_response(200, &serde_json::json!({
1301 "received": attestations.len(),
1302 "added": added,
1303 "skipped": attestations.len() - added,
1304 "added_ids": added_ids,
1305 }))
1306}
1307
1308pub(crate) fn branch_head_handler(state: &State, name: &str)
1317 -> Response<std::io::Cursor<Vec<u8>>>
1318{
1319 let store = state.store.lock().unwrap();
1320 let head = match store.get_branch(name) {
1321 Ok(Some(b)) => b.head_op,
1322 Ok(None) => None,
1323 Err(e) => return error_response(500, format!("get_branch: {e}")),
1324 };
1325 json_response(200, &serde_json::json!({
1326 "branch": name,
1327 "head_op": head,
1328 }))
1329}
1330
1331pub(crate) fn ops_since_handler(state: &State, query: &str)
1355 -> Response<std::io::Cursor<Vec<u8>>>
1356{
1357 let mut after: Option<String> = None;
1358 let mut branch = String::from("main");
1359 let mut limit: Option<usize> = None;
1360 for kv in query.split('&') {
1361 let Some((k, v)) = kv.split_once('=') else { continue };
1362 match k {
1363 "after" => after = Some(v.to_string()),
1364 "branch" => branch = v.to_string(),
1365 "limit" => {
1366 limit = Some(match v.parse::<usize>() {
1367 Ok(n) => n,
1368 Err(_) => return error_response(400,
1369 format!("limit must be a positive integer, got `{v}`")),
1370 });
1371 }
1372 _ => {}
1373 }
1374 }
1375
1376 let store = state.store.lock().unwrap();
1377 let log = match lex_vcs::OpLog::open(store.root()) {
1378 Ok(l) => l,
1379 Err(e) => return error_response(500, format!("opening op log: {e}")),
1380 };
1381 let head = match store.get_branch(&branch) {
1382 Ok(Some(b)) => b.head_op,
1383 Ok(None) => None,
1384 Err(e) => return error_response(500, format!("get_branch: {e}")),
1385 };
1386 let Some(head) = head else {
1387 return json_response(200, &serde_json::json!([]));
1388 };
1389
1390 let ops_since = match log.ops_since(&head, after.as_ref()) {
1391 Ok(o) => o,
1392 Err(e) => return error_response(500, format!("ops_since: {e}")),
1393 };
1394 let mut ops = ops_since;
1398 ops.reverse();
1399 if let Some(n) = limit {
1400 ops.truncate(n);
1401 }
1402
1403 json_response(200, &serde_json::to_value(&ops).unwrap_or_default())
1404}
1405
1406pub(crate) fn attestations_since_handler(state: &State, query: &str)
1419 -> Response<std::io::Cursor<Vec<u8>>>
1420{
1421 let mut after_op: Option<String> = None;
1422 let mut limit: Option<usize> = None;
1423 for kv in query.split('&') {
1424 let Some((k, v)) = kv.split_once('=') else { continue };
1425 match k {
1426 "after-op" => after_op = Some(v.to_string()),
1427 "limit" => {
1428 limit = Some(match v.parse::<usize>() {
1429 Ok(n) => n,
1430 Err(_) => return error_response(400,
1431 format!("limit must be a positive integer, got `{v}`")),
1432 });
1433 }
1434 _ => {}
1435 }
1436 }
1437
1438 let store = state.store.lock().unwrap();
1439 let log = match store.attestation_log() {
1440 Ok(l) => l,
1441 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1442 };
1443
1444 let exclude: std::collections::BTreeSet<String> = match &after_op {
1448 None => std::collections::BTreeSet::new(),
1449 Some(cutoff) => {
1450 let op_log = match lex_vcs::OpLog::open(store.root()) {
1451 Ok(l) => l,
1452 Err(e) => return error_response(500, format!("opening op log: {e}")),
1453 };
1454 match op_log.walk_back(cutoff, None) {
1455 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1456 Err(_) => {
1457 std::collections::BTreeSet::new()
1461 }
1462 }
1463 }
1464 };
1465
1466 let all = match log.list_all() {
1467 Ok(v) => v,
1468 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1469 };
1470 let mut filtered: Vec<lex_vcs::Attestation> = all
1471 .into_iter()
1472 .filter(|a| match &a.op_id {
1473 Some(op_id) => !exclude.contains(op_id),
1474 None => true,
1478 })
1479 .collect();
1480 filtered.sort_by(|a, b| {
1484 a.timestamp.cmp(&b.timestamp)
1485 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1486 });
1487 if let Some(n) = limit {
1488 filtered.truncate(n);
1489 }
1490
1491 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1492}
1493
1494#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1499struct PkgRecord {
1500 name: String,
1501 version: String,
1502 head_op: Option<String>,
1503 published_at: u64,
1504 function_names: Vec<String>,
1506 ops: Vec<serde_json::Value>,
1508}
1509
1510fn pkg_index_dir(root: &std::path::Path) -> PathBuf {
1511 root.join("packages")
1512}
1513
1514fn pkg_record_path(root: &std::path::Path, name: &str) -> PathBuf {
1515 pkg_index_dir(root).join(format!("{}.json", name))
1516}
1517
1518fn load_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1519 let bytes = std::fs::read(pkg_record_path(root, name)).ok()?;
1520 serde_json::from_slice(&bytes).ok()
1521}
1522
1523fn save_pkg_record(root: &std::path::Path, record: &PkgRecord) -> std::io::Result<()> {
1524 let dir = pkg_index_dir(root);
1525 std::fs::create_dir_all(&dir)?;
1526 let bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
1527 std::fs::write(pkg_record_path(root, &record.name), bytes)
1528}
1529
1530fn list_pkg_records(root: &std::path::Path) -> Vec<PkgRecord> {
1531 let dir = pkg_index_dir(root);
1532 let Ok(entries) = std::fs::read_dir(&dir) else {
1533 return Vec::new();
1534 };
1535 let mut records: Vec<PkgRecord> = entries
1536 .filter_map(|e| e.ok())
1537 .filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("json"))
1538 .filter_map(|e| {
1539 let bytes = std::fs::read(e.path()).ok()?;
1540 serde_json::from_slice(&bytes).ok()
1541 })
1542 .collect();
1543 records.sort_by(|a, b| a.name.cmp(&b.name));
1544 records
1545}
1546
1547fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
1548 let Ok(entries) = std::fs::read_dir(dir) else { return };
1549 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
1550 entries.sort_by_key(|e| e.path());
1551 for entry in entries {
1552 let path = entry.path();
1553 if path.is_dir() {
1554 collect_lex_files(&path, out);
1555 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
1556 out.push(path);
1557 }
1558 }
1559}
1560
1561fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
1564 let tmp = match tempfile::TempDir::new() {
1565 Ok(t) => t,
1566 Err(e) => return error_response(500, format!("create temp dir: {e}")),
1567 };
1568 {
1569 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
1570 let mut ar = tar::Archive::new(gz);
1571 if let Err(e) = ar.unpack(tmp.path()) {
1572 return error_response(400, format!("unpack archive: {e}"));
1573 }
1574 }
1575
1576 let toml_path = tmp.path().join("lex.toml");
1577 if !toml_path.exists() {
1578 return error_response(400, "archive must contain lex.toml at root");
1579 }
1580 let manifest = match Manifest::load(&toml_path) {
1581 Ok(m) => m,
1582 Err(e) => return error_response(400, format!("lex.toml: {e}")),
1583 };
1584 let (pkg_name, pkg_version) = match &manifest.package {
1585 Some(m) => (m.name.clone(), m.version.clone()),
1586 None => return error_response(400, "lex.toml must have a [package] section"),
1587 };
1588
1589 let src_dir = tmp.path().join("src");
1590 if !src_dir.exists() {
1591 return error_response(400, "archive must contain a src/ directory");
1592 }
1593 let mut lex_files: Vec<PathBuf> = Vec::new();
1594 collect_lex_files(&src_dir, &mut lex_files);
1595 if lex_files.is_empty() {
1596 return error_response(400, "no .lex files found in src/");
1597 }
1598
1599 let store = state.store.lock().unwrap();
1600 let branch = store.current_branch();
1601
1602 let mut all_ops: Vec<serde_json::Value> = Vec::new();
1603 let mut final_head_op: Option<String> = None;
1604 let mut all_function_names: Vec<String> = Vec::new();
1605
1606 for lex_path in &lex_files {
1607 let prog = match load_program(lex_path) {
1608 Ok(p) => p,
1609 Err(e) => return error_response(400, format!("load {}: {e}", lex_path.display())),
1610 };
1611 let mut stages = canonicalize_program(&prog);
1612 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
1613 return error_with_detail(
1614 422,
1615 format!("type errors in {}", lex_path.display()),
1616 serde_json::to_value(&errs).unwrap(),
1617 );
1618 }
1619
1620 let old_head = match store.branch_head(&branch) {
1621 Ok(h) => h,
1622 Err(e) => return error_response(500, format!("branch_head: {e}")),
1623 };
1624 let old_fns: BTreeMap<String, lex_ast::FnDecl> = old_head.values()
1625 .filter_map(|stg| store.get_ast(stg).ok())
1626 .filter_map(|s| match s {
1627 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd)),
1628 _ => None,
1629 })
1630 .collect();
1631 let new_fns: BTreeMap<String, lex_ast::FnDecl> = stages.iter()
1632 .filter_map(|s| match s {
1633 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
1634 _ => None,
1635 })
1636 .collect();
1637
1638 for name in new_fns.keys() {
1639 if !all_function_names.contains(name) {
1640 all_function_names.push(name.clone());
1641 }
1642 }
1643
1644 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
1645
1646 let file_key = lex_path
1647 .strip_prefix(tmp.path())
1648 .unwrap_or(lex_path)
1649 .display()
1650 .to_string();
1651 let mut new_imports = lex_vcs::ImportMap::new();
1652 {
1653 let entry = new_imports.entry(file_key).or_default();
1654 for s in &stages {
1655 if let lex_ast::Stage::Import(im) = s {
1656 entry.insert(im.reference.clone());
1657 }
1658 }
1659 }
1660
1661 match store.publish_program(&branch, &stages, &report, &new_imports, false) {
1662 Ok(outcome) => {
1663 let ops_json = serde_json::to_value(&outcome.ops).unwrap_or_default();
1664 if let serde_json::Value::Array(arr) = ops_json {
1665 all_ops.extend(arr);
1666 }
1667 if let Some(h) = outcome.head_op {
1668 final_head_op = Some(h);
1669 }
1670 }
1671 Err(lex_store::StoreError::TypeError(errs)) => {
1672 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
1673 }
1674 Err(e) => return write_error_response("publish_program", e),
1675 }
1676 }
1677
1678 let now = SystemTime::now()
1679 .duration_since(UNIX_EPOCH)
1680 .map(|d| d.as_secs())
1681 .unwrap_or(0);
1682 let record = PkgRecord {
1683 name: pkg_name.clone(),
1684 version: pkg_version,
1685 head_op: final_head_op.clone(),
1686 published_at: now,
1687 function_names: all_function_names,
1688 ops: all_ops.clone(),
1689 };
1690 if let Err(e) = save_pkg_record(&state.root, &record) {
1691 return error_response(500, format!("save package index: {e}"));
1692 }
1693
1694 json_response(200, &serde_json::json!({
1695 "package": pkg_name,
1696 "ops": all_ops,
1697 "head_op": final_head_op,
1698 }))
1699}
1700
1701fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
1703 let records = list_pkg_records(&state.root);
1704 let packages: Vec<serde_json::Value> = records.iter().map(|r| serde_json::json!({
1705 "name": r.name,
1706 "version": r.version,
1707 "head_op": r.head_op,
1708 "published_at": r.published_at,
1709 })).collect();
1710 json_response(200, &serde_json::json!({ "packages": packages }))
1711}
1712
1713fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1715 match load_pkg_record(&state.root, name) {
1716 Some(r) => json_response(200, &serde_json::json!({
1717 "name": r.name,
1718 "version": r.version,
1719 "head_op": r.head_op,
1720 "published_at": r.published_at,
1721 "function_names": r.function_names,
1722 "ops": r.ops,
1723 })),
1724 None => error_response(404, format!("package {name:?} not found")),
1725 }
1726}
1727
1728fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1730 match load_pkg_record(&state.root, name) {
1731 Some(r) => json_response(200, &serde_json::json!({
1732 "name": r.name,
1733 "head_op": r.head_op,
1734 })),
1735 None => error_response(404, format!("package {name:?} not found")),
1736 }
1737}
1738
1739fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1741 let record = match load_pkg_record(&state.root, name) {
1742 Some(r) => r,
1743 None => return error_response(404, format!("package {name:?} not found")),
1744 };
1745
1746 let store = state.store.lock().unwrap();
1747 let branch = store.current_branch();
1748
1749 let head = match store.branch_head(&branch) {
1750 Ok(h) => h,
1751 Err(e) => return error_response(500, format!("branch_head: {e}")),
1752 };
1753
1754 let old_fns: BTreeMap<String, lex_ast::FnDecl> = head.values()
1756 .filter_map(|stage_id| store.get_ast(stage_id).ok())
1757 .filter_map(|s| match s {
1758 lex_ast::Stage::FnDecl(fd)
1759 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
1760 _ => None,
1761 })
1762 .collect();
1763
1764 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
1765 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
1766 let empty_imports = lex_vcs::ImportMap::new();
1767
1768 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
1769 Ok(outcome) => {
1770 let _ = std::fs::remove_file(pkg_record_path(&state.root, name));
1771 json_response(200, &serde_json::json!({
1772 "deleted": name,
1773 "ops": outcome.ops,
1774 "head_op": outcome.head_op,
1775 }))
1776 }
1777 Err(lex_store::StoreError::TypeError(errs)) => {
1778 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
1779 }
1780 Err(e) => write_error_response("retract package", e),
1781 }
1782}
1783
1784#[cfg(test)]
1785mod policy_ceiling_tests {
1786 use super::*;
1787 use lex_runtime::Policy;
1788 use std::path::PathBuf;
1789
1790 fn permissive_request() -> Policy {
1794 Policy {
1795 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
1796 .iter()
1797 .map(|s| s.to_string())
1798 .collect(),
1799 allow_fs_read: vec![PathBuf::from("/")],
1800 allow_fs_write: vec![PathBuf::from("/")],
1801 allow_net_host: Vec::new(),
1802 allow_proc: Vec::new(),
1803 budget: None,
1804 }
1805 }
1806
1807 #[test]
1808 fn ceiling_drops_effects_the_caller_was_not_granted() {
1809 let ceiling = Policy {
1810 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
1811 ..Policy::default()
1812 };
1813 let got = clamp_policy(permissive_request(), &ceiling);
1814 assert!(got.allow_effects.contains("io"));
1815 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
1816 assert!(!got.allow_effects.contains("fs_write"));
1817 assert!(!got.allow_effects.contains("net"));
1818 assert!(!got.allow_effects.contains("time"));
1820 }
1821
1822 #[test]
1823 fn ceiling_scopes_override_caller_scopes() {
1824 let ceiling = Policy {
1825 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
1826 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
1827 ..Policy::default()
1828 };
1829 let got = clamp_policy(permissive_request(), &ceiling);
1830 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
1833 assert!(got.allow_fs_write.is_empty());
1834 assert!(got.allow_proc.is_empty());
1835 assert!(got.allow_net_host.is_empty());
1836 }
1837
1838 #[test]
1839 fn ceiling_caps_budget_and_prefers_the_smaller() {
1840 let mut req = permissive_request();
1842 req.budget = None;
1843 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
1844 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
1845
1846 let mut req2 = permissive_request();
1848 req2.budget = Some(50);
1849 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
1850 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
1851 }
1852
1853 #[test]
1854 fn empty_ceiling_is_pure_only() {
1855 let got = clamp_policy(permissive_request(), &Policy::default());
1856 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
1857 assert!(got.allow_proc.is_empty());
1858 assert!(got.allow_fs_write.is_empty());
1859 }
1860}