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/") && p.ends_with("/versions") => {
401 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
402 pkg_versions_handler(state, name)
403 }
404 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
406 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
407 if let Some((name, version)) = inner.split_once('/') {
409 pkg_archive_handler(state, name, version)
410 } else {
411 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
412 }
413 }
414 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
416 let inner = &p["/v1/pkg/".len()..];
417 if let Some((name, version)) = inner.split_once('/') {
418 pkg_get_version_handler(state, name, version)
419 } else {
420 error_response(400, "expected /v1/pkg/{name}/{version}")
421 }
422 }
423 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
424 let name = &p["/v1/pkg/".len()..];
425 pkg_get_handler(state, name)
426 }
427 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
428 let name = &p["/v1/pkg/".len()..];
429 pkg_delete_handler(state, name)
430 }
431 _ => error_response(404, format!("unknown route: {method:?} {path}")),
432 }
433}
434
435#[derive(Deserialize)]
436struct ParseReq { source: String }
437
438fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
439 let req: ParseReq = match serde_json::from_str(body) {
440 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
441 };
442 match load_program_from_str(&req.source) {
443 Ok(prog) => {
444 let stages = canonicalize_program(&prog);
445 json_response(200, &serde_json::to_value(&stages).unwrap())
446 }
447 Err(e) => error_response(400, format!("syntax error: {e}")),
448 }
449}
450
451pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
452 let req: ParseReq = match serde_json::from_str(body) {
453 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
454 };
455 let prog = match load_program_from_str(&req.source) {
456 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
457 };
458 let stages = canonicalize_program(&prog);
459 match lex_types::check_program(&stages) {
460 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
461 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
462 }
463}
464
465#[derive(Deserialize)]
466struct PublishReq { source: String, #[serde(default)] activate: bool }
467
468pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
469 let req: PublishReq = match serde_json::from_str(body) {
470 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
471 };
472 let prog = match load_program_from_str(&req.source) {
473 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
474 };
475 let mut stages = canonicalize_program(&prog);
479 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
480 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
481 }
482
483 let store = state.store.lock().unwrap();
484 let branch = store.current_branch();
485
486 let old_head = match store.branch_head(&branch) {
488 Ok(h) => h,
489 Err(e) => return error_response(500, format!("branch_head: {e}")),
490 };
491 let old_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = old_head.values()
492 .filter_map(|stg| store.get_ast(stg).ok())
493 .filter_map(|s| match s {
494 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd)),
495 _ => None,
496 })
497 .collect();
498 let new_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = stages.iter()
499 .filter_map(|s| match s {
500 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
501 _ => None,
502 })
503 .collect();
504 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
505
506 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
508 {
509 let entry = new_imports.entry("<source>".into()).or_default();
510 for s in &stages {
511 if let lex_ast::Stage::Import(im) = s {
512 entry.insert(im.reference.clone());
513 }
514 }
515 }
516
517 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
518 Ok(outcome) => json_response(200, &serde_json::json!({
519 "ops": outcome.ops,
520 "head_op": outcome.head_op,
521 })),
522 Err(lex_store::StoreError::TypeError(errs)) => {
530 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
531 }
532 Err(e) => write_error_response("publish_program", e),
533 }
534}
535
536#[derive(Deserialize)]
537struct PatchReq {
538 stage_id: String,
539 patch: lex_ast::Patch,
540 #[serde(default)] activate: bool,
541}
542
543fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
546 let req: PatchReq = match serde_json::from_str(body) {
547 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
548 };
549 let store = state.store.lock().unwrap();
550
551 let original = match store.get_ast(&req.stage_id) {
553 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
554 };
555
556 let patched = match lex_ast::apply_patch(&original, &req.patch) {
558 Ok(s) => s,
559 Err(e) => return error_with_detail(422, "patch failed",
560 serde_json::to_value(&e).unwrap_or_default()),
561 };
562
563 let stages = vec![patched.clone()];
565 if let Err(errs) = lex_types::check_program(&stages) {
566 return error_with_detail(422, "type errors after patch",
567 serde_json::to_value(&errs).unwrap_or_default());
568 }
569
570 let branch = store.current_branch();
574
575 let sig = match lex_ast::sig_id(&patched) {
577 Some(s) => s,
578 None => return error_response(500, "patched stage has no sig_id"),
579 };
580
581 let new_id = match store.publish(&patched) {
582 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
583 };
584 if req.activate {
585 if let Err(e) = store.activate(&new_id) {
586 return error_response(500, format!("activate: {e}"));
587 }
588 }
589
590 let original_effects: std::collections::BTreeSet<String> = match &original {
592 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
593 _ => std::collections::BTreeSet::new(),
594 };
595 let patched_effects: std::collections::BTreeSet<String> = match &patched {
596 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
597 _ => std::collections::BTreeSet::new(),
598 };
599 let head_now = match store.get_branch(&branch) {
600 Ok(b) => b.and_then(|b| b.head_op),
601 Err(e) => return error_response(500, format!("get_branch: {e}")),
602 };
603 let kind = if original_effects != patched_effects {
604 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
611 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
612 lex_vcs::OperationKind::ChangeEffectSig {
613 sig_id: sig.clone(),
614 from_stage_id: req.stage_id.clone(),
615 to_stage_id: new_id.clone(),
616 from_effects: original_effects,
617 to_effects: patched_effects,
618 from_budget,
619 to_budget,
620 }
621 } else {
622 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
623 lex_vcs::OperationKind::ModifyBody {
624 sig_id: sig.clone(),
625 from_stage_id: req.stage_id.clone(),
626 to_stage_id: new_id.clone(),
627 from_budget: budget,
628 to_budget: budget,
629 }
630 };
631 let transition = lex_vcs::StageTransition::Replace {
632 sig_id: sig.clone(),
633 from: req.stage_id.clone(),
634 to: new_id.clone(),
635 };
636 let op = lex_vcs::Operation::new(
637 kind,
638 head_now.into_iter().collect::<Vec<_>>(),
639 );
640 let op_id = match store.apply_operation(&branch, op, transition) {
641 Ok(id) => id,
642 Err(e) => return write_error_response("apply_operation", e),
643 };
644
645 let status = format!("{:?}",
646 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
647 json_response(200, &serde_json::json!({
648 "old_stage_id": req.stage_id,
649 "new_stage_id": new_id,
650 "sig_id": sig,
651 "status": status,
652 "op_id": op_id,
653 }))
654}
655
656pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
657 let store = state.store.lock().unwrap();
658 let meta = match store.get_metadata(id) {
659 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
660 };
661 let ast = match store.get_ast(id) {
662 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
663 };
664 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
665 json_response(200, &serde_json::json!({
666 "metadata": meta,
667 "ast": ast,
668 "status": status,
669 }))
670}
671
672pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
681 let store = state.store.lock().unwrap();
682 if let Err(e) = store.get_metadata(id) {
683 return error_response(404, format!("{e}"));
684 }
685 let log = match store.attestation_log() {
686 Ok(l) => l,
687 Err(e) => return error_response(500, format!("attestation log: {e}")),
688 };
689 let mut listing = match log.list_for_stage(&id.to_string()) {
690 Ok(v) => v,
691 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
692 };
693 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
694 json_response(200, &serde_json::json!({"attestations": listing}))
695}
696
697#[derive(Deserialize, Default)]
698struct PolicyJson {
699 #[serde(default)] allow_effects: Vec<String>,
700 #[serde(default)] allow_fs_read: Vec<String>,
701 #[serde(default)] allow_fs_write: Vec<String>,
702 #[serde(default)] budget: Option<u64>,
703}
704
705impl PolicyJson {
706 fn into_policy(self) -> Policy {
707 Policy {
708 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
709 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
710 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
711 allow_net_host: Vec::new(),
712 allow_proc: Vec::new(),
713 budget: self.budget,
714 }
715 }
716}
717
718#[derive(Deserialize)]
719struct RunReq {
720 source: String,
721 #[serde(rename = "fn")] func: String,
722 #[serde(default)] args: Vec<serde_json::Value>,
723 #[serde(default)] policy: PolicyJson,
724 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
725}
726
727pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
728 let req: RunReq = match serde_json::from_str(body) {
729 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
730 };
731 let prog = match load_program_from_str(&req.source) {
732 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
733 };
734 let stages = canonicalize_program(&prog);
735 if let Err(errs) = lex_types::check_program(&stages) {
736 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
737 }
738 let bc = compile_program(&stages);
739 let mut policy = req.policy.into_policy();
740 if let Some(ceiling) = &state.policy_ceiling {
746 policy = clamp_policy(policy, ceiling);
747 }
748 if let Err(violations) = check_policy(&bc, &policy) {
749 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
750 }
751
752 let mut recorder = lex_trace::Recorder::new();
753 if with_overrides && !req.overrides.is_empty() {
754 recorder = recorder.with_overrides(req.overrides);
755 }
756 let handle = recorder.handle();
757 let handler = DefaultHandler::new(policy);
758 let mut vm = Vm::with_handler(&bc, Box::new(handler));
759 vm.set_tracer(Box::new(recorder));
760
761 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
762 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
763 let result = vm.call(&req.func, vargs);
764 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
765
766 let store = state.store.lock().unwrap();
767 let (root_out, root_err, status) = match &result {
768 Ok(v) => (Some(value_to_json(v)), None, 200u16),
769 Err(e) => (None, Some(format!("{e}")), 200u16),
770 };
771 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
772 root_out.clone(), root_err.clone(), started, ended);
773 let run_id = match store.save_trace(&tree) {
774 Ok(id) => id,
775 Err(e) => return error_response(500, format!("save_trace: {e}")),
776 };
777
778 let mut body = serde_json::json!({
779 "run_id": run_id,
780 "output": root_out,
781 });
782 if let Some(err) = root_err {
783 body["error"] = serde_json::Value::String(err);
784 }
785 json_response(status, &body)
786}
787
788fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
789 let store = state.store.lock().unwrap();
790 match store.load_trace(id) {
791 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
792 Err(e) => error_response(404, format!("{e}")),
793 }
794}
795
796fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
797 let mut a = None;
798 let mut b = None;
799 for kv in query.split('&') {
800 if let Some((k, v)) = kv.split_once('=') {
801 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
802 }
803 }
804 let (Some(a), Some(b)) = (a, b) else {
805 return error_response(400, "missing a or b query params");
806 };
807 let store = state.store.lock().unwrap();
808 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
809 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
810 match lex_trace::diff_runs(&ta, &tb) {
811 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
812 None => json_response(200, &serde_json::json!({"divergence": null})),
813 }
814}
815
816fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
817
818fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
819
820#[derive(Deserialize)]
821struct MergeStartReq {
822 src_branch: String,
823 dst_branch: String,
824}
825
826fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
837 let req: MergeStartReq = match serde_json::from_str(body) {
838 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
839 };
840 let store = state.store.lock().unwrap();
841 let src_head = match store.get_branch(&req.src_branch) {
842 Ok(Some(b)) => b.head_op,
843 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
844 Err(e) => return error_response(500, format!("src branch read: {e}")),
845 };
846 let dst_head = match store.get_branch(&req.dst_branch) {
847 Ok(Some(b)) => b.head_op,
848 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
849 Err(e) => return error_response(500, format!("dst branch read: {e}")),
850 };
851 let log = match lex_vcs::OpLog::open(store.root()) {
852 Ok(l) => l,
853 Err(e) => return error_response(500, format!("op log: {e}")),
854 };
855 let merge_id = mint_merge_id();
859 let session = match MergeSession::start(
860 merge_id.clone(),
861 &log,
862 src_head.as_ref(),
863 dst_head.as_ref(),
864 ) {
865 Ok(s) => s,
866 Err(e) => return error_response(500, format!("merge start: {e}")),
867 };
868 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
869 let auto_resolved_count = session.auto_resolved.len();
870 let body = serde_json::json!({
871 "merge_id": merge_id,
872 "src_head": session.src_head,
873 "dst_head": session.dst_head,
874 "lca": session.lca,
875 "conflicts": conflicts,
876 "auto_resolved_count": auto_resolved_count,
877 });
878 drop(conflicts);
879 drop(store);
880 let wrapped = ApiMergeSession {
881 inner: session,
882 src_branch: req.src_branch,
883 dst_branch: req.dst_branch,
884 };
885 state.sessions.lock().unwrap().insert(merge_id, wrapped);
886 json_response(200, &body)
887}
888
889#[derive(Deserialize)]
890struct MergeResolveReq {
891 resolutions: Vec<MergeResolveEntry>,
896}
897
898#[derive(Deserialize)]
899struct MergeResolveEntry {
900 conflict_id: String,
901 resolution: lex_vcs::Resolution,
902}
903
904fn merge_resolve_handler(
915 state: &State,
916 merge_id: &str,
917 body: &str,
918) -> Response<std::io::Cursor<Vec<u8>>> {
919 let req: MergeResolveReq = match serde_json::from_str(body) {
920 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
921 };
922 let mut sessions = state.sessions.lock().unwrap();
923 let Some(wrapped) = sessions.get_mut(merge_id) else {
924 return error_response(404, format!("unknown merge_id `{merge_id}`"));
925 };
926 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
927 .map(|e| (e.conflict_id, e.resolution))
928 .collect();
929 let verdicts = wrapped.inner.resolve(pairs);
930 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
931 let body = serde_json::json!({
932 "verdicts": verdicts,
933 "remaining_conflicts": remaining,
934 });
935 json_response(200, &body)
936}
937
938fn merge_commit_handler(
957 state: &State,
958 merge_id: &str,
959) -> Response<std::io::Cursor<Vec<u8>>> {
960 use std::collections::BTreeMap;
961 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
962 Some(w) => w,
963 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
964 };
965 let dst_branch = wrapped.dst_branch.clone();
966 let src_head = wrapped.inner.src_head.clone();
967 let dst_head = wrapped.inner.dst_head.clone();
968 let auto_resolved = wrapped.inner.auto_resolved.clone();
969
970 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
973
974 for outcome in &auto_resolved {
976 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
977 entries.insert(sig_id.clone(), stage_id.clone());
978 }
979 }
980
981 let resolved = match wrapped.inner.commit() {
983 Ok(r) => r,
984 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
985 return error_with_detail(
989 422,
990 "conflicts remaining",
991 serde_json::json!({"unresolved": ids}),
992 );
993 }
994 };
995
996 for (conflict_id, resolution) in resolved {
997 match resolution {
998 lex_vcs::Resolution::TakeOurs => {
999 }
1001 lex_vcs::Resolution::TakeTheirs => {
1002 match resolve_take_theirs(state, &src_head, &conflict_id) {
1012 Ok(stage_id) => {
1013 entries.insert(conflict_id.clone(), stage_id);
1014 }
1015 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1016 }
1017 }
1018 lex_vcs::Resolution::Custom { op } => {
1019 match op.kind.merge_target() {
1028 Some((sig, stage)) => {
1029 if sig != conflict_id {
1030 return error_with_detail(
1031 422,
1032 "custom op targets a different sig than the conflict",
1033 serde_json::json!({
1034 "conflict_id": conflict_id,
1035 "op_targets": sig,
1036 }),
1037 );
1038 }
1039 entries.insert(conflict_id, stage);
1040 }
1041 None => {
1042 return error_with_detail(
1043 422,
1044 "custom op kind doesn't yield a single sig→stage delta",
1045 serde_json::json!({
1046 "conflict_id": conflict_id,
1047 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1048 }),
1049 );
1050 }
1051 }
1052 }
1053 lex_vcs::Resolution::Defer => {
1054 return error_response(500, "internal: Defer slipped past commit gate");
1056 }
1057 }
1058 }
1059
1060 let resolved_count = entries.len();
1061 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1062 if let Some(d) = dst_head { parents.push(d); }
1063 if let Some(s) = src_head { parents.push(s); }
1064 let op = lex_vcs::Operation::new(
1065 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1066 parents,
1067 );
1068 let transition = lex_vcs::StageTransition::Merge { entries };
1069 let store = state.store.lock().unwrap();
1070 match store.apply_operation(&dst_branch, op, transition) {
1071 Ok(new_head_op) => json_response(200, &serde_json::json!({
1072 "new_head_op": new_head_op,
1073 "dst_branch": dst_branch,
1074 })),
1075 Err(e) => write_error_response("apply merge op", e),
1076 }
1077}
1078
1079fn resolve_take_theirs(
1084 state: &State,
1085 src_head: &Option<lex_vcs::OpId>,
1086 sig: &lex_vcs::SigId,
1087) -> std::io::Result<Option<lex_vcs::StageId>> {
1088 let store = state.store.lock().unwrap();
1089 let log = lex_vcs::OpLog::open(store.root())?;
1090 let Some(head) = src_head.as_ref() else { return Ok(None); };
1091 let mut current: Option<lex_vcs::StageId> = None;
1094 for record in log.walk_forward(head, None)? {
1095 match &record.produces {
1096 lex_vcs::StageTransition::Create { sig_id, stage_id }
1097 if sig_id == sig => { current = Some(stage_id.clone()); }
1098 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1099 if sig_id == sig => { current = Some(to.clone()); }
1100 lex_vcs::StageTransition::Remove { sig_id, .. }
1101 if sig_id == sig => { current = None; }
1102 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1103 if from == sig || to == sig => {
1104 if from == sig { current = None; }
1105 if to == sig { current = Some(body_stage_id.clone()); }
1106 }
1107 lex_vcs::StageTransition::Merge { entries } => {
1108 if let Some(opt) = entries.get(sig) {
1109 current = opt.clone();
1110 }
1111 }
1112 _ => {}
1113 }
1114 }
1115 Ok(current)
1116}
1117
1118fn mint_merge_id() -> MergeSessionId {
1119 use std::sync::atomic::{AtomicU64, Ordering};
1120 static COUNTER: AtomicU64 = AtomicU64::new(0);
1121 let nanos = SystemTime::now()
1122 .duration_since(UNIX_EPOCH)
1123 .map(|d| d.as_nanos())
1124 .unwrap_or(0);
1125 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1126 format!("merge_{nanos:x}_{n:x}")
1127}
1128
1129pub(crate) fn ops_batch_handler(state: &State, body: &str)
1160 -> Response<std::io::Cursor<Vec<u8>>>
1161{
1162 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1163 Ok(r) => r,
1164 Err(e) => return error_response(400,
1165 format!("body must be a JSON array of OperationRecord: {e}")),
1166 };
1167 let store = state.store.lock().unwrap();
1168 let log = match lex_vcs::OpLog::open(store.root()) {
1169 Ok(l) => l,
1170 Err(e) => return error_response(500, format!("opening op log: {e}")),
1171 };
1172
1173 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1181 std::collections::BTreeSet::new();
1182 for rec in &records {
1183 let expected = rec.op.op_id();
1184 if expected != rec.op_id {
1185 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1186 "supplied": rec.op_id,
1187 "expected": expected,
1188 }));
1189 }
1190 for parent in &rec.op.parents {
1191 let known = match log.get(parent) {
1192 Ok(Some(_)) => true,
1193 Ok(None) => false,
1194 Err(e) => return error_response(500, format!("op log read: {e}")),
1195 };
1196 if !known && !batch_ids.contains(parent) {
1197 return error_with_detail(422, "MissingParent", serde_json::json!({
1198 "op_id": rec.op_id,
1199 "missing_parent": parent,
1200 }));
1201 }
1202 }
1203 batch_ids.insert(rec.op_id.clone());
1204 }
1205
1206 let mut added = 0usize;
1209 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1210 for rec in &records {
1211 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1212 match log.put(rec) {
1213 Ok(()) => {
1214 if !already_present {
1215 added += 1;
1216 added_ids.push(&rec.op_id);
1217 }
1218 }
1219 Err(e) => return error_response(500, format!("op log write: {e}")),
1220 }
1221 }
1222
1223 json_response(200, &serde_json::json!({
1224 "received": records.len(),
1225 "added": added,
1226 "skipped": records.len() - added,
1227 "added_ids": added_ids,
1228 }))
1229}
1230
1231pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1253 -> Response<std::io::Cursor<Vec<u8>>>
1254{
1255 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1256 Ok(a) => a,
1257 Err(e) => return error_response(400,
1258 format!("body must be a JSON array of Attestation: {e}")),
1259 };
1260 let store = state.store.lock().unwrap();
1261 let log = match store.attestation_log() {
1262 Ok(l) => l,
1263 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1264 };
1265 let op_log = match lex_vcs::OpLog::open(store.root()) {
1266 Ok(l) => l,
1267 Err(e) => return error_response(500, format!("opening op log: {e}")),
1268 };
1269
1270 for att in &attestations {
1272 let expected = lex_vcs::Attestation::with_timestamp(
1275 att.stage_id.clone(),
1276 att.op_id.clone(),
1277 att.intent_id.clone(),
1278 att.kind.clone(),
1279 att.result.clone(),
1280 att.produced_by.clone(),
1281 att.cost.clone(),
1282 att.timestamp,
1283 ).attestation_id;
1284 if expected != att.attestation_id {
1285 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1286 "supplied": att.attestation_id,
1287 "expected": expected,
1288 }));
1289 }
1290 if let Some(op_id) = &att.op_id {
1294 match op_log.get(op_id) {
1295 Ok(Some(_)) => {}
1296 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1297 "attestation_id": att.attestation_id,
1298 "op_id": op_id,
1299 })),
1300 Err(e) => return error_response(500, format!("op log read: {e}")),
1301 }
1302 }
1303 }
1304
1305 let mut added = 0usize;
1309 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1310 for att in &attestations {
1311 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1312 match log.put(att) {
1313 Ok(()) => {
1314 if !already_present {
1315 added += 1;
1316 added_ids.push(&att.attestation_id);
1317 }
1318 }
1319 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1320 }
1321 }
1322
1323 json_response(200, &serde_json::json!({
1324 "received": attestations.len(),
1325 "added": added,
1326 "skipped": attestations.len() - added,
1327 "added_ids": added_ids,
1328 }))
1329}
1330
1331pub(crate) fn branch_head_handler(state: &State, name: &str)
1340 -> Response<std::io::Cursor<Vec<u8>>>
1341{
1342 let store = state.store.lock().unwrap();
1343 let head = match store.get_branch(name) {
1344 Ok(Some(b)) => b.head_op,
1345 Ok(None) => None,
1346 Err(e) => return error_response(500, format!("get_branch: {e}")),
1347 };
1348 json_response(200, &serde_json::json!({
1349 "branch": name,
1350 "head_op": head,
1351 }))
1352}
1353
1354pub(crate) fn ops_since_handler(state: &State, query: &str)
1378 -> Response<std::io::Cursor<Vec<u8>>>
1379{
1380 let mut after: Option<String> = None;
1381 let mut branch = String::from("main");
1382 let mut limit: Option<usize> = None;
1383 for kv in query.split('&') {
1384 let Some((k, v)) = kv.split_once('=') else { continue };
1385 match k {
1386 "after" => after = Some(v.to_string()),
1387 "branch" => branch = v.to_string(),
1388 "limit" => {
1389 limit = Some(match v.parse::<usize>() {
1390 Ok(n) => n,
1391 Err(_) => return error_response(400,
1392 format!("limit must be a positive integer, got `{v}`")),
1393 });
1394 }
1395 _ => {}
1396 }
1397 }
1398
1399 let store = state.store.lock().unwrap();
1400 let log = match lex_vcs::OpLog::open(store.root()) {
1401 Ok(l) => l,
1402 Err(e) => return error_response(500, format!("opening op log: {e}")),
1403 };
1404 let head = match store.get_branch(&branch) {
1405 Ok(Some(b)) => b.head_op,
1406 Ok(None) => None,
1407 Err(e) => return error_response(500, format!("get_branch: {e}")),
1408 };
1409 let Some(head) = head else {
1410 return json_response(200, &serde_json::json!([]));
1411 };
1412
1413 let ops_since = match log.ops_since(&head, after.as_ref()) {
1414 Ok(o) => o,
1415 Err(e) => return error_response(500, format!("ops_since: {e}")),
1416 };
1417 let mut ops = ops_since;
1421 ops.reverse();
1422 if let Some(n) = limit {
1423 ops.truncate(n);
1424 }
1425
1426 json_response(200, &serde_json::to_value(&ops).unwrap_or_default())
1427}
1428
1429pub(crate) fn attestations_since_handler(state: &State, query: &str)
1442 -> Response<std::io::Cursor<Vec<u8>>>
1443{
1444 let mut after_op: Option<String> = None;
1445 let mut limit: Option<usize> = None;
1446 for kv in query.split('&') {
1447 let Some((k, v)) = kv.split_once('=') else { continue };
1448 match k {
1449 "after-op" => after_op = Some(v.to_string()),
1450 "limit" => {
1451 limit = Some(match v.parse::<usize>() {
1452 Ok(n) => n,
1453 Err(_) => return error_response(400,
1454 format!("limit must be a positive integer, got `{v}`")),
1455 });
1456 }
1457 _ => {}
1458 }
1459 }
1460
1461 let store = state.store.lock().unwrap();
1462 let log = match store.attestation_log() {
1463 Ok(l) => l,
1464 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1465 };
1466
1467 let exclude: std::collections::BTreeSet<String> = match &after_op {
1471 None => std::collections::BTreeSet::new(),
1472 Some(cutoff) => {
1473 let op_log = match lex_vcs::OpLog::open(store.root()) {
1474 Ok(l) => l,
1475 Err(e) => return error_response(500, format!("opening op log: {e}")),
1476 };
1477 match op_log.walk_back(cutoff, None) {
1478 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1479 Err(_) => {
1480 std::collections::BTreeSet::new()
1484 }
1485 }
1486 }
1487 };
1488
1489 let all = match log.list_all() {
1490 Ok(v) => v,
1491 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1492 };
1493 let mut filtered: Vec<lex_vcs::Attestation> = all
1494 .into_iter()
1495 .filter(|a| match &a.op_id {
1496 Some(op_id) => !exclude.contains(op_id),
1497 None => true,
1501 })
1502 .collect();
1503 filtered.sort_by(|a, b| {
1507 a.timestamp.cmp(&b.timestamp)
1508 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1509 });
1510 if let Some(n) = limit {
1511 filtered.truncate(n);
1512 }
1513
1514 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1515}
1516
1517#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1521struct PkgRecord {
1522 name: String,
1523 version: String,
1524 head_op: Option<String>,
1525 published_at: u64,
1526 function_names: Vec<String>,
1528 ops: Vec<serde_json::Value>,
1530}
1531
1532#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1537struct PkgIndex {
1538 latest: Option<String>,
1540 versions: Vec<PkgVersionSummary>,
1542}
1543
1544#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1545struct PkgVersionSummary {
1546 version: String,
1547 head_op: Option<String>,
1548 published_at: u64,
1549}
1550
1551fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1552 root.join("packages").join(name)
1553}
1554
1555fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
1556 pkg_name_dir(root, name).join("index.json")
1557}
1558
1559fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1560 pkg_name_dir(root, name).join(format!("{version}.json"))
1561}
1562
1563fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1564 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
1565}
1566
1567fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
1568 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
1569 serde_json::from_slice(&bytes).ok()
1570}
1571
1572fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
1573 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
1574 serde_json::from_slice(&bytes).ok()
1575}
1576
1577fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1578 let index = load_pkg_index(root, name)?;
1579 let latest = index.latest?;
1580 load_pkg_record(root, name, &latest)
1581}
1582
1583fn save_pkg_record(
1584 root: &std::path::Path,
1585 record: &PkgRecord,
1586 archive: &[u8],
1587) -> std::io::Result<()> {
1588 let dir = pkg_name_dir(root, &record.name);
1589 std::fs::create_dir_all(&dir)?;
1590
1591 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
1593 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
1594
1595 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
1597
1598 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
1600 index.latest = Some(record.version.clone());
1601 if !index.versions.iter().any(|v| v.version == record.version) {
1602 index.versions.push(PkgVersionSummary {
1603 version: record.version.clone(),
1604 head_op: record.head_op.clone(),
1605 published_at: record.published_at,
1606 });
1607 }
1608 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1609 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
1610}
1611
1612fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
1613 let dir = root.join("packages");
1614 let Ok(entries) = std::fs::read_dir(&dir) else {
1615 return Vec::new();
1616 };
1617 let mut names: Vec<String> = entries
1618 .filter_map(|e| e.ok())
1619 .filter(|e| e.path().is_dir())
1620 .filter_map(|e| e.file_name().into_string().ok())
1621 .collect();
1622 names.sort();
1623 names
1624}
1625
1626fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
1627 let Ok(entries) = std::fs::read_dir(dir) else { return };
1628 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
1629 entries.sort_by_key(|e| e.path());
1630 for entry in entries {
1631 let path = entry.path();
1632 if path.is_dir() {
1633 collect_lex_files(&path, out);
1634 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
1635 out.push(path);
1636 }
1637 }
1638}
1639
1640fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
1643 let tmp = match tempfile::TempDir::new() {
1644 Ok(t) => t,
1645 Err(e) => return error_response(500, format!("create temp dir: {e}")),
1646 };
1647 {
1648 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
1649 let mut ar = tar::Archive::new(gz);
1650 if let Err(e) = ar.unpack(tmp.path()) {
1651 return error_response(400, format!("unpack archive: {e}"));
1652 }
1653 }
1654
1655 let toml_path = tmp.path().join("lex.toml");
1656 if !toml_path.exists() {
1657 return error_response(400, "archive must contain lex.toml at root");
1658 }
1659 let manifest = match Manifest::load(&toml_path) {
1660 Ok(m) => m,
1661 Err(e) => return error_response(400, format!("lex.toml: {e}")),
1662 };
1663 let (pkg_name, pkg_version) = match &manifest.package {
1664 Some(m) => (m.name.clone(), m.version.clone()),
1665 None => return error_response(400, "lex.toml must have a [package] section"),
1666 };
1667
1668 let src_dir = tmp.path().join("src");
1669 if !src_dir.exists() {
1670 return error_response(400, "archive must contain a src/ directory");
1671 }
1672 let mut lex_files: Vec<PathBuf> = Vec::new();
1673 collect_lex_files(&src_dir, &mut lex_files);
1674 if lex_files.is_empty() {
1675 return error_response(400, "no .lex files found in src/");
1676 }
1677
1678 let store = state.store.lock().unwrap();
1679 let branch = store.current_branch();
1680
1681 let mut all_ops: Vec<serde_json::Value> = Vec::new();
1682 let mut final_head_op: Option<String> = None;
1683 let mut all_function_names: Vec<String> = Vec::new();
1684
1685 for lex_path in &lex_files {
1686 let prog = match load_program(lex_path) {
1687 Ok(p) => p,
1688 Err(e) => return error_response(400, format!("load {}: {e}", lex_path.display())),
1689 };
1690 let mut stages = canonicalize_program(&prog);
1691 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
1692 return error_with_detail(
1693 422,
1694 format!("type errors in {}", lex_path.display()),
1695 serde_json::to_value(&errs).unwrap(),
1696 );
1697 }
1698
1699 let old_head = match store.branch_head(&branch) {
1700 Ok(h) => h,
1701 Err(e) => return error_response(500, format!("branch_head: {e}")),
1702 };
1703 let old_fns: BTreeMap<String, lex_ast::FnDecl> = old_head.values()
1704 .filter_map(|stg| store.get_ast(stg).ok())
1705 .filter_map(|s| match s {
1706 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd)),
1707 _ => None,
1708 })
1709 .collect();
1710 let new_fns: BTreeMap<String, lex_ast::FnDecl> = stages.iter()
1711 .filter_map(|s| match s {
1712 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
1713 _ => None,
1714 })
1715 .collect();
1716
1717 for name in new_fns.keys() {
1718 if !all_function_names.contains(name) {
1719 all_function_names.push(name.clone());
1720 }
1721 }
1722
1723 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
1724
1725 let file_key = lex_path
1726 .strip_prefix(tmp.path())
1727 .unwrap_or(lex_path)
1728 .display()
1729 .to_string();
1730 let mut new_imports = lex_vcs::ImportMap::new();
1731 {
1732 let entry = new_imports.entry(file_key).or_default();
1733 for s in &stages {
1734 if let lex_ast::Stage::Import(im) = s {
1735 entry.insert(im.reference.clone());
1736 }
1737 }
1738 }
1739
1740 match store.publish_program(&branch, &stages, &report, &new_imports, false) {
1741 Ok(outcome) => {
1742 let ops_json = serde_json::to_value(&outcome.ops).unwrap_or_default();
1743 if let serde_json::Value::Array(arr) = ops_json {
1744 all_ops.extend(arr);
1745 }
1746 if let Some(h) = outcome.head_op {
1747 final_head_op = Some(h);
1748 }
1749 }
1750 Err(lex_store::StoreError::TypeError(errs)) => {
1751 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
1752 }
1753 Err(e) => return write_error_response("publish_program", e),
1754 }
1755 }
1756
1757 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
1759 return error_response(
1760 409,
1761 format!(
1762 "package {pkg_name}@{pkg_version} already published; \
1763 bump the version in lex.toml to publish a new release"
1764 ),
1765 );
1766 }
1767
1768 let now = SystemTime::now()
1769 .duration_since(UNIX_EPOCH)
1770 .map(|d| d.as_secs())
1771 .unwrap_or(0);
1772 let record = PkgRecord {
1773 name: pkg_name.clone(),
1774 version: pkg_version,
1775 head_op: final_head_op.clone(),
1776 published_at: now,
1777 function_names: all_function_names,
1778 ops: all_ops.clone(),
1779 };
1780 if let Err(e) = save_pkg_record(&state.root, &record, body) {
1781 return error_response(500, format!("save package index: {e}"));
1782 }
1783
1784 json_response(200, &serde_json::json!({
1785 "package": pkg_name,
1786 "ops": all_ops,
1787 "head_op": final_head_op,
1788 }))
1789}
1790
1791fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
1793 let names = list_pkg_names(&state.root);
1794 let packages: Vec<serde_json::Value> = names.iter()
1795 .filter_map(|name| {
1796 let idx = load_pkg_index(&state.root, name)?;
1797 let latest = idx.latest.as_deref()?;
1798 let r = load_pkg_record(&state.root, name, latest)?;
1799 Some(serde_json::json!({
1800 "name": r.name,
1801 "version": r.version,
1802 "head_op": r.head_op,
1803 "published_at": r.published_at,
1804 }))
1805 })
1806 .collect();
1807 json_response(200, &serde_json::json!({ "packages": packages }))
1808}
1809
1810fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1812 match load_latest_pkg_record(&state.root, name) {
1813 Some(r) => json_response(200, &serde_json::json!({
1814 "name": r.name,
1815 "version": r.version,
1816 "head_op": r.head_op,
1817 "published_at": r.published_at,
1818 "function_names": r.function_names,
1819 "ops": r.ops,
1820 })),
1821 None => error_response(404, format!("package {name:?} not found")),
1822 }
1823}
1824
1825fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1827 match load_pkg_index(&state.root, name) {
1828 Some(idx) => json_response(200, &serde_json::json!({
1829 "name": name,
1830 "latest": idx.latest,
1831 "versions": idx.versions,
1832 })),
1833 None => error_response(404, format!("package {name:?} not found")),
1834 }
1835}
1836
1837fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1839 match load_pkg_record(&state.root, name, version) {
1840 Some(r) => json_response(200, &serde_json::json!({
1841 "name": r.name,
1842 "version": r.version,
1843 "head_op": r.head_op,
1844 "published_at": r.published_at,
1845 "function_names": r.function_names,
1846 "ops": r.ops,
1847 })),
1848 None => error_response(404, format!("package {name:?}@{version:?} not found")),
1849 }
1850}
1851
1852fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1854 let path = pkg_archive_path(&state.root, name, version);
1855 match std::fs::read(&path) {
1856 Ok(bytes) => Response::from_data(bytes)
1857 .with_status_code(200)
1858 .with_header(
1859 tiny_http::Header::from_bytes(
1860 &b"Content-Type"[..],
1861 &b"application/gzip"[..],
1862 )
1863 .unwrap(),
1864 ),
1865 Err(_) => error_response(404, format!("archive for {name:?}@{version:?} not found")),
1866 }
1867}
1868
1869fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1871 match load_latest_pkg_record(&state.root, name) {
1872 Some(r) => json_response(200, &serde_json::json!({
1873 "name": r.name,
1874 "version": r.version,
1875 "head_op": r.head_op,
1876 })),
1877 None => error_response(404, format!("package {name:?} not found")),
1878 }
1879}
1880
1881fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1883 let record = match load_latest_pkg_record(&state.root, name) {
1884 Some(r) => r,
1885 None => return error_response(404, format!("package {name:?} not found")),
1886 };
1887
1888 let store = state.store.lock().unwrap();
1889 let branch = store.current_branch();
1890
1891 let head = match store.branch_head(&branch) {
1892 Ok(h) => h,
1893 Err(e) => return error_response(500, format!("branch_head: {e}")),
1894 };
1895
1896 let old_fns: BTreeMap<String, lex_ast::FnDecl> = head.values()
1898 .filter_map(|stage_id| store.get_ast(stage_id).ok())
1899 .filter_map(|s| match s {
1900 lex_ast::Stage::FnDecl(fd)
1901 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
1902 _ => None,
1903 })
1904 .collect();
1905
1906 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
1907 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
1908 let empty_imports = lex_vcs::ImportMap::new();
1909
1910 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
1911 Ok(outcome) => {
1912 let ver = record.version.clone();
1914 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
1915 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
1916 if let Some(mut idx) = load_pkg_index(&state.root, name) {
1918 idx.versions.retain(|v| v.version != ver);
1919 idx.latest = idx.versions.last().map(|v| v.version.clone());
1920 if idx.versions.is_empty() {
1921 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
1922 } else {
1923 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
1924 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
1925 }
1926 }
1927 json_response(200, &serde_json::json!({
1928 "deleted": name,
1929 "version": ver,
1930 "ops": outcome.ops,
1931 "head_op": outcome.head_op,
1932 }))
1933 }
1934 Err(lex_store::StoreError::TypeError(errs)) => {
1935 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
1936 }
1937 Err(e) => write_error_response("retract package", e),
1938 }
1939}
1940
1941#[cfg(test)]
1942mod policy_ceiling_tests {
1943 use super::*;
1944 use lex_runtime::Policy;
1945 use std::path::PathBuf;
1946
1947 fn permissive_request() -> Policy {
1951 Policy {
1952 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
1953 .iter()
1954 .map(|s| s.to_string())
1955 .collect(),
1956 allow_fs_read: vec![PathBuf::from("/")],
1957 allow_fs_write: vec![PathBuf::from("/")],
1958 allow_net_host: Vec::new(),
1959 allow_proc: Vec::new(),
1960 budget: None,
1961 }
1962 }
1963
1964 #[test]
1965 fn ceiling_drops_effects_the_caller_was_not_granted() {
1966 let ceiling = Policy {
1967 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
1968 ..Policy::default()
1969 };
1970 let got = clamp_policy(permissive_request(), &ceiling);
1971 assert!(got.allow_effects.contains("io"));
1972 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
1973 assert!(!got.allow_effects.contains("fs_write"));
1974 assert!(!got.allow_effects.contains("net"));
1975 assert!(!got.allow_effects.contains("time"));
1977 }
1978
1979 #[test]
1980 fn ceiling_scopes_override_caller_scopes() {
1981 let ceiling = Policy {
1982 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
1983 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
1984 ..Policy::default()
1985 };
1986 let got = clamp_policy(permissive_request(), &ceiling);
1987 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
1990 assert!(got.allow_fs_write.is_empty());
1991 assert!(got.allow_proc.is_empty());
1992 assert!(got.allow_net_host.is_empty());
1993 }
1994
1995 #[test]
1996 fn ceiling_caps_budget_and_prefers_the_smaller() {
1997 let mut req = permissive_request();
1999 req.budget = None;
2000 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
2001 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
2002
2003 let mut req2 = permissive_request();
2005 req2.budget = Some(50);
2006 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
2007 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
2008 }
2009
2010 #[test]
2011 fn empty_ceiling_is_pure_only() {
2012 let got = clamp_policy(permissive_request(), &Policy::default());
2013 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
2014 assert!(got.allow_proc.is_empty());
2015 assert!(got.allow_fs_write.is_empty());
2016 }
2017}