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 crate::publish_examples::record_examples_for_publish;
14use lex_syntax::{load_package, load_program_from_str, Manifest};
15use lex_vcs::{MergeSession, MergeSessionId};
16use serde::{Deserialize, Serialize};
17use std::collections::{BTreeMap, BTreeSet, HashMap};
18use std::path::PathBuf;
19use std::sync::{Arc, Mutex};
20use std::time::{SystemTime, UNIX_EPOCH};
21use tiny_http::{Header, Method, Request, Response};
22
23fn stage_fns(stages: &[lex_ast::Stage]) -> BTreeMap<String, lex_ast::FnDecl> {
25 stages.iter().filter_map(|s| match s {
26 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
27 _ => None,
28 }).collect()
29}
30
31fn stage_types(stages: &[lex_ast::Stage]) -> BTreeMap<String, lex_ast::TypeDecl> {
34 stages.iter().filter_map(|s| match s {
35 lex_ast::Stage::TypeDecl(td) => Some((td.name.clone(), td.clone())),
36 _ => None,
37 }).collect()
38}
39
40pub struct State {
41 pub store: Mutex<Store>,
42 pub root: PathBuf,
47 pub sessions: Mutex<HashMap<MergeSessionId, ApiMergeSession>>,
54 pub policy_ceiling: Option<Policy>,
75}
76
77pub struct ApiMergeSession {
84 pub inner: MergeSession,
85 pub src_branch: String,
86 pub dst_branch: String,
87}
88
89impl State {
90 pub fn open(root: PathBuf) -> anyhow::Result<Self> {
91 Self::open_with_ceiling(root, None)
92 }
93
94 pub fn open_with_ceiling(
99 root: PathBuf,
100 policy_ceiling: Option<Policy>,
101 ) -> anyhow::Result<Self> {
102 Ok(Self {
103 store: Mutex::new(Store::open(&root)?),
104 root,
105 sessions: Mutex::new(HashMap::new()),
106 policy_ceiling,
107 })
108 }
109
110 pub fn new_with_tenant(tenant_id: &str, store_root: PathBuf) -> anyhow::Result<Self> {
120 validate_tenant_id(tenant_id)?;
121 Self::open(store_root.join(tenant_id))
122 }
123
124 pub fn new_with_tenant_and_ceiling(
129 tenant_id: &str,
130 store_root: PathBuf,
131 policy_ceiling: Option<Policy>,
132 ) -> anyhow::Result<Self> {
133 validate_tenant_id(tenant_id)?;
134 Self::open_with_ceiling(store_root.join(tenant_id), policy_ceiling)
135 }
136}
137
138fn clamp_policy(requested: Policy, ceiling: &Policy) -> Policy {
154 let allow_effects: BTreeSet<String> = requested
155 .allow_effects
156 .intersection(&ceiling.allow_effects)
157 .cloned()
158 .collect();
159 let budget = match (requested.budget, ceiling.budget) {
160 (Some(r), Some(c)) => Some(r.min(c)),
161 (None, Some(c)) => Some(c),
162 (Some(r), None) => Some(r),
163 (None, None) => None,
164 };
165 Policy {
166 allow_effects,
167 allow_fs_read: ceiling.allow_fs_read.clone(),
168 allow_fs_write: ceiling.allow_fs_write.clone(),
169 allow_net_host: ceiling.allow_net_host.clone(),
170 allow_proc: ceiling.allow_proc.clone(),
171 allow_approval: ceiling.allow_approval.clone(),
172 budget,
173 }
174}
175
176fn validate_tenant_id(tenant_id: &str) -> anyhow::Result<()> {
177 if tenant_id.is_empty() {
178 anyhow::bail!("tenant_id must not be empty");
179 }
180 if tenant_id.len() > 64 {
181 anyhow::bail!("tenant_id must be at most 64 bytes");
182 }
183 if !tenant_id
184 .bytes()
185 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
186 {
187 anyhow::bail!(
188 "tenant_id {tenant_id:?} contains characters outside [A-Za-z0-9_-]"
189 );
190 }
191 Ok(())
192}
193
194#[derive(Debug, Serialize, Deserialize)]
195struct ErrorEnvelope {
196 error: String,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 detail: Option<serde_json::Value>,
199}
200
201pub(crate) fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
202 let bytes = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
203 Response::from_data(bytes)
204 .with_status_code(status)
205 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
206}
207
208pub(crate) fn error_response(status: u16, msg: impl Into<String>) -> Response<std::io::Cursor<Vec<u8>>> {
209 json_response(status, &serde_json::to_value(ErrorEnvelope {
210 error: msg.into(), detail: None,
211 }).unwrap())
212}
213
214pub(crate) fn error_with_detail(status: u16, msg: impl Into<String>, detail: serde_json::Value)
215 -> Response<std::io::Cursor<Vec<u8>>>
216{
217 json_response(status, &serde_json::to_value(ErrorEnvelope {
218 error: msg.into(), detail: Some(detail),
219 }).unwrap())
220}
221
222fn write_error_response(prefix: &str, err: lex_store::StoreError)
228 -> Response<std::io::Cursor<Vec<u8>>>
229{
230 if let lex_store::StoreError::Contention { branch, attempts } = &err {
231 let body = serde_json::to_vec(&ErrorEnvelope {
232 error: format!("{prefix}: branch '{branch}' is contended (attempts={attempts})"),
233 detail: Some(serde_json::json!({
234 "kind": "contention",
235 "branch": branch,
236 "attempts": attempts,
237 })),
238 }).unwrap_or_else(|_| b"{}".to_vec());
239 return Response::from_data(body)
240 .with_status_code(503)
241 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
242 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"1"[..]).unwrap());
243 }
244 if let lex_store::StoreError::BudgetExceeded { session_id, cap, spent_after } = &err {
252 let body = serde_json::to_vec(&ErrorEnvelope {
253 error: format!(
254 "{prefix}: session `{session_id}` budget exceeded \
255 (spent_after={spent_after}, cap={cap})"
256 ),
257 detail: Some(serde_json::json!({
258 "kind": "budget_exceeded",
259 "session_id": session_id,
260 "cap": cap,
261 "spent_after": spent_after,
262 })),
263 }).unwrap_or_else(|_| b"{}".to_vec());
264 return Response::from_data(body)
265 .with_status_code(503)
266 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
267 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"0"[..]).unwrap());
268 }
269 error_response(500, format!("{prefix}: {err}"))
270}
271
272pub fn handle(state: Arc<State>, mut req: Request) -> std::io::Result<()> {
273 let method = req.method().clone();
274 let url = req.url().to_string();
275 let path = url.split('?').next().unwrap_or("").to_string();
276 let query = url.split_once('?').map(|(_, q)| q.to_string()).unwrap_or_default();
277
278 let x_lex_user = req.headers().iter()
283 .find(|h| h.field.equiv("x-lex-user"))
284 .map(|h| h.value.as_str().to_string());
285
286 if matches!(method, Method::Post) && path == "/v1/pkg/publish" {
288 let mut body_bytes: Vec<u8> = Vec::new();
289 let _ = req.as_reader().read_to_end(&mut body_bytes);
290 let resp = pkg_publish_handler(&state, &body_bytes);
291 return req.respond(resp);
292 }
293
294 let mut body = String::new();
295 let _ = req.as_reader().read_to_string(&mut body);
296
297 let resp = route(&state, &method, &path, &query, &body, x_lex_user.as_deref());
298 req.respond(resp)
299}
300
301pub fn handle_with_auth<F>(state: Arc<State>, req: Request, auth: F) -> std::io::Result<()>
305where
306 F: FnOnce(&str, &[Header]) -> bool,
307{
308 let path = req.url().split('?').next().unwrap_or("").to_string();
309 if !auth(&path, req.headers()) {
310 return req.respond(
311 Response::from_data(br#"{"error":"unauthorized"}"#.to_vec())
312 .with_status_code(401)
313 .with_header(
314 Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
315 ),
316 );
317 }
318 handle(state, req)
319}
320
321fn route(
322 state: &State,
323 method: &Method,
324 path: &str,
325 query: &str,
326 body: &str,
327 x_lex_user: Option<&str>,
328) -> Response<std::io::Cursor<Vec<u8>>> {
329 match (method, path) {
330 (Method::Get, "/") => crate::web::activity_handler(state),
332 (Method::Get, "/web/branches") => crate::web::branches_handler(state),
333 (Method::Get, "/web/trust") => crate::web::trust_handler(state),
334 (Method::Get, "/web/attention") => crate::web::attention_handler(state),
335 (Method::Get, p) if p.starts_with("/web/branch/") => {
336 let name = &p["/web/branch/".len()..];
337 crate::web::branch_handler(state, name)
338 }
339 (Method::Get, p) if p.starts_with("/web/stage/") => {
340 let id = &p["/web/stage/".len()..];
341 crate::web::stage_html_handler(state, id)
342 }
343 (Method::Post, p) if p.starts_with("/web/stage/") && (
348 p.ends_with("/pin") || p.ends_with("/defer")
349 || p.ends_with("/block") || p.ends_with("/unblock")
350 ) => {
351 let prefix_len = "/web/stage/".len();
352 let last_slash = p.rfind('/').unwrap_or(p.len());
353 let id = &p[prefix_len..last_slash];
354 let verb = &p[last_slash + 1..];
355 let decision = match verb {
356 "pin" => crate::web::WebStageDecision::Pin,
357 "defer" => crate::web::WebStageDecision::Defer,
358 "block" => crate::web::WebStageDecision::Block,
359 "unblock" => crate::web::WebStageDecision::Unblock,
360 _ => unreachable!("matched in outer guard"),
361 };
362 crate::web::stage_decision_handler(state, id, body, decision, x_lex_user)
363 }
364 (Method::Get, "/v1/health") => json_response(200, &serde_json::json!({"ok": true})),
366 (Method::Post, "/v1/parse") => parse_handler(body),
367 (Method::Post, "/v1/check") => check_handler(body),
368 (Method::Post, "/v1/publish") => publish_handler(state, body),
369 (Method::Post, "/v1/patch") => patch_handler(state, body),
370 (Method::Get, p) if p.starts_with("/v1/stage/") => {
371 let suffix = &p["/v1/stage/".len()..];
372 if let Some(id) = suffix.strip_suffix("/attestations") {
375 stage_attestations_handler(state, id)
376 } else {
377 stage_handler(state, suffix)
378 }
379 }
380 (Method::Post, "/v1/run") => run_handler(state, body, false),
381 (Method::Post, "/v1/replay") => run_handler(state, body, true),
382 (Method::Get, p) if p.starts_with("/v1/trace/") => {
383 let id = &p["/v1/trace/".len()..];
384 trace_handler(state, id)
385 }
386 (Method::Get, "/v1/diff") => diff_handler(state, query),
387 (Method::Post, "/v1/merge/start") => merge_start_handler(state, body),
388 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/resolve") => {
389 let id = &p["/v1/merge/".len()..p.len() - "/resolve".len()];
390 merge_resolve_handler(state, id, body)
391 }
392 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/commit") => {
393 let id = &p["/v1/merge/".len()..p.len() - "/commit".len()];
394 merge_commit_handler(state, id)
395 }
396 (Method::Post, "/v1/ops/batch") => ops_batch_handler(state, body),
398 (Method::Post, "/v1/attestations/batch") => attestations_batch_handler(state, body),
399 (Method::Post, "/v1/stages/batch") => crate::sync_http::stages_batch_handler(state, body),
402 (Method::Post, "/v1/stages/fetch") => crate::sync_http::stages_fetch_handler(state, body),
403 (Method::Post, "/v1/intents/batch") => crate::sync_http::intents_batch_handler(state, body),
404 (Method::Post, "/v1/intents/fetch") => crate::sync_http::intents_fetch_handler(state, body),
405 (Method::Get, "/v1/branches") => crate::branches_http::branches_list_handler(state),
409 (Method::Post, "/v1/branches") => crate::branches_http::branch_create_handler(state, body),
410 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/checkout") => {
411 let name = &p["/v1/branches/".len()..p.len() - "/checkout".len()];
412 crate::branches_http::branch_checkout_handler(state, name)
413 }
414 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
417 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
418 crate::branches_http::branch_head_handler(state, name)
419 }
420 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
421 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
422 crate::branches_http::branch_advance_head_handler(state, name, body)
423 }
424 (Method::Get, "/v1/ops/since") => ops_since_handler(state, query),
428 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
429 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
433 (Method::Put, p) if p.starts_with("/v1/pkg/") && p.ends_with("/visibility") => {
437 let name = &p["/v1/pkg/".len()..p.len() - "/visibility".len()];
438 pkg_set_visibility_handler(state, name, body)
439 }
440 (Method::Post, p) if p.starts_with("/v1/pkg/") && p.ends_with("/release") => {
443 let name = &p["/v1/pkg/".len()..p.len() - "/release".len()];
444 pkg_release_handler(state, name, body)
445 }
446 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
447 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
448 pkg_head_handler(state, name)
449 }
450 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/versions") => {
451 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
452 pkg_versions_handler(state, name)
453 }
454 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
456 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
457 if let Some((name, version)) = inner.split_once('/') {
459 pkg_archive_handler(state, name, version)
460 } else {
461 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
462 }
463 }
464 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
466 let inner = &p["/v1/pkg/".len()..];
467 if let Some((name, version)) = inner.split_once('/') {
468 pkg_get_version_handler(state, name, version)
469 } else {
470 error_response(400, "expected /v1/pkg/{name}/{version}")
471 }
472 }
473 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
474 let name = &p["/v1/pkg/".len()..];
475 pkg_get_handler(state, name)
476 }
477 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
478 let name = &p["/v1/pkg/".len()..];
479 pkg_delete_handler(state, name)
480 }
481 _ => error_response(404, format!("unknown route: {method:?} {path}")),
482 }
483}
484
485#[derive(Deserialize)]
486struct ParseReq { source: String }
487
488fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
489 let req: ParseReq = match serde_json::from_str(body) {
490 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
491 };
492 match load_program_from_str(&req.source) {
493 Ok(prog) => {
494 let stages = canonicalize_program(&prog);
495 json_response(200, &serde_json::to_value(&stages).unwrap())
496 }
497 Err(e) => error_response(400, format!("syntax error: {e}")),
498 }
499}
500
501pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
502 let req: ParseReq = match serde_json::from_str(body) {
503 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
504 };
505 let prog = match load_program_from_str(&req.source) {
506 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
507 };
508 let stages = canonicalize_program(&prog);
509 match lex_types::check_program(&stages) {
510 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
511 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
512 }
513}
514
515#[derive(Deserialize)]
516struct PublishReq { source: String, #[serde(default)] activate: bool }
517
518pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
519 let req: PublishReq = match serde_json::from_str(body) {
520 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
521 };
522 let prog = match load_program_from_str(&req.source) {
523 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
524 };
525 let mut stages = canonicalize_program(&prog);
529 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
530 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
531 }
532 let example_errors = lex_runtime::evaluate_examples(&stages);
538 if !example_errors.is_empty() {
539 return error_with_detail(422, "example mismatch",
540 serde_json::to_value(&example_errors).unwrap_or_default());
541 }
542
543 let store = state.store.lock().unwrap();
544 let branch = store.current_branch();
545
546 let old_head = match store.branch_head(&branch) {
548 Ok(h) => h,
549 Err(e) => return error_response(500, format!("branch_head: {e}")),
550 };
551 let old_head_stages: Vec<lex_ast::Stage> =
553 old_head.values().filter_map(|stg| store.get_ast(stg).ok()).collect();
554 let old_fns = stage_fns(&old_head_stages);
555 let new_fns = stage_fns(&stages);
556 let old_types = stage_types(&old_head_stages);
557 let new_types = stage_types(&stages);
558 let report =
559 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
560
561 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
563 {
564 let entry = new_imports.entry("<source>".into()).or_default();
565 for s in &stages {
566 if let lex_ast::Stage::Import(im) = s {
567 entry.insert(lex_vcs::ImportRef {
568 reference: im.reference.clone(),
569 alias: im.alias.clone(),
570 });
571 }
572 }
573 }
574
575 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
576 Ok(outcome) => {
577 record_examples_for_publish(&store, &stages, &outcome);
581 json_response(200, &serde_json::json!({
582 "ops": outcome.ops,
583 "head_op": outcome.head_op,
584 }))
585 }
586 Err(lex_store::StoreError::TypeError(errs)) => {
594 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
595 }
596 Err(e) => write_error_response("publish_program", e),
597 }
598}
599
600#[derive(Deserialize)]
601struct PatchReq {
602 stage_id: String,
603 patch: lex_ast::Patch,
604 #[serde(default)] activate: bool,
605}
606
607fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
610 let req: PatchReq = match serde_json::from_str(body) {
611 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
612 };
613 let store = state.store.lock().unwrap();
614
615 let original = match store.get_ast(&req.stage_id) {
617 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
618 };
619
620 let patched = match lex_ast::apply_patch(&original, &req.patch) {
622 Ok(s) => s,
623 Err(e) => return error_with_detail(422, "patch failed",
624 serde_json::to_value(&e).unwrap_or_default()),
625 };
626
627 let branch = store.current_branch();
638
639 let sig = match lex_ast::sig_id(&patched) {
641 Some(s) => s,
642 None => return error_response(500, "patched stage has no sig_id"),
643 };
644
645 let new_id = match store.publish(&patched) {
648 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
649 };
650
651 let original_effects: std::collections::BTreeSet<String> = match &original {
653 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
654 _ => std::collections::BTreeSet::new(),
655 };
656 let patched_effects: std::collections::BTreeSet<String> = match &patched {
657 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
658 _ => std::collections::BTreeSet::new(),
659 };
660 let head_now = match store.get_branch(&branch) {
661 Ok(b) => b.and_then(|b| b.head_op),
662 Err(e) => return error_response(500, format!("get_branch: {e}")),
663 };
664 let kind = if original_effects != patched_effects {
665 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
672 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
673 lex_vcs::OperationKind::ChangeEffectSig {
674 sig_id: sig.clone(),
675 from_stage_id: req.stage_id.clone(),
676 to_stage_id: new_id.clone(),
677 from_effects: original_effects,
678 to_effects: patched_effects,
679 from_budget,
680 to_budget,
681 }
682 } else {
683 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
684 lex_vcs::OperationKind::ModifyBody {
685 sig_id: sig.clone(),
686 from_stage_id: req.stage_id.clone(),
687 to_stage_id: new_id.clone(),
688 from_budget: budget,
689 to_budget: budget,
690 }
691 };
692 let transition = lex_vcs::StageTransition::Replace {
693 sig_id: sig.clone(),
694 from: req.stage_id.clone(),
695 to: new_id.clone(),
696 };
697 let op = lex_vcs::Operation::new(
698 kind,
699 head_now.into_iter().collect::<Vec<_>>(),
700 );
701 let op_id = match store.apply_operation_gated(&branch, op, transition) {
702 Ok(id) => id,
703 Err(lex_store::StoreError::TypeError(errs)) => return error_with_detail(
704 422, "type errors after patch", serde_json::to_value(&errs).unwrap_or_default()),
705 Err(e) => return write_error_response("apply_operation_gated", e),
706 };
707 if req.activate {
708 if let Err(e) = store.activate(&new_id) {
709 return error_response(500, format!("activate: {e}"));
710 }
711 }
712
713 let status = format!("{:?}",
714 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
715 json_response(200, &serde_json::json!({
716 "old_stage_id": req.stage_id,
717 "new_stage_id": new_id,
718 "sig_id": sig,
719 "status": status,
720 "op_id": op_id,
721 }))
722}
723
724pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
725 let store = state.store.lock().unwrap();
726 let meta = match store.get_metadata(id) {
727 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
728 };
729 let ast = match store.get_ast(id) {
730 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
731 };
732 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
733 json_response(200, &serde_json::json!({
734 "metadata": meta,
735 "ast": ast,
736 "status": status,
737 }))
738}
739
740pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
749 let store = state.store.lock().unwrap();
750 if let Err(e) = store.get_metadata(id) {
751 return error_response(404, format!("{e}"));
752 }
753 let log = match store.attestation_log() {
754 Ok(l) => l,
755 Err(e) => return error_response(500, format!("attestation log: {e}")),
756 };
757 let mut listing = match log.list_for_stage(&id.to_string()) {
758 Ok(v) => v,
759 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
760 };
761 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
762 json_response(200, &serde_json::json!({"attestations": listing}))
763}
764
765#[derive(Deserialize, Default)]
766struct PolicyJson {
767 #[serde(default)] allow_effects: Vec<String>,
768 #[serde(default)] allow_fs_read: Vec<String>,
769 #[serde(default)] allow_fs_write: Vec<String>,
770 #[serde(default)] budget: Option<u64>,
771}
772
773impl PolicyJson {
774 fn into_policy(self) -> Policy {
775 Policy {
776 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
777 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
778 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
779 allow_net_host: Vec::new(),
780 allow_proc: Vec::new(),
781 allow_approval: Vec::new(),
782 budget: self.budget,
783 }
784 }
785}
786
787#[derive(Deserialize)]
788struct RunReq {
789 source: String,
790 #[serde(rename = "fn")] func: String,
791 #[serde(default)] args: Vec<serde_json::Value>,
792 #[serde(default)] policy: PolicyJson,
793 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
794}
795
796pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
797 let req: RunReq = match serde_json::from_str(body) {
798 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
799 };
800 let prog = match load_program_from_str(&req.source) {
801 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
802 };
803 let stages = canonicalize_program(&prog);
804 if let Err(errs) = lex_types::check_program(&stages) {
805 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
806 }
807 let bc = compile_program(&stages);
808 let mut policy = req.policy.into_policy();
809 if let Some(ceiling) = &state.policy_ceiling {
815 policy = clamp_policy(policy, ceiling);
816 }
817 if let Err(violations) = check_policy(&bc, &policy) {
818 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
819 }
820
821 let mut recorder = lex_trace::Recorder::new();
822 if with_overrides && !req.overrides.is_empty() {
823 recorder = recorder.with_overrides(req.overrides);
824 }
825 let handle = recorder.handle();
826 let handler = DefaultHandler::new(policy);
827 let mut vm = Vm::with_handler(&bc, Box::new(handler));
828 vm.set_tracer(Box::new(recorder));
829
830 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
831 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
832 let result = vm.call(&req.func, vargs);
833 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
834
835 let store = state.store.lock().unwrap();
836 let (root_out, root_err, status) = match &result {
837 Ok(v) => (Some(value_to_json(v)), None, 200u16),
838 Err(e) => (None, Some(format!("{e}")), 200u16),
839 };
840 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
841 root_out.clone(), root_err.clone(), started, ended);
842 let run_id = match store.save_trace(&tree) {
843 Ok(id) => id,
844 Err(e) => return error_response(500, format!("save_trace: {e}")),
845 };
846
847 let mut body = serde_json::json!({
848 "run_id": run_id,
849 "output": root_out,
850 });
851 if let Some(err) = root_err {
852 body["error"] = serde_json::Value::String(err);
853 }
854 json_response(status, &body)
855}
856
857fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
858 let store = state.store.lock().unwrap();
859 match store.load_trace(id) {
860 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
861 Err(e) => error_response(404, format!("{e}")),
862 }
863}
864
865fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
866 let mut a = None;
867 let mut b = None;
868 for kv in query.split('&') {
869 if let Some((k, v)) = kv.split_once('=') {
870 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
871 }
872 }
873 let (Some(a), Some(b)) = (a, b) else {
874 return error_response(400, "missing a or b query params");
875 };
876 let store = state.store.lock().unwrap();
877 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
878 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
879 match lex_trace::diff_runs(&ta, &tb) {
880 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
881 None => json_response(200, &serde_json::json!({"divergence": null})),
882 }
883}
884
885fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
886
887fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
888
889#[derive(Deserialize)]
890struct MergeStartReq {
891 src_branch: String,
892 dst_branch: String,
893}
894
895fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
906 let req: MergeStartReq = match serde_json::from_str(body) {
907 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
908 };
909 let store = state.store.lock().unwrap();
910 let src_head = match store.get_branch(&req.src_branch) {
911 Ok(Some(b)) => b.head_op,
912 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
913 Err(e) => return error_response(500, format!("src branch read: {e}")),
914 };
915 let dst_head = match store.get_branch(&req.dst_branch) {
916 Ok(Some(b)) => b.head_op,
917 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
918 Err(e) => return error_response(500, format!("dst branch read: {e}")),
919 };
920 let log = match lex_vcs::OpLog::open(store.root()) {
921 Ok(l) => l,
922 Err(e) => return error_response(500, format!("op log: {e}")),
923 };
924 let merge_id = mint_merge_id();
928 let session = match MergeSession::start(
929 merge_id.clone(),
930 &log,
931 src_head.as_ref(),
932 dst_head.as_ref(),
933 ) {
934 Ok(s) => s,
935 Err(e) => return error_response(500, format!("merge start: {e}")),
936 };
937 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
938 let auto_resolved_count = session.auto_resolved.len();
939 let body = serde_json::json!({
940 "merge_id": merge_id,
941 "src_head": session.src_head,
942 "dst_head": session.dst_head,
943 "lca": session.lca,
944 "conflicts": conflicts,
945 "auto_resolved_count": auto_resolved_count,
946 });
947 drop(conflicts);
948 drop(store);
949 let wrapped = ApiMergeSession {
950 inner: session,
951 src_branch: req.src_branch,
952 dst_branch: req.dst_branch,
953 };
954 state.sessions.lock().unwrap().insert(merge_id, wrapped);
955 json_response(200, &body)
956}
957
958#[derive(Deserialize)]
959struct MergeResolveReq {
960 resolutions: Vec<MergeResolveEntry>,
965}
966
967#[derive(Deserialize)]
968struct MergeResolveEntry {
969 conflict_id: String,
970 resolution: lex_vcs::Resolution,
971}
972
973fn merge_resolve_handler(
984 state: &State,
985 merge_id: &str,
986 body: &str,
987) -> Response<std::io::Cursor<Vec<u8>>> {
988 let req: MergeResolveReq = match serde_json::from_str(body) {
989 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
990 };
991 let mut sessions = state.sessions.lock().unwrap();
992 let Some(wrapped) = sessions.get_mut(merge_id) else {
993 return error_response(404, format!("unknown merge_id `{merge_id}`"));
994 };
995 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
996 .map(|e| (e.conflict_id, e.resolution))
997 .collect();
998 let store = state.store.lock().unwrap();
1003 let checker = lex_store::MergeResolutionChecker::new(&store, wrapped.dst_branch.clone());
1004 let verdicts = wrapped.inner.resolve_checked(pairs, &checker);
1005 drop(store);
1006 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
1007 let body = serde_json::json!({
1008 "verdicts": verdicts,
1009 "remaining_conflicts": remaining,
1010 });
1011 json_response(200, &body)
1012}
1013
1014fn merge_commit_handler(
1033 state: &State,
1034 merge_id: &str,
1035) -> Response<std::io::Cursor<Vec<u8>>> {
1036 use std::collections::BTreeMap;
1037 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
1038 Some(w) => w,
1039 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
1040 };
1041 let dst_branch = wrapped.dst_branch.clone();
1042 let src_head = wrapped.inner.src_head.clone();
1043 let dst_head = wrapped.inner.dst_head.clone();
1044 let auto_resolved = wrapped.inner.auto_resolved.clone();
1045
1046 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
1049
1050 for outcome in &auto_resolved {
1052 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
1053 entries.insert(sig_id.clone(), stage_id.clone());
1054 }
1055 }
1056
1057 let resolved = match wrapped.inner.commit() {
1059 Ok(r) => r,
1060 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
1061 return error_with_detail(
1065 422,
1066 "conflicts remaining",
1067 serde_json::json!({"unresolved": ids}),
1068 );
1069 }
1070 };
1071
1072 for (conflict_id, resolution) in resolved {
1073 match resolution {
1074 lex_vcs::Resolution::TakeOurs => {
1075 }
1077 lex_vcs::Resolution::TakeTheirs => {
1078 match resolve_take_theirs(state, &src_head, &conflict_id) {
1088 Ok(stage_id) => {
1089 entries.insert(conflict_id.clone(), stage_id);
1090 }
1091 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1092 }
1093 }
1094 lex_vcs::Resolution::Custom { op } => {
1095 match op.kind.merge_target() {
1104 Some((sig, stage)) => {
1105 if sig != conflict_id {
1106 return error_with_detail(
1107 422,
1108 "custom op targets a different sig than the conflict",
1109 serde_json::json!({
1110 "conflict_id": conflict_id,
1111 "op_targets": sig,
1112 }),
1113 );
1114 }
1115 entries.insert(conflict_id, stage);
1116 }
1117 None => {
1118 return error_with_detail(
1119 422,
1120 "custom op kind doesn't yield a single sig→stage delta",
1121 serde_json::json!({
1122 "conflict_id": conflict_id,
1123 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1124 }),
1125 );
1126 }
1127 }
1128 }
1129 lex_vcs::Resolution::Defer => {
1130 return error_response(500, "internal: Defer slipped past commit gate");
1132 }
1133 }
1134 }
1135
1136 let resolved_count = entries.len();
1137 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1138 if let Some(d) = dst_head { parents.push(d); }
1139 if let Some(s) = src_head { parents.push(s); }
1140 let op = lex_vcs::Operation::new(
1141 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1142 parents,
1143 );
1144 let transition = lex_vcs::StageTransition::Merge { entries };
1145 let store = state.store.lock().unwrap();
1146 match store.apply_merge_op_gated(&dst_branch, op, transition) {
1149 Ok(new_head_op) => json_response(200, &serde_json::json!({
1150 "new_head_op": new_head_op,
1151 "dst_branch": dst_branch,
1152 })),
1153 Err(lex_store::StoreError::TypeError(errs)) => error_with_detail(
1154 422, "merged program has type errors", serde_json::to_value(&errs).unwrap_or_default()),
1155 Err(e) => write_error_response("apply merge op", e),
1156 }
1157}
1158
1159fn resolve_take_theirs(
1164 state: &State,
1165 src_head: &Option<lex_vcs::OpId>,
1166 sig: &lex_vcs::SigId,
1167) -> std::io::Result<Option<lex_vcs::StageId>> {
1168 let store = state.store.lock().unwrap();
1169 let log = lex_vcs::OpLog::open(store.root())?;
1170 let Some(head) = src_head.as_ref() else { return Ok(None); };
1171 let mut current: Option<lex_vcs::StageId> = None;
1174 for record in log.walk_forward(head, None)? {
1175 match &record.produces {
1176 lex_vcs::StageTransition::Create { sig_id, stage_id }
1177 if sig_id == sig => { current = Some(stage_id.clone()); }
1178 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1179 if sig_id == sig => { current = Some(to.clone()); }
1180 lex_vcs::StageTransition::Remove { sig_id, .. }
1181 if sig_id == sig => { current = None; }
1182 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1183 if from == sig || to == sig => {
1184 if from == sig { current = None; }
1185 if to == sig { current = Some(body_stage_id.clone()); }
1186 }
1187 lex_vcs::StageTransition::Merge { entries } => {
1188 if let Some(opt) = entries.get(sig) {
1189 current = opt.clone();
1190 }
1191 }
1192 _ => {}
1193 }
1194 }
1195 Ok(current)
1196}
1197
1198fn mint_merge_id() -> MergeSessionId {
1199 use std::sync::atomic::{AtomicU64, Ordering};
1200 static COUNTER: AtomicU64 = AtomicU64::new(0);
1201 let nanos = SystemTime::now()
1202 .duration_since(UNIX_EPOCH)
1203 .map(|d| d.as_nanos())
1204 .unwrap_or(0);
1205 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1206 format!("merge_{nanos:x}_{n:x}")
1207}
1208
1209pub(crate) fn ops_batch_handler(state: &State, body: &str)
1240 -> Response<std::io::Cursor<Vec<u8>>>
1241{
1242 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1243 Ok(r) => r,
1244 Err(e) => return error_response(400,
1245 format!("body must be a JSON array of OperationRecord: {e}")),
1246 };
1247 let store = state.store.lock().unwrap();
1248 let log = match lex_vcs::OpLog::open(store.root()) {
1249 Ok(l) => l,
1250 Err(e) => return error_response(500, format!("opening op log: {e}")),
1251 };
1252
1253 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1261 std::collections::BTreeSet::new();
1262 for rec in &records {
1263 let expected = rec.op.op_id();
1264 if expected != rec.op_id {
1265 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1266 "supplied": rec.op_id,
1267 "expected": expected,
1268 }));
1269 }
1270 for parent in &rec.op.parents {
1271 let known = match log.get(parent) {
1272 Ok(Some(_)) => true,
1273 Ok(None) => false,
1274 Err(e) => return error_response(500, format!("op log read: {e}")),
1275 };
1276 if !known && !batch_ids.contains(parent) {
1277 return error_with_detail(422, "MissingParent", serde_json::json!({
1278 "op_id": rec.op_id,
1279 "missing_parent": parent,
1280 }));
1281 }
1282 }
1283 batch_ids.insert(rec.op_id.clone());
1284 }
1285
1286 let mut added = 0usize;
1289 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1290 for rec in &records {
1291 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1292 match log.put(rec) {
1293 Ok(()) => {
1294 if !already_present {
1295 added += 1;
1296 added_ids.push(&rec.op_id);
1297 }
1298 }
1299 Err(e) => return error_response(500, format!("op log write: {e}")),
1300 }
1301 }
1302
1303 json_response(200, &serde_json::json!({
1304 "received": records.len(),
1305 "added": added,
1306 "skipped": records.len() - added,
1307 "added_ids": added_ids,
1308 }))
1309}
1310
1311pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1333 -> Response<std::io::Cursor<Vec<u8>>>
1334{
1335 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1336 Ok(a) => a,
1337 Err(e) => return error_response(400,
1338 format!("body must be a JSON array of Attestation: {e}")),
1339 };
1340 let store = state.store.lock().unwrap();
1341 let log = match store.attestation_log() {
1342 Ok(l) => l,
1343 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1344 };
1345 let op_log = match lex_vcs::OpLog::open(store.root()) {
1346 Ok(l) => l,
1347 Err(e) => return error_response(500, format!("opening op log: {e}")),
1348 };
1349
1350 for att in &attestations {
1352 let expected = lex_vcs::Attestation::with_timestamp(
1355 att.stage_id.clone(),
1356 att.op_id.clone(),
1357 att.intent_id.clone(),
1358 att.kind.clone(),
1359 att.result.clone(),
1360 att.produced_by.clone(),
1361 att.cost.clone(),
1362 att.timestamp,
1363 ).attestation_id;
1364 if expected != att.attestation_id {
1365 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1366 "supplied": att.attestation_id,
1367 "expected": expected,
1368 }));
1369 }
1370 if let Some(op_id) = &att.op_id {
1374 match op_log.get(op_id) {
1375 Ok(Some(_)) => {}
1376 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1377 "attestation_id": att.attestation_id,
1378 "op_id": op_id,
1379 })),
1380 Err(e) => return error_response(500, format!("op log read: {e}")),
1381 }
1382 }
1383 }
1384
1385 let mut added = 0usize;
1389 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1390 for att in &attestations {
1391 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1392 match log.put(att) {
1393 Ok(()) => {
1394 if !already_present {
1395 added += 1;
1396 added_ids.push(&att.attestation_id);
1397 }
1398 }
1399 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1400 }
1401 }
1402
1403 json_response(200, &serde_json::json!({
1404 "received": attestations.len(),
1405 "added": added,
1406 "skipped": attestations.len() - added,
1407 "added_ids": added_ids,
1408 }))
1409}
1410
1411pub(crate) fn ops_since_handler(state: &State, query: &str)
1443 -> Response<std::io::Cursor<Vec<u8>>>
1444{
1445 let mut after: Option<String> = None;
1446 let mut branch = String::from("main");
1447 let mut limit: Option<usize> = None;
1448 for kv in query.split('&') {
1449 let Some((k, v)) = kv.split_once('=') else { continue };
1450 match k {
1451 "after" => after = Some(v.to_string()),
1452 "branch" => branch = v.to_string(),
1453 "limit" => {
1454 limit = Some(match v.parse::<usize>() {
1455 Ok(n) => n,
1456 Err(_) => return error_response(400,
1457 format!("limit must be a positive integer, got `{v}`")),
1458 });
1459 }
1460 _ => {}
1461 }
1462 }
1463
1464 let store = state.store.lock().unwrap();
1465 let log = match lex_vcs::OpLog::open(store.root()) {
1466 Ok(l) => l,
1467 Err(e) => return error_response(500, format!("opening op log: {e}")),
1468 };
1469 let head = match store.get_branch(&branch) {
1470 Ok(Some(b)) => b.head_op,
1471 Ok(None) => None,
1472 Err(e) => return error_response(500, format!("get_branch: {e}")),
1473 };
1474 let Some(head) = head else {
1475 return json_response(200, &serde_json::json!([]));
1476 };
1477
1478 let ops_since = match log.ops_since(&head, after.as_ref()) {
1479 Ok(o) => o,
1480 Err(e) => return error_response(500, format!("ops_since: {e}")),
1481 };
1482 let mut ops = ops_since;
1486 ops.reverse();
1487 if let Some(n) = limit {
1488 ops.truncate(n);
1489 }
1490
1491 json_response(200, &serde_json::to_value(&ops).unwrap_or_default())
1492}
1493
1494pub(crate) fn attestations_since_handler(state: &State, query: &str)
1507 -> Response<std::io::Cursor<Vec<u8>>>
1508{
1509 let mut after_op: Option<String> = None;
1510 let mut limit: Option<usize> = None;
1511 for kv in query.split('&') {
1512 let Some((k, v)) = kv.split_once('=') else { continue };
1513 match k {
1514 "after-op" => after_op = Some(v.to_string()),
1515 "limit" => {
1516 limit = Some(match v.parse::<usize>() {
1517 Ok(n) => n,
1518 Err(_) => return error_response(400,
1519 format!("limit must be a positive integer, got `{v}`")),
1520 });
1521 }
1522 _ => {}
1523 }
1524 }
1525
1526 let store = state.store.lock().unwrap();
1527 let log = match store.attestation_log() {
1528 Ok(l) => l,
1529 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1530 };
1531
1532 let exclude: std::collections::BTreeSet<String> = match &after_op {
1536 None => std::collections::BTreeSet::new(),
1537 Some(cutoff) => {
1538 let op_log = match lex_vcs::OpLog::open(store.root()) {
1539 Ok(l) => l,
1540 Err(e) => return error_response(500, format!("opening op log: {e}")),
1541 };
1542 match op_log.walk_back(cutoff, None) {
1543 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1544 Err(_) => {
1545 std::collections::BTreeSet::new()
1549 }
1550 }
1551 }
1552 };
1553
1554 let all = match log.list_all() {
1555 Ok(v) => v,
1556 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1557 };
1558 let mut filtered: Vec<lex_vcs::Attestation> = all
1559 .into_iter()
1560 .filter(|a| match &a.op_id {
1561 Some(op_id) => !exclude.contains(op_id),
1562 None => true,
1566 })
1567 .collect();
1568 filtered.sort_by(|a, b| {
1572 a.timestamp.cmp(&b.timestamp)
1573 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1574 });
1575 if let Some(n) = limit {
1576 filtered.truncate(n);
1577 }
1578
1579 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1580}
1581
1582#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1586struct PkgRecord {
1587 name: String,
1588 version: String,
1589 head_op: Option<String>,
1590 published_at: u64,
1591 function_names: Vec<String>,
1593 ops: Vec<serde_json::Value>,
1595}
1596
1597#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
1604#[serde(rename_all = "lowercase")]
1605pub enum Visibility {
1606 #[default]
1607 Private,
1608 Public,
1609}
1610
1611#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1616struct PkgIndex {
1617 latest: Option<String>,
1619 versions: Vec<PkgVersionSummary>,
1621 #[serde(default)]
1625 visibility: Visibility,
1626}
1627
1628#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1629struct PkgVersionSummary {
1630 version: String,
1631 head_op: Option<String>,
1632 published_at: u64,
1633}
1634
1635fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1636 root.join("packages").join(name)
1637}
1638
1639fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
1640 pkg_name_dir(root, name).join("index.json")
1641}
1642
1643fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1644 pkg_name_dir(root, name).join(format!("{version}.json"))
1645}
1646
1647fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1648 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
1649}
1650
1651fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
1652 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
1653 serde_json::from_slice(&bytes).ok()
1654}
1655
1656fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
1657 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
1658 serde_json::from_slice(&bytes).ok()
1659}
1660
1661fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1662 let index = load_pkg_index(root, name)?;
1663 let latest = index.latest.clone()?;
1664 load_pkg_record(root, name, &latest)
1665}
1666
1667fn pkg_is_public(root: &std::path::Path, name: &str) -> bool {
1671 load_pkg_index(root, name).map(|i| i.visibility) == Some(Visibility::Public)
1672}
1673
1674fn valid_pkg_segment(s: &str) -> bool {
1679 !s.is_empty()
1680 && s.len() <= 128
1681 && s != "."
1682 && s != ".."
1683 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
1684}
1685
1686#[derive(Deserialize)]
1687struct VisibilityReq {
1688 visibility: Visibility,
1689}
1690
1691fn pkg_set_visibility_handler(
1698 state: &State,
1699 name: &str,
1700 body: &str,
1701) -> Response<std::io::Cursor<Vec<u8>>> {
1702 if !valid_pkg_segment(name) {
1703 return error_response(400, format!("invalid package name {name:?}"));
1704 }
1705 let req: VisibilityReq = match serde_json::from_str(body) {
1706 Ok(r) => r,
1707 Err(e) => return error_response(400, format!("bad request: {e}")),
1708 };
1709 let mut index = match load_pkg_index(&state.root, name) {
1710 Some(i) => i,
1711 None => return error_response(404, format!("package {name:?} not found")),
1712 };
1713 index.visibility = req.visibility;
1714 let bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1715 match std::fs::write(pkg_index_path(&state.root, name), bytes) {
1716 Ok(()) => json_response(
1717 200,
1718 &serde_json::json!({ "name": name, "visibility": index.visibility }),
1719 ),
1720 Err(e) => error_response(500, format!("write index: {e}")),
1721 }
1722}
1723
1724#[derive(serde::Deserialize)]
1725struct ReleaseReq {
1726 version: String,
1727 #[serde(default)]
1728 branch: Option<String>,
1729}
1730
1731fn pkg_release_handler(state: &State, name: &str, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1740 if !valid_pkg_segment(name) {
1741 return error_response(400, format!("invalid package name {name:?}"));
1742 }
1743 let req: ReleaseReq = match serde_json::from_str(body) {
1744 Ok(r) => r,
1745 Err(e) => return error_response(400, format!("bad request: {e}")),
1746 };
1747 let version = req.version.trim().to_string();
1748 if version.is_empty() || !valid_pkg_segment(&version) {
1749 return error_response(400, "version must be a non-empty, path-safe string (e.g. 1.2.0)");
1750 }
1751 if load_pkg_record(&state.root, name, &version).is_some() {
1753 return error_response(
1754 409,
1755 format!("{name}@{version} already released; releases are immutable — bump the version"),
1756 );
1757 }
1758
1759 let store = state.store.lock().unwrap();
1760 let branch = req.branch.unwrap_or_else(|| store.current_branch());
1761 let head_op = match store.get_branch(&branch) {
1762 Ok(Some(b)) => b.head_op,
1763 Ok(None) => return error_response(404, format!("unknown branch {branch:?}")),
1764 Err(e) => return error_response(500, format!("get_branch: {e}")),
1765 };
1766 let Some(head_op) = head_op else {
1767 return error_response(400, format!("branch {branch:?} has no commits to release"));
1768 };
1769
1770 let predecessor = load_pkg_index(&state.root, name)
1781 .map(|i| i.versions)
1782 .unwrap_or_default()
1783 .into_iter()
1784 .filter_map(|v| lex_syntax::semver::parse_exact(&v.version).map(|p| (p, v)))
1785 .filter(|(p, _)| lex_syntax::semver::parse_exact(&version).map(|n| *p < n).unwrap_or(false))
1786 .max_by_key(|(p, _)| *p)
1787 .map(|(_, v)| v);
1788 if let Some(prev) = predecessor {
1789 if let (Some(prev_head), Some(declared)) = (
1790 prev.head_op.clone(),
1791 lex_syntax::semver::bump_between(&prev.version, &version),
1792 ) {
1793 if let (Ok(prev_api), Ok(new_api)) = (
1794 lex_store::api::public_api_at_op(&store, &prev_head),
1795 lex_store::api::public_api_at_op(&store, &head_op),
1796 ) {
1797 use lex_store::api::ApiChange;
1798 use lex_syntax::semver::Bump;
1799 let (required, why) = match lex_store::api::classify_api_change(&prev_api, &new_api) {
1800 ApiChange::Breaking(d) => (Bump::Major, d),
1801 ApiChange::Additive(d) => (Bump::Minor, d),
1802 ApiChange::None => (Bump::Patch, String::new()),
1803 };
1804 if declared < required {
1805 let need = match required {
1806 Bump::Major => "major",
1807 Bump::Minor => "minor",
1808 Bump::Patch => "patch",
1809 };
1810 return error_response(
1811 422,
1812 format!(
1813 "version bump too small: {} → {version} is a {declared:?} bump, \
1814 but the API change ({why}) requires a {need} bump",
1815 prev.version
1816 ),
1817 );
1818 }
1819 }
1820 }
1821 }
1822
1823 let head = store.branch_head(&branch).unwrap_or_default();
1826 let pairs: Vec<(String, String)> = head.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
1827 let function_names: Vec<String> = store
1828 .get_asts_for_sigs_bulk(&pairs)
1829 .into_iter()
1830 .filter_map(|r| r.ok())
1831 .filter_map(|s| match s {
1832 lex_ast::Stage::FnDecl(fd) => Some(fd.name),
1833 _ => None,
1834 })
1835 .collect();
1836 drop(store);
1837
1838 let published_at = std::time::SystemTime::now()
1839 .duration_since(std::time::UNIX_EPOCH)
1840 .map(|d| d.as_secs())
1841 .unwrap_or(0);
1842 let record = PkgRecord {
1843 name: name.to_string(),
1844 version: version.clone(),
1845 head_op: Some(head_op.clone()),
1846 published_at,
1847 function_names,
1848 ops: Vec::new(),
1849 };
1850 if let Err(e) = save_pkg_record(&state.root, &record, None) {
1851 return error_response(500, format!("write release: {e}"));
1852 }
1853 json_response(
1854 201,
1855 &serde_json::json!({
1856 "name": name,
1857 "version": version,
1858 "head_op": head_op,
1859 "branch": branch,
1860 }),
1861 )
1862}
1863
1864fn public_pkg_names(root: &std::path::Path) -> Vec<String> {
1867 list_pkg_names(root)
1868 .into_iter()
1869 .filter(|name| pkg_is_public(root, name))
1870 .collect()
1871}
1872
1873fn public_pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
1877 let packages: Vec<serde_json::Value> = public_pkg_names(&state.root)
1878 .iter()
1879 .filter_map(|name| {
1880 let r = load_latest_pkg_record(&state.root, name)?;
1881 Some(serde_json::json!({
1882 "name": r.name,
1883 "version": r.version,
1884 "head_op": r.head_op,
1885 "published_at": r.published_at,
1886 }))
1887 })
1888 .collect();
1889 json_response(200, &serde_json::json!({ "packages": packages }))
1890}
1891
1892#[derive(Debug, PartialEq, Eq)]
1897enum PublicTarget {
1898 List,
1899 Latest(String),
1900 Versions(String),
1901 Head(String),
1902 Version(String, String),
1903 Archive(String, String),
1904}
1905
1906impl PublicTarget {
1907 fn pkg_name(&self) -> Option<&str> {
1909 match self {
1910 PublicTarget::List => None,
1911 PublicTarget::Latest(n)
1912 | PublicTarget::Versions(n)
1913 | PublicTarget::Head(n)
1914 | PublicTarget::Version(n, _)
1915 | PublicTarget::Archive(n, _) => Some(n),
1916 }
1917 }
1918}
1919
1920fn resolve_public(method: &Method, path: &str) -> Result<PublicTarget, u16> {
1925 if !matches!(method, Method::Get) {
1926 return Err(405);
1927 }
1928 let rest = path.trim_matches('/');
1929 if rest.is_empty() {
1930 return Ok(PublicTarget::List);
1931 }
1932 let segs: Vec<&str> = rest.split('/').collect();
1933 if !segs.iter().all(|s| valid_pkg_segment(s)) {
1934 return Err(404);
1935 }
1936 match segs.as_slice() {
1937 [n] => Ok(PublicTarget::Latest(n.to_string())),
1938 [n, "versions"] => Ok(PublicTarget::Versions(n.to_string())),
1939 [n, "head"] => Ok(PublicTarget::Head(n.to_string())),
1940 [n, v, "archive"] => Ok(PublicTarget::Archive(n.to_string(), v.to_string())),
1941 [n, v] => Ok(PublicTarget::Version(n.to_string(), v.to_string())),
1942 _ => Err(404),
1943 }
1944}
1945
1946pub fn route_public(
1957 state: &State,
1958 method: &Method,
1959 path: &str,
1960 _query: &str,
1961) -> Response<std::io::Cursor<Vec<u8>>> {
1962 let target = match resolve_public(method, path) {
1963 Ok(t) => t,
1964 Err(405) => return error_response(405, "public read is GET-only"),
1965 Err(_) => return error_response(404, "not found"),
1966 };
1967 if let PublicTarget::List = target {
1969 return public_pkg_list_handler(state);
1970 }
1971 if let Some(name) = target.pkg_name() {
1973 if !pkg_is_public(&state.root, name) {
1974 return error_response(404, format!("package {name:?} not found"));
1975 }
1976 }
1977 match target {
1978 PublicTarget::List => unreachable!("handled above"),
1979 PublicTarget::Latest(n) => pkg_get_handler(state, &n),
1980 PublicTarget::Versions(n) => pkg_versions_handler(state, &n),
1981 PublicTarget::Head(n) => pkg_head_handler(state, &n),
1982 PublicTarget::Version(n, v) => pkg_get_version_handler(state, &n, &v),
1983 PublicTarget::Archive(n, v) => pkg_archive_handler(state, &n, &v),
1984 }
1985}
1986
1987fn save_pkg_record(
1988 root: &std::path::Path,
1989 record: &PkgRecord,
1990 archive: Option<&[u8]>,
1993) -> std::io::Result<()> {
1994 let dir = pkg_name_dir(root, &record.name);
1995 std::fs::create_dir_all(&dir)?;
1996
1997 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
1999 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
2000
2001 if let Some(archive) = archive {
2003 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
2004 }
2005
2006 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
2008 index.latest = Some(record.version.clone());
2009 if !index.versions.iter().any(|v| v.version == record.version) {
2010 index.versions.push(PkgVersionSummary {
2011 version: record.version.clone(),
2012 head_op: record.head_op.clone(),
2013 published_at: record.published_at,
2014 });
2015 }
2016 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
2017 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
2018}
2019
2020fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
2021 let dir = root.join("packages");
2022 let Ok(entries) = std::fs::read_dir(&dir) else {
2023 return Vec::new();
2024 };
2025 let mut names: Vec<String> = entries
2026 .filter_map(|e| e.ok())
2027 .filter(|e| e.path().is_dir())
2028 .filter_map(|e| e.file_name().into_string().ok())
2029 .collect();
2030 names.sort();
2031 names
2032}
2033
2034fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
2035 let Ok(entries) = std::fs::read_dir(dir) else { return };
2036 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
2037 entries.sort_by_key(|e| e.path());
2038 for entry in entries {
2039 let path = entry.path();
2040 if path.is_dir() {
2041 collect_lex_files(&path, out);
2042 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
2043 out.push(path);
2044 }
2045 }
2046}
2047
2048fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
2051 let tmp = match tempfile::TempDir::new() {
2052 Ok(t) => t,
2053 Err(e) => return error_response(500, format!("create temp dir: {e}")),
2054 };
2055 {
2056 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
2057 let mut ar = tar::Archive::new(gz);
2058 if let Err(e) = ar.unpack(tmp.path()) {
2059 return error_response(400, format!("unpack archive: {e}"));
2060 }
2061 }
2062
2063 let toml_path = tmp.path().join("lex.toml");
2064 if !toml_path.exists() {
2065 return error_response(400, "archive must contain lex.toml at root");
2066 }
2067 let manifest = match Manifest::load(&toml_path) {
2068 Ok(m) => m,
2069 Err(e) => return error_response(400, format!("lex.toml: {e}")),
2070 };
2071 let (pkg_name, pkg_version) = match &manifest.package {
2072 Some(m) => (m.name.clone(), m.version.clone()),
2073 None => return error_response(400, "lex.toml must have a [package] section"),
2074 };
2075
2076 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
2080 return error_response(
2081 409,
2082 format!(
2083 "package {pkg_name}@{pkg_version} already published; \
2084 bump the version in lex.toml to publish a new release"
2085 ),
2086 );
2087 }
2088
2089 let src_dir = tmp.path().join("src");
2090 if !src_dir.exists() {
2091 return error_response(400, "archive must contain a src/ directory");
2092 }
2093 let mut lex_files: Vec<PathBuf> = Vec::new();
2094 collect_lex_files(&src_dir, &mut lex_files);
2095 if lex_files.is_empty() {
2096 return error_response(400, "no .lex files found in src/");
2097 }
2098
2099 let store = state.store.lock().unwrap();
2100 let branch = store.current_branch();
2101
2102 let old_head = match store.branch_head(&branch) {
2122 Ok(h) => h,
2123 Err(e) => return error_response(500, format!("branch_head: {e}")),
2124 };
2125 let old_pairs: Vec<(String, String)> =
2132 old_head.iter().map(|(sig, stage)| (sig.clone(), stage.clone())).collect();
2133 let mut old_fns_by_name: BTreeMap<String, Vec<lex_ast::FnDecl>> = BTreeMap::new();
2134 for fd in store.get_asts_for_sigs_bulk(&old_pairs)
2135 .into_iter()
2136 .filter_map(|r| r.ok())
2137 .filter_map(|s| match s { lex_ast::Stage::FnDecl(fd) => Some(fd), _ => None })
2138 {
2139 old_fns_by_name.entry(fd.name.clone()).or_default().push(fd);
2140 }
2141 let mut old_types_by_name: BTreeMap<String, lex_ast::TypeDecl> = BTreeMap::new();
2144 for td in store.get_asts_for_sigs_bulk(&old_pairs)
2145 .into_iter()
2146 .filter_map(|r| r.ok())
2147 .filter_map(|s| match s { lex_ast::Stage::TypeDecl(td) => Some(td), _ => None })
2148 {
2149 old_types_by_name.insert(td.name.clone(), td);
2150 }
2151 fn structural_key(fd: &lex_ast::FnDecl) -> Option<String> {
2159 let mut anon = fd.clone();
2160 anon.name = String::new();
2161 lex_ast::sig_id(&lex_ast::Stage::FnDecl(anon))
2162 }
2163
2164 fn take_matching(
2174 map: &mut BTreeMap<String, Vec<lex_ast::FnDecl>>,
2175 name: &str,
2176 new_fd: &lex_ast::FnDecl,
2177 ) -> Option<lex_ast::FnDecl> {
2178 let candidates = map.get_mut(name)?;
2179 let idx = match candidates.len() {
2180 0 => return None,
2181 1 => 0,
2182 _ => {
2183 let want = structural_key(new_fd);
2184 candidates.iter().position(|c| structural_key(c) == want)?
2185 }
2186 };
2187 let matched = candidates.remove(idx);
2188 if candidates.is_empty() {
2189 map.remove(name);
2190 }
2191 Some(matched)
2192 }
2193
2194 let loaded = match load_package(&lex_files, tmp.path(), &pkg_name) {
2214 Ok(p) => p,
2215 Err(e) => return error_response(400, format!("load package: {e}")),
2216 };
2217 let mut stages = canonicalize_program(&loaded.program);
2218 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
2224 return error_with_detail(
2225 422,
2226 format!("type errors in package {pkg_name}"),
2227 serde_json::to_value(&errs).unwrap(),
2228 );
2229 }
2230 let new_fns = stage_fns(&stages);
2231 let all_function_names: Vec<String> = new_fns.keys().cloned().collect();
2232
2233 let mut old_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2238 for (name, new_fd) in &new_fns {
2239 if let Some(fd) = take_matching(&mut old_fns_by_name, name, new_fd) {
2240 old_fns.insert(name.clone(), fd);
2241 }
2242 }
2243 let new_types = stage_types(&stages);
2244 let old_types: BTreeMap<String, lex_ast::TypeDecl> = new_types
2248 .keys()
2249 .filter_map(|n| old_types_by_name.get(n).map(|td| (n.clone(), td.clone())))
2250 .collect();
2251 let report =
2252 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
2253
2254 let mut new_imports = lex_vcs::ImportMap::new();
2264 for (file, modules) in &loaded.imports_by_file {
2265 let entry = new_imports.entry(file.clone()).or_default();
2266 for m in modules {
2267 entry.insert(lex_vcs::ImportRef {
2268 reference: m.clone(),
2269 alias: lex_vcs::default_import_alias(m),
2270 });
2271 }
2272 }
2273
2274 let outcome = match store.publish_program_with_intent(
2277 &branch,
2278 &stages,
2279 &report,
2280 &new_imports,
2281 false,
2282 None,
2283 None,
2284 &loaded.module_prefixes,
2285 ) {
2286 Ok(outcome) => outcome,
2287 Err(lex_store::StoreError::TypeError(errs)) => {
2288 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
2289 }
2290 Err(e) => return write_error_response("publish_program", e),
2291 };
2292 let all_ops: Vec<serde_json::Value> = match serde_json::to_value(&outcome.ops) {
2293 Ok(serde_json::Value::Array(arr)) => arr,
2294 _ => Vec::new(),
2295 };
2296 let final_head_op = outcome.head_op;
2297
2298 let now = SystemTime::now()
2323 .duration_since(UNIX_EPOCH)
2324 .map(|d| d.as_secs())
2325 .unwrap_or(0);
2326 let record = PkgRecord {
2327 name: pkg_name.clone(),
2328 version: pkg_version,
2329 head_op: final_head_op.clone(),
2330 published_at: now,
2331 function_names: all_function_names,
2332 ops: all_ops.clone(),
2333 };
2334 if let Err(e) = save_pkg_record(&state.root, &record, Some(body)) {
2335 return error_response(500, format!("save package index: {e}"));
2336 }
2337
2338 json_response(200, &serde_json::json!({
2339 "package": pkg_name,
2340 "ops": all_ops,
2341 "head_op": final_head_op,
2342 }))
2343}
2344
2345fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2347 let names = list_pkg_names(&state.root);
2348 let packages: Vec<serde_json::Value> = names.iter()
2349 .filter_map(|name| {
2350 let idx = load_pkg_index(&state.root, name)?;
2351 let latest = idx.latest.as_deref()?;
2352 let r = load_pkg_record(&state.root, name, latest)?;
2353 Some(serde_json::json!({
2354 "name": r.name,
2355 "version": r.version,
2356 "head_op": r.head_op,
2357 "published_at": r.published_at,
2358 }))
2359 })
2360 .collect();
2361 json_response(200, &serde_json::json!({ "packages": packages }))
2362}
2363
2364fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2366 match load_latest_pkg_record(&state.root, name) {
2367 Some(r) => json_response(200, &serde_json::json!({
2368 "name": r.name,
2369 "version": r.version,
2370 "head_op": r.head_op,
2371 "published_at": r.published_at,
2372 "function_names": r.function_names,
2373 "ops": r.ops,
2374 })),
2375 None => error_response(404, format!("package {name:?} not found")),
2376 }
2377}
2378
2379fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2381 match load_pkg_index(&state.root, name) {
2382 Some(idx) => json_response(200, &serde_json::json!({
2383 "name": name,
2384 "latest": idx.latest,
2385 "versions": idx.versions,
2386 })),
2387 None => error_response(404, format!("package {name:?} not found")),
2388 }
2389}
2390
2391fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2393 match load_pkg_record(&state.root, name, version) {
2394 Some(r) => json_response(200, &serde_json::json!({
2395 "name": r.name,
2396 "version": r.version,
2397 "head_op": r.head_op,
2398 "published_at": r.published_at,
2399 "function_names": r.function_names,
2400 "ops": r.ops,
2401 })),
2402 None => error_response(404, format!("package {name:?}@{version:?} not found")),
2403 }
2404}
2405
2406fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2408 let gzip = |bytes: Vec<u8>| {
2409 Response::from_data(bytes).with_status_code(200).with_header(
2410 tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"application/gzip"[..]).unwrap(),
2411 )
2412 };
2413
2414 if let Ok(bytes) = std::fs::read(pkg_archive_path(&state.root, name, version)) {
2416 return gzip(bytes);
2417 }
2418
2419 if let Some(head_op) = load_pkg_record(&state.root, name, version).and_then(|r| r.head_op) {
2423 match render_op_log_archive(state, name, version, &head_op) {
2424 Ok(bytes) => return gzip(bytes),
2425 Err(e) => {
2426 return error_response(500, format!("rendering archive for {name:?}@{version:?}: {e}"));
2427 }
2428 }
2429 }
2430
2431 error_response(404, format!("archive for {name:?}@{version:?} not found"))
2432}
2433
2434fn render_op_log_archive(
2440 state: &State,
2441 name: &str,
2442 version: &str,
2443 head_op: &str,
2444) -> Result<Vec<u8>, String> {
2445 let files: Vec<(String, String)> = {
2450 let store = state.store.lock().unwrap();
2451 let head = lex_store::render::package_head_at_op(&store, head_op)
2452 .map_err(|e| format!("reading head {head_op}: {e}"))?;
2453 match lex_store::render::render_source(&store, &head)
2454 .map_err(|e| format!("rendering source at {head_op}: {e}"))?
2455 {
2456 lex_store::render::RenderedSource::Single(src) => vec![("src/lib.lex".to_string(), src)],
2457 lex_store::render::RenderedSource::Multi(tree) => tree.into_iter().collect(),
2458 }
2459 };
2460
2461 let manifest = format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\n");
2462 let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2463 {
2464 let mut ar = tar::Builder::new(&mut enc);
2465 let mut append = |p: &str, data: &[u8]| -> std::io::Result<()> {
2466 let mut h = tar::Header::new_gnu();
2467 h.set_size(data.len() as u64);
2468 h.set_mode(0o644);
2469 h.set_cksum();
2470 ar.append_data(&mut h, p, data)
2471 };
2472 append("lex.toml", manifest.as_bytes()).map_err(|e| e.to_string())?;
2473 for (path, src) in &files {
2474 append(path, src.as_bytes()).map_err(|e| e.to_string())?;
2475 }
2476 ar.finish().map_err(|e| e.to_string())?;
2477 }
2478 enc.finish().map_err(|e| e.to_string())
2479}
2480
2481fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2483 match load_latest_pkg_record(&state.root, name) {
2484 Some(r) => json_response(200, &serde_json::json!({
2485 "name": r.name,
2486 "version": r.version,
2487 "head_op": r.head_op,
2488 })),
2489 None => error_response(404, format!("package {name:?} not found")),
2490 }
2491}
2492
2493fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2495 let record = match load_latest_pkg_record(&state.root, name) {
2496 Some(r) => r,
2497 None => return error_response(404, format!("package {name:?} not found")),
2498 };
2499
2500 let store = state.store.lock().unwrap();
2501 let branch = store.current_branch();
2502
2503 let head = match store.branch_head(&branch) {
2504 Ok(h) => h,
2505 Err(e) => return error_response(500, format!("branch_head: {e}")),
2506 };
2507
2508 let head_pairs: Vec<(String, String)> = head
2515 .iter()
2516 .map(|(sig, stage)| (sig.clone(), stage.clone()))
2517 .collect();
2518 let old_fns: BTreeMap<String, lex_ast::FnDecl> = store
2519 .get_asts_for_sigs_bulk(&head_pairs)
2520 .into_iter()
2521 .filter_map(|r| r.ok())
2522 .filter_map(|s| match s {
2523 lex_ast::Stage::FnDecl(fd)
2524 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
2525 _ => None,
2526 })
2527 .collect();
2528
2529 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2530 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2531 let empty_imports = lex_vcs::ImportMap::new();
2532
2533 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
2534 Ok(outcome) => {
2535 let ver = record.version.clone();
2537 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
2538 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
2539 if let Some(mut idx) = load_pkg_index(&state.root, name) {
2541 idx.versions.retain(|v| v.version != ver);
2542 idx.latest = idx.versions.last().map(|v| v.version.clone());
2543 if idx.versions.is_empty() {
2544 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
2545 } else {
2546 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
2547 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
2548 }
2549 }
2550 json_response(200, &serde_json::json!({
2551 "deleted": name,
2552 "version": ver,
2553 "ops": outcome.ops,
2554 "head_op": outcome.head_op,
2555 }))
2556 }
2557 Err(lex_store::StoreError::TypeError(errs)) => {
2558 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
2559 }
2560 Err(e) => write_error_response("retract package", e),
2561 }
2562}
2563
2564#[cfg(test)]
2565mod policy_ceiling_tests {
2566 use super::*;
2567 use lex_runtime::Policy;
2568 use std::path::PathBuf;
2569
2570 fn permissive_request() -> Policy {
2574 Policy {
2575 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
2576 .iter()
2577 .map(|s| s.to_string())
2578 .collect(),
2579 allow_fs_read: vec![PathBuf::from("/")],
2580 allow_fs_write: vec![PathBuf::from("/")],
2581 allow_net_host: Vec::new(),
2582 allow_proc: Vec::new(),
2583 allow_approval: Vec::new(),
2584 budget: None,
2585 }
2586 }
2587
2588 #[test]
2589 fn ceiling_drops_effects_the_caller_was_not_granted() {
2590 let ceiling = Policy {
2591 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
2592 ..Policy::default()
2593 };
2594 let got = clamp_policy(permissive_request(), &ceiling);
2595 assert!(got.allow_effects.contains("io"));
2596 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
2597 assert!(!got.allow_effects.contains("fs_write"));
2598 assert!(!got.allow_effects.contains("net"));
2599 assert!(!got.allow_effects.contains("time"));
2601 }
2602
2603 #[test]
2604 fn ceiling_scopes_override_caller_scopes() {
2605 let ceiling = Policy {
2606 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
2607 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
2608 ..Policy::default()
2609 };
2610 let got = clamp_policy(permissive_request(), &ceiling);
2611 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
2614 assert!(got.allow_fs_write.is_empty());
2615 assert!(got.allow_proc.is_empty());
2616 assert!(got.allow_net_host.is_empty());
2617 }
2618
2619 #[test]
2620 fn ceiling_caps_budget_and_prefers_the_smaller() {
2621 let mut req = permissive_request();
2623 req.budget = None;
2624 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
2625 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
2626
2627 let mut req2 = permissive_request();
2629 req2.budget = Some(50);
2630 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
2631 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
2632 }
2633
2634 #[test]
2635 fn empty_ceiling_is_pure_only() {
2636 let got = clamp_policy(permissive_request(), &Policy::default());
2637 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
2638 assert!(got.allow_proc.is_empty());
2639 assert!(got.allow_fs_write.is_empty());
2640 }
2641}
2642
2643#[cfg(test)]
2644mod public_read_tests {
2645 use super::*;
2646
2647 fn seed_pkg(root: &std::path::Path, name: &str, version: &str) {
2650 let record = PkgRecord {
2651 name: name.to_string(),
2652 version: version.to_string(),
2653 head_op: Some(format!("op-{name}")),
2654 published_at: 1,
2655 function_names: vec![format!("{name}.f")],
2656 ops: vec![],
2657 };
2658 save_pkg_record(root, &record, Some(format!("ARCHIVE:{name}@{version}").as_bytes()))
2659 .expect("seed package");
2660 }
2661
2662 #[test]
2663 fn new_package_defaults_to_private() {
2664 let tmp = tempfile::TempDir::new().unwrap();
2665 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2666 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2667 assert!(!pkg_is_public(tmp.path(), "does-not-exist"));
2669 }
2670
2671 #[test]
2672 fn set_visibility_round_trips_and_index_persists() {
2673 let tmp = tempfile::TempDir::new().unwrap();
2674 let state = State::open(tmp.path().to_path_buf()).unwrap();
2675 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2676
2677 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"public"}"#);
2678 assert!(pkg_is_public(tmp.path(), "lex-schema"));
2679 let idx = load_pkg_index(tmp.path(), "lex-schema").unwrap();
2681 assert_eq!(idx.latest.as_deref(), Some("0.9.2"));
2682 assert_eq!(idx.versions.len(), 1);
2683
2684 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"private"}"#);
2685 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2686 }
2687
2688 #[test]
2689 fn set_visibility_on_unknown_package_is_a_noop() {
2690 let tmp = tempfile::TempDir::new().unwrap();
2691 let state = State::open(tmp.path().to_path_buf()).unwrap();
2692 let _ = pkg_set_visibility_handler(&state, "ghost", r#"{"visibility":"public"}"#);
2694 assert!(load_pkg_index(tmp.path(), "ghost").is_none());
2695 }
2696
2697 #[test]
2698 fn public_listing_omits_private_packages() {
2699 let tmp = tempfile::TempDir::new().unwrap();
2700 let state = State::open(tmp.path().to_path_buf()).unwrap();
2701 seed_pkg(tmp.path(), "pub-pkg", "1.0.0");
2702 seed_pkg(tmp.path(), "priv-pkg", "1.0.0");
2703 let _ = pkg_set_visibility_handler(&state, "pub-pkg", r#"{"visibility":"public"}"#);
2704
2705 let names = public_pkg_names(tmp.path());
2706 assert_eq!(names, vec!["pub-pkg".to_string()]);
2707 }
2708
2709 #[test]
2710 fn resolve_public_maps_routes() {
2711 let get = Method::Get;
2712 assert_eq!(resolve_public(&get, "").unwrap(), PublicTarget::List);
2713 assert_eq!(resolve_public(&get, "/").unwrap(), PublicTarget::List);
2714 assert_eq!(
2715 resolve_public(&get, "/lex-schema").unwrap(),
2716 PublicTarget::Latest("lex-schema".into())
2717 );
2718 assert_eq!(
2719 resolve_public(&get, "/lex-schema/versions").unwrap(),
2720 PublicTarget::Versions("lex-schema".into())
2721 );
2722 assert_eq!(
2723 resolve_public(&get, "/lex-schema/head").unwrap(),
2724 PublicTarget::Head("lex-schema".into())
2725 );
2726 assert_eq!(
2727 resolve_public(&get, "/lex-schema/0.9.2").unwrap(),
2728 PublicTarget::Version("lex-schema".into(), "0.9.2".into())
2729 );
2730 assert_eq!(
2731 resolve_public(&get, "/lex-schema/0.9.2/archive").unwrap(),
2732 PublicTarget::Archive("lex-schema".into(), "0.9.2".into())
2733 );
2734 }
2735
2736 #[test]
2737 fn resolve_public_rejects_bad_method_and_traversal() {
2738 assert_eq!(resolve_public(&Method::Put, "/lex-schema"), Err(405));
2740 assert_eq!(resolve_public(&Method::Post, "").err(), Some(405));
2741 assert_eq!(resolve_public(&Method::Get, "/.."), Err(404));
2743 assert_eq!(resolve_public(&Method::Get, "/lex-schema/../etc"), Err(404));
2744 assert_eq!(resolve_public(&Method::Get, "/a/b/c/d"), Err(404));
2745 assert!(resolve_public(&Method::Get, "/lex schema").is_err());
2747 }
2748}