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
222pub const UNSATISFIABLE_PAIR_HINT: &str =
225 "republish from source to retire the stranded entry (#995)";
226
227pub(crate) fn unsatisfiable_pair_response(err: &lex_store::StoreError)
240 -> Option<Response<std::io::Cursor<Vec<u8>>>>
241{
242 let lex_store::StoreError::UnsatisfiablePair { sig_id, stage_id, filed_under } = err else {
243 return None;
244 };
245 Some(error_with_detail(422, "UnsatisfiablePair", serde_json::json!({
246 "sig_id": sig_id,
247 "stage_id": stage_id,
248 "filed_under": filed_under,
249 "message": err.to_string(),
250 "hint": UNSATISFIABLE_PAIR_HINT,
251 })))
252}
253
254fn write_error_response(prefix: &str, err: lex_store::StoreError)
260 -> Response<std::io::Cursor<Vec<u8>>>
261{
262 if let Some(resp) = unsatisfiable_pair_response(&err) {
263 return resp;
264 }
265 if let lex_store::StoreError::Contention { branch, attempts } = &err {
266 let body = serde_json::to_vec(&ErrorEnvelope {
267 error: format!("{prefix}: branch '{branch}' is contended (attempts={attempts})"),
268 detail: Some(serde_json::json!({
269 "kind": "contention",
270 "branch": branch,
271 "attempts": attempts,
272 })),
273 }).unwrap_or_else(|_| b"{}".to_vec());
274 return Response::from_data(body)
275 .with_status_code(503)
276 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
277 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"1"[..]).unwrap());
278 }
279 if let lex_store::StoreError::BudgetExceeded { session_id, cap, spent_after } = &err {
287 let body = serde_json::to_vec(&ErrorEnvelope {
288 error: format!(
289 "{prefix}: session `{session_id}` budget exceeded \
290 (spent_after={spent_after}, cap={cap})"
291 ),
292 detail: Some(serde_json::json!({
293 "kind": "budget_exceeded",
294 "session_id": session_id,
295 "cap": cap,
296 "spent_after": spent_after,
297 })),
298 }).unwrap_or_else(|_| b"{}".to_vec());
299 return Response::from_data(body)
300 .with_status_code(503)
301 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
302 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"0"[..]).unwrap());
303 }
304 error_response(500, format!("{prefix}: {err}"))
305}
306
307pub fn handle(state: Arc<State>, mut req: Request) -> std::io::Result<()> {
308 let method = req.method().clone();
309 let url = req.url().to_string();
310 let path = url.split('?').next().unwrap_or("").to_string();
311 let query = url.split_once('?').map(|(_, q)| q.to_string()).unwrap_or_default();
312
313 let x_lex_user = req.headers().iter()
318 .find(|h| h.field.equiv("x-lex-user"))
319 .map(|h| h.value.as_str().to_string());
320
321 if matches!(method, Method::Post) && path == "/v1/pkg/publish" {
323 let mut body_bytes: Vec<u8> = Vec::new();
324 let _ = req.as_reader().read_to_end(&mut body_bytes);
325 let resp = pkg_publish_handler(&state, &body_bytes);
326 return req.respond(resp);
327 }
328
329 let mut body = String::new();
330 let _ = req.as_reader().read_to_string(&mut body);
331
332 let resp = route(&state, &method, &path, &query, &body, x_lex_user.as_deref());
333 req.respond(resp)
334}
335
336pub fn handle_with_auth<F>(state: Arc<State>, req: Request, auth: F) -> std::io::Result<()>
340where
341 F: FnOnce(&str, &[Header]) -> bool,
342{
343 let path = req.url().split('?').next().unwrap_or("").to_string();
344 if !auth(&path, req.headers()) {
345 return req.respond(
346 Response::from_data(br#"{"error":"unauthorized"}"#.to_vec())
347 .with_status_code(401)
348 .with_header(
349 Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
350 ),
351 );
352 }
353 handle(state, req)
354}
355
356fn route(
357 state: &State,
358 method: &Method,
359 path: &str,
360 query: &str,
361 body: &str,
362 x_lex_user: Option<&str>,
363) -> Response<std::io::Cursor<Vec<u8>>> {
364 match (method, path) {
365 (Method::Get, "/") => crate::web::activity_handler(state),
367 (Method::Get, "/web/branches") => crate::web::branches_handler(state),
368 (Method::Get, "/web/trust") => crate::web::trust_handler(state),
369 (Method::Get, "/web/attention") => crate::web::attention_handler(state),
370 (Method::Get, p) if p.starts_with("/web/branch/") => {
371 let name = &p["/web/branch/".len()..];
372 crate::web::branch_handler(state, name)
373 }
374 (Method::Get, p) if p.starts_with("/web/stage/") => {
375 let id = &p["/web/stage/".len()..];
376 crate::web::stage_html_handler(state, id)
377 }
378 (Method::Post, p) if p.starts_with("/web/stage/") && (
383 p.ends_with("/pin") || p.ends_with("/defer")
384 || p.ends_with("/block") || p.ends_with("/unblock")
385 ) => {
386 let prefix_len = "/web/stage/".len();
387 let last_slash = p.rfind('/').unwrap_or(p.len());
388 let id = &p[prefix_len..last_slash];
389 let verb = &p[last_slash + 1..];
390 let decision = match verb {
391 "pin" => crate::web::WebStageDecision::Pin,
392 "defer" => crate::web::WebStageDecision::Defer,
393 "block" => crate::web::WebStageDecision::Block,
394 "unblock" => crate::web::WebStageDecision::Unblock,
395 _ => unreachable!("matched in outer guard"),
396 };
397 crate::web::stage_decision_handler(state, id, body, decision, x_lex_user)
398 }
399 (Method::Get, "/v1/health") => json_response(200, &serde_json::json!({"ok": true})),
401 (Method::Post, "/v1/parse") => parse_handler(body),
402 (Method::Post, "/v1/check") => check_handler(body),
403 (Method::Post, "/v1/publish") => publish_handler(state, body),
404 (Method::Post, "/v1/patch") => patch_handler(state, body),
405 (Method::Get, p) if p.starts_with("/v1/stage/") => {
406 let suffix = &p["/v1/stage/".len()..];
407 if let Some(id) = suffix.strip_suffix("/attestations") {
410 stage_attestations_handler(state, id)
411 } else {
412 stage_handler(state, suffix)
413 }
414 }
415 (Method::Post, "/v1/run") => run_handler(state, body, false),
416 (Method::Post, "/v1/replay") => run_handler(state, body, true),
417 (Method::Get, p) if p.starts_with("/v1/trace/") => {
418 let id = &p["/v1/trace/".len()..];
419 trace_handler(state, id)
420 }
421 (Method::Get, "/v1/diff") => diff_handler(state, query),
422 (Method::Post, "/v1/merge/start") => merge_start_handler(state, body),
423 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/resolve") => {
424 let id = &p["/v1/merge/".len()..p.len() - "/resolve".len()];
425 merge_resolve_handler(state, id, body)
426 }
427 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/commit") => {
428 let id = &p["/v1/merge/".len()..p.len() - "/commit".len()];
429 merge_commit_handler(state, id)
430 }
431 (Method::Post, "/v1/ops/batch") => ops_batch_handler(state, body),
433 (Method::Post, "/v1/attestations/batch") => attestations_batch_handler(state, body),
434 (Method::Post, "/v1/stages/batch") => crate::sync_http::stages_batch_handler(state, body),
437 (Method::Post, "/v1/stages/fetch") => crate::sync_http::stages_fetch_handler(state, body),
438 (Method::Post, "/v1/stages/missing") => crate::sync_http::stages_missing_handler(state, body),
439 (Method::Post, "/v1/intents/batch") => crate::sync_http::intents_batch_handler(state, body),
440 (Method::Post, "/v1/intents/fetch") => crate::sync_http::intents_fetch_handler(state, body),
441 (Method::Post, "/v1/locks/batch") => crate::sync_http::locks_batch_handler(state, body),
444 (Method::Post, "/v1/locks/fetch") => crate::sync_http::locks_fetch_handler(state, body),
445 (Method::Post, "/v1/issues/batch") => crate::sync_http::issues_batch_handler(state, body),
448 (Method::Post, "/v1/issues/fetch") => crate::sync_http::issues_fetch_handler(state, body),
449 (Method::Get, "/v1/issues/list") => crate::sync_http::issues_list_handler(state),
450 (Method::Get, "/v1/issues") => crate::issues_http::issues_state_handler(state),
454 (Method::Get, "/v1/projects") => crate::issues_http::projects_handler(state),
455 (Method::Get, p) if p.starts_with("/v1/issues/") => {
456 crate::issues_http::issue_detail_handler(state, &p["/v1/issues/".len()..])
457 }
458 (Method::Get, "/v1/review/inbox") => crate::review_http::review_inbox_handler(state, query),
462 (Method::Post, "/v1/review/verdict") => crate::review_http::review_verdict_handler(state, body),
463 (Method::Get, "/v1/branches") => crate::branches_http::branches_list_handler(state),
464 (Method::Post, "/v1/branches") => crate::branches_http::branch_create_handler(state, body),
465 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/checkout") => {
466 let name = &p["/v1/branches/".len()..p.len() - "/checkout".len()];
467 crate::branches_http::branch_checkout_handler(state, name)
468 }
469 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
472 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
473 crate::branches_http::branch_head_handler(state, name)
474 }
475 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
476 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
477 crate::branches_http::branch_advance_head_handler(state, name, body)
478 }
479 (Method::Get, "/v1/ops/since") => ops_since_handler(state, query),
483 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
484 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
488 (Method::Put, p) if p.starts_with("/v1/pkg/") && p.ends_with("/visibility") => {
492 let name = &p["/v1/pkg/".len()..p.len() - "/visibility".len()];
493 pkg_set_visibility_handler(state, name, body)
494 }
495 (Method::Post, p) if p.starts_with("/v1/pkg/") && p.ends_with("/release") => {
498 let name = &p["/v1/pkg/".len()..p.len() - "/release".len()];
499 pkg_release_handler(state, name, body)
500 }
501 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
502 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
503 pkg_head_handler(state, name)
504 }
505 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/versions") => {
506 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
507 pkg_versions_handler(state, name)
508 }
509 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/api-diff") => {
510 let name = &p["/v1/pkg/".len()..p.len() - "/api-diff".len()];
511 pkg_api_diff_handler(state, name, query)
512 }
513 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
515 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
516 if let Some((name, version)) = inner.split_once('/') {
518 pkg_archive_handler(state, name, version)
519 } else {
520 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
521 }
522 }
523 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
525 let inner = &p["/v1/pkg/".len()..];
526 if let Some((name, version)) = inner.split_once('/') {
527 pkg_get_version_handler(state, name, version)
528 } else {
529 error_response(400, "expected /v1/pkg/{name}/{version}")
530 }
531 }
532 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
533 let name = &p["/v1/pkg/".len()..];
534 pkg_get_handler(state, name)
535 }
536 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
537 let name = &p["/v1/pkg/".len()..];
538 pkg_delete_handler(state, name)
539 }
540 _ => error_response(404, format!("unknown route: {method:?} {path}")),
541 }
542}
543
544#[derive(Deserialize)]
545struct ParseReq { source: String }
546
547fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
548 let req: ParseReq = match serde_json::from_str(body) {
549 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
550 };
551 match load_program_from_str(&req.source) {
552 Ok(prog) => {
553 let stages = canonicalize_program(&prog);
554 json_response(200, &serde_json::to_value(&stages).unwrap())
555 }
556 Err(e) => error_response(400, format!("syntax error: {e}")),
557 }
558}
559
560pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
561 let req: ParseReq = match serde_json::from_str(body) {
562 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
563 };
564 let prog = match load_program_from_str(&req.source) {
565 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
566 };
567 let stages = canonicalize_program(&prog);
568 match lex_types::check_program(&stages) {
569 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
570 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
571 }
572}
573
574#[derive(Deserialize)]
575struct PublishReq { source: String, #[serde(default)] activate: bool }
576
577pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
578 let req: PublishReq = match serde_json::from_str(body) {
579 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
580 };
581 let prog = match load_program_from_str(&req.source) {
582 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
583 };
584 let mut stages = canonicalize_program(&prog);
588 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
589 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
590 }
591 let example_errors = lex_runtime::evaluate_examples(&stages);
597 if !example_errors.is_empty() {
598 return error_with_detail(422, "example mismatch",
599 serde_json::to_value(&example_errors).unwrap_or_default());
600 }
601
602 let store = state.store.lock().unwrap();
603 let branch = store.current_branch();
604
605 let old_head = match store.branch_head(&branch) {
607 Ok(h) => h,
608 Err(e) => return error_response(500, format!("branch_head: {e}")),
609 };
610 let old_head_stages: Vec<lex_ast::Stage> =
612 old_head.values().filter_map(|stg| store.get_ast(stg).ok()).collect();
613 let old_fns = stage_fns(&old_head_stages);
614 let new_fns = stage_fns(&stages);
615 let old_types = stage_types(&old_head_stages);
616 let new_types = stage_types(&stages);
617 let report =
618 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
619
620 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
622 {
623 let entry = new_imports.entry("<source>".into()).or_default();
624 for s in &stages {
625 if let lex_ast::Stage::Import(im) = s {
626 entry.insert(lex_vcs::ImportRef {
627 reference: im.reference.clone(),
628 alias: im.alias.clone(),
629 });
630 }
631 }
632 }
633
634 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
635 Ok(outcome) => {
636 record_examples_for_publish(&store, &stages, &outcome);
640 json_response(200, &serde_json::json!({
641 "ops": outcome.ops,
642 "head_op": outcome.head_op,
643 }))
644 }
645 Err(lex_store::StoreError::TypeError(errs)) => {
653 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
654 }
655 Err(e) => write_error_response("publish_program", e),
656 }
657}
658
659#[derive(Deserialize)]
660struct PatchReq {
661 stage_id: String,
662 patch: lex_ast::Patch,
663 #[serde(default)] activate: bool,
664}
665
666fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
669 let req: PatchReq = match serde_json::from_str(body) {
670 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
671 };
672 let store = state.store.lock().unwrap();
673
674 let original = match store.get_ast(&req.stage_id) {
676 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
677 };
678
679 let patched = match lex_ast::apply_patch(&original, &req.patch) {
681 Ok(s) => s,
682 Err(e) => return error_with_detail(422, "patch failed",
683 serde_json::to_value(&e).unwrap_or_default()),
684 };
685
686 let branch = store.current_branch();
697
698 let sig = match lex_ast::sig_id(&patched) {
700 Some(s) => s,
701 None => return error_response(500, "patched stage has no sig_id"),
702 };
703
704 let new_id = match store.publish(&patched) {
707 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
708 };
709
710 let original_effects: std::collections::BTreeSet<String> = match &original {
712 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
713 _ => std::collections::BTreeSet::new(),
714 };
715 let patched_effects: std::collections::BTreeSet<String> = match &patched {
716 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
717 _ => std::collections::BTreeSet::new(),
718 };
719 let head_now = match store.get_branch(&branch) {
720 Ok(b) => b.and_then(|b| b.head_op),
721 Err(e) => return error_response(500, format!("get_branch: {e}")),
722 };
723 let kind = if original_effects != patched_effects {
724 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
731 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
732 let to_sig_id = lex_ast::sig_id(&patched).filter(|s| *s != sig);
736 lex_vcs::OperationKind::ChangeEffectSig {
737 sig_id: sig.clone(),
738 from_stage_id: req.stage_id.clone(),
739 to_stage_id: new_id.clone(),
740 from_effects: original_effects,
741 to_effects: patched_effects,
742 from_budget,
743 to_budget,
744 to_sig_id,
745 }
746 } else {
747 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
748 lex_vcs::OperationKind::ModifyBody {
749 sig_id: sig.clone(),
750 from_stage_id: req.stage_id.clone(),
751 to_stage_id: new_id.clone(),
752 from_budget: budget,
753 to_budget: budget,
754 to_sig_id: lex_ast::sig_id(&patched).filter(|s| *s != sig),
757 }
758 };
759 let transition = lex_store::transition_for_kind(&kind);
763 let op = lex_vcs::Operation::new(
764 kind,
765 head_now.into_iter().collect::<Vec<_>>(),
766 );
767 let op_id = match store.apply_operation_gated(&branch, op, transition) {
768 Ok(id) => id,
769 Err(lex_store::StoreError::TypeError(errs)) => return error_with_detail(
770 422, "type errors after patch", serde_json::to_value(&errs).unwrap_or_default()),
771 Err(e) => return write_error_response("apply_operation_gated", e),
772 };
773 if req.activate {
774 if let Err(e) = store.activate(&new_id) {
775 return error_response(500, format!("activate: {e}"));
776 }
777 }
778
779 let status = format!("{:?}",
780 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
781 json_response(200, &serde_json::json!({
782 "old_stage_id": req.stage_id,
783 "new_stage_id": new_id,
784 "sig_id": sig,
785 "status": status,
786 "op_id": op_id,
787 }))
788}
789
790pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
791 let store = state.store.lock().unwrap();
792 let meta = match store.get_metadata(id) {
793 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
794 };
795 let ast = match store.get_ast(id) {
796 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
797 };
798 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
799 json_response(200, &serde_json::json!({
800 "metadata": meta,
801 "ast": ast,
802 "status": status,
803 }))
804}
805
806pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
815 let store = state.store.lock().unwrap();
816 if let Err(e) = store.get_metadata(id) {
817 return error_response(404, format!("{e}"));
818 }
819 let log = match store.attestation_log() {
820 Ok(l) => l,
821 Err(e) => return error_response(500, format!("attestation log: {e}")),
822 };
823 let mut listing = match log.list_for_stage(&id.to_string()) {
824 Ok(v) => v,
825 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
826 };
827 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
828 json_response(200, &serde_json::json!({"attestations": listing}))
829}
830
831#[derive(Deserialize, Default)]
832struct PolicyJson {
833 #[serde(default)] allow_effects: Vec<String>,
834 #[serde(default)] allow_fs_read: Vec<String>,
835 #[serde(default)] allow_fs_write: Vec<String>,
836 #[serde(default)] budget: Option<u64>,
837}
838
839impl PolicyJson {
840 fn into_policy(self) -> Policy {
841 Policy {
842 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
843 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
844 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
845 allow_net_host: Vec::new(),
846 allow_proc: Vec::new(),
847 allow_approval: Vec::new(),
848 budget: self.budget,
849 }
850 }
851}
852
853#[derive(Deserialize)]
854struct RunReq {
855 source: String,
856 #[serde(rename = "fn")] func: String,
857 #[serde(default)] args: Vec<serde_json::Value>,
858 #[serde(default)] policy: PolicyJson,
859 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
860}
861
862pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
863 let req: RunReq = match serde_json::from_str(body) {
864 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
865 };
866 let prog = match load_program_from_str(&req.source) {
867 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
868 };
869 let stages = canonicalize_program(&prog);
870 if let Err(errs) = lex_types::check_program(&stages) {
871 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
872 }
873 let bc = compile_program(&stages);
874 let mut policy = req.policy.into_policy();
875 if let Some(ceiling) = &state.policy_ceiling {
881 policy = clamp_policy(policy, ceiling);
882 }
883 if let Err(violations) = check_policy(&bc, &policy) {
884 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
885 }
886
887 let mut recorder = lex_trace::Recorder::new();
888 if with_overrides && !req.overrides.is_empty() {
889 recorder = recorder.with_overrides(req.overrides);
890 }
891 let handle = recorder.handle();
892 let handler = DefaultHandler::new(policy);
893 let mut vm = Vm::with_handler(&bc, Box::new(handler));
894 vm.set_tracer(Box::new(recorder));
895
896 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
897 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
898 let result = vm.call(&req.func, vargs);
899 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
900
901 let store = state.store.lock().unwrap();
902 let (root_out, root_err, status) = match &result {
903 Ok(v) => (Some(value_to_json(v)), None, 200u16),
904 Err(e) => (None, Some(format!("{e}")), 200u16),
905 };
906 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
907 root_out.clone(), root_err.clone(), started, ended);
908 let run_id = match store.save_trace(&tree) {
909 Ok(id) => id,
910 Err(e) => return error_response(500, format!("save_trace: {e}")),
911 };
912
913 let mut body = serde_json::json!({
914 "run_id": run_id,
915 "output": root_out,
916 });
917 if let Some(err) = root_err {
918 body["error"] = serde_json::Value::String(err);
919 }
920 json_response(status, &body)
921}
922
923fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
924 let store = state.store.lock().unwrap();
925 match store.load_trace(id) {
926 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
927 Err(e) => error_response(404, format!("{e}")),
928 }
929}
930
931fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
932 let mut a = None;
933 let mut b = None;
934 for kv in query.split('&') {
935 if let Some((k, v)) = kv.split_once('=') {
936 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
937 }
938 }
939 let (Some(a), Some(b)) = (a, b) else {
940 return error_response(400, "missing a or b query params");
941 };
942 let store = state.store.lock().unwrap();
943 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
944 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
945 match lex_trace::diff_runs(&ta, &tb) {
946 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
947 None => json_response(200, &serde_json::json!({"divergence": null})),
948 }
949}
950
951fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
952
953fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
954
955#[derive(Deserialize)]
956struct MergeStartReq {
957 src_branch: String,
958 dst_branch: String,
959}
960
961fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
972 let req: MergeStartReq = match serde_json::from_str(body) {
973 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
974 };
975 let store = state.store.lock().unwrap();
976 let src_head = match store.get_branch(&req.src_branch) {
977 Ok(Some(b)) => b.head_op,
978 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
979 Err(e) => return error_response(500, format!("src branch read: {e}")),
980 };
981 let dst_head = match store.get_branch(&req.dst_branch) {
982 Ok(Some(b)) => b.head_op,
983 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
984 Err(e) => return error_response(500, format!("dst branch read: {e}")),
985 };
986 let log = match lex_vcs::OpLog::open(store.root()) {
987 Ok(l) => l,
988 Err(e) => return error_response(500, format!("op log: {e}")),
989 };
990 let merge_id = mint_merge_id();
994 let session = match MergeSession::start(
995 merge_id.clone(),
996 &log,
997 src_head.as_ref(),
998 dst_head.as_ref(),
999 ) {
1000 Ok(s) => s,
1001 Err(e) => return error_response(500, format!("merge start: {e}")),
1002 };
1003 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
1004 let auto_resolved_count = session.auto_resolved.len();
1005 let body = serde_json::json!({
1006 "merge_id": merge_id,
1007 "src_head": session.src_head,
1008 "dst_head": session.dst_head,
1009 "lca": session.lca,
1010 "conflicts": conflicts,
1011 "auto_resolved_count": auto_resolved_count,
1012 });
1013 drop(conflicts);
1014 drop(store);
1015 let wrapped = ApiMergeSession {
1016 inner: session,
1017 src_branch: req.src_branch,
1018 dst_branch: req.dst_branch,
1019 };
1020 state.sessions.lock().unwrap().insert(merge_id, wrapped);
1021 json_response(200, &body)
1022}
1023
1024#[derive(Deserialize)]
1025struct MergeResolveReq {
1026 resolutions: Vec<MergeResolveEntry>,
1031}
1032
1033#[derive(Deserialize)]
1034struct MergeResolveEntry {
1035 conflict_id: String,
1036 resolution: lex_vcs::Resolution,
1037}
1038
1039fn merge_resolve_handler(
1050 state: &State,
1051 merge_id: &str,
1052 body: &str,
1053) -> Response<std::io::Cursor<Vec<u8>>> {
1054 let req: MergeResolveReq = match serde_json::from_str(body) {
1055 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
1056 };
1057 let mut sessions = state.sessions.lock().unwrap();
1058 let Some(wrapped) = sessions.get_mut(merge_id) else {
1059 return error_response(404, format!("unknown merge_id `{merge_id}`"));
1060 };
1061 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
1062 .map(|e| (e.conflict_id, e.resolution))
1063 .collect();
1064 let store = state.store.lock().unwrap();
1069 let checker = lex_store::MergeResolutionChecker::new(&store, wrapped.dst_branch.clone());
1070 let verdicts = wrapped.inner.resolve_checked(pairs, &checker);
1071 drop(store);
1072 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
1073 let body = serde_json::json!({
1074 "verdicts": verdicts,
1075 "remaining_conflicts": remaining,
1076 });
1077 json_response(200, &body)
1078}
1079
1080fn merge_commit_handler(
1102 state: &State,
1103 merge_id: &str,
1104) -> Response<std::io::Cursor<Vec<u8>>> {
1105 use std::collections::BTreeMap;
1106 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
1107 Some(w) => w,
1108 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
1109 };
1110 let dst_branch = wrapped.dst_branch.clone();
1111 let src_head = wrapped.inner.src_head.clone();
1112 let dst_head = wrapped.inner.dst_head.clone();
1113 let auto_resolved = wrapped.inner.auto_resolved.clone();
1114
1115 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
1118
1119 for outcome in &auto_resolved {
1121 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
1122 entries.insert(sig_id.clone(), stage_id.clone());
1123 }
1124 }
1125
1126 let resolved = match wrapped.inner.commit() {
1128 Ok(r) => r,
1129 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
1130 return error_with_detail(
1134 422,
1135 "conflicts remaining",
1136 serde_json::json!({"unresolved": ids}),
1137 );
1138 }
1139 };
1140
1141 for (conflict_id, resolution) in resolved {
1142 match resolution {
1143 lex_vcs::Resolution::TakeOurs => {
1144 }
1146 lex_vcs::Resolution::TakeTheirs => {
1147 match resolve_take_theirs(state, &src_head, &conflict_id) {
1157 Ok(stage_id) => {
1158 entries.insert(conflict_id.clone(), stage_id);
1159 }
1160 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1161 }
1162 }
1163 lex_vcs::Resolution::Custom { op } => {
1164 match op.kind.merge_target() {
1173 Some((sig, stage)) => {
1174 if sig != conflict_id {
1175 return error_with_detail(
1176 422,
1177 "custom op targets a different sig than the conflict",
1178 serde_json::json!({
1179 "conflict_id": conflict_id,
1180 "op_targets": sig,
1181 }),
1182 );
1183 }
1184 entries.insert(conflict_id, stage);
1185 }
1186 None => {
1187 return error_with_detail(
1188 422,
1189 "custom op kind doesn't yield a single sig→stage delta",
1190 serde_json::json!({
1191 "conflict_id": conflict_id,
1192 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1193 }),
1194 );
1195 }
1196 }
1197 }
1198 lex_vcs::Resolution::Defer => {
1199 return error_response(500, "internal: Defer slipped past commit gate");
1201 }
1202 }
1203 }
1204
1205 let resolved_count = entries.len();
1206 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1207 if let Some(d) = dst_head { parents.push(d); }
1208 if let Some(s) = src_head { parents.push(s); }
1209 let op = lex_vcs::Operation::new(
1210 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1211 parents,
1212 );
1213 let transition = lex_vcs::StageTransition::Merge { entries };
1214 let store = state.store.lock().unwrap();
1215 match store.apply_merge_op_gated(&dst_branch, op, transition) {
1218 Ok(new_head_op) => json_response(200, &serde_json::json!({
1219 "new_head_op": new_head_op,
1220 "dst_branch": dst_branch,
1221 })),
1222 Err(lex_store::StoreError::TypeError(errs)) => error_with_detail(
1223 422, "merged program has type errors", serde_json::to_value(&errs).unwrap_or_default()),
1224 Err(e @ lex_store::StoreError::DependencyConflict { .. }) => {
1230 let detail = match &e {
1231 lex_store::StoreError::DependencyConflict { package, dst_version, src_version } => {
1232 serde_json::json!({
1233 "kind": "dependency_conflict",
1234 "package": package,
1235 "dst_branch": dst_branch,
1236 "dst_version": dst_version,
1237 "src_branch": wrapped.src_branch,
1238 "src_version": src_version,
1239 })
1240 }
1241 _ => serde_json::Value::Null,
1242 };
1243 error_with_detail(409, e.to_string(), detail)
1244 }
1245 Err(e) => write_error_response("apply merge op", e),
1246 }
1247}
1248
1249fn resolve_take_theirs(
1254 state: &State,
1255 src_head: &Option<lex_vcs::OpId>,
1256 sig: &lex_vcs::SigId,
1257) -> std::io::Result<Option<lex_vcs::StageId>> {
1258 let store = state.store.lock().unwrap();
1259 let log = lex_vcs::OpLog::open(store.root())?;
1260 let Some(head) = src_head.as_ref() else { return Ok(None); };
1261 let mut current: Option<lex_vcs::StageId> = None;
1264 for record in log.walk_forward(head, None)? {
1265 match &record.produces {
1266 lex_vcs::StageTransition::Create { sig_id, stage_id }
1267 if sig_id == sig => { current = Some(stage_id.clone()); }
1268 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1269 if sig_id == sig => { current = Some(to.clone()); }
1270 lex_vcs::StageTransition::Remove { sig_id, .. }
1271 if sig_id == sig => { current = None; }
1272 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1273 if from == sig || to == sig => {
1274 if from == sig { current = None; }
1275 if to == sig { current = Some(body_stage_id.clone()); }
1276 }
1277 lex_vcs::StageTransition::Merge { entries } => {
1278 if let Some(opt) = entries.get(sig) {
1279 current = opt.clone();
1280 }
1281 }
1282 _ => {}
1283 }
1284 }
1285 Ok(current)
1286}
1287
1288fn mint_merge_id() -> MergeSessionId {
1289 use std::sync::atomic::{AtomicU64, Ordering};
1290 static COUNTER: AtomicU64 = AtomicU64::new(0);
1291 let nanos = SystemTime::now()
1292 .duration_since(UNIX_EPOCH)
1293 .map(|d| d.as_nanos())
1294 .unwrap_or(0);
1295 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1296 format!("merge_{nanos:x}_{n:x}")
1297}
1298
1299pub(crate) fn ops_batch_handler(state: &State, body: &str)
1330 -> Response<std::io::Cursor<Vec<u8>>>
1331{
1332 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1333 Ok(r) => r,
1334 Err(e) => return error_response(400,
1335 format!("body must be a JSON array of OperationRecord: {e}")),
1336 };
1337 let store = state.store.lock().unwrap();
1338 let log = match lex_vcs::OpLog::open(store.root()) {
1339 Ok(l) => l,
1340 Err(e) => return error_response(500, format!("opening op log: {e}")),
1341 };
1342
1343 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1351 std::collections::BTreeSet::new();
1352 for rec in &records {
1353 let expected = rec.op.op_id();
1354 if expected != rec.op_id {
1355 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1356 "supplied": rec.op_id,
1357 "expected": expected,
1358 }));
1359 }
1360 for parent in &rec.op.parents {
1361 let known = match log.get(parent) {
1362 Ok(Some(_)) => true,
1363 Ok(None) => false,
1364 Err(e) => return error_response(500, format!("op log read: {e}")),
1365 };
1366 if !known && !batch_ids.contains(parent) {
1367 return error_with_detail(422, "MissingParent", serde_json::json!({
1368 "op_id": rec.op_id,
1369 "missing_parent": parent,
1370 }));
1371 }
1372 }
1373 batch_ids.insert(rec.op_id.clone());
1374 }
1375
1376 let mut added = 0usize;
1379 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1380 for rec in &records {
1381 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1382 match log.put(rec) {
1383 Ok(()) => {
1384 if !already_present {
1385 added += 1;
1386 added_ids.push(&rec.op_id);
1387 }
1388 }
1389 Err(e) => return error_response(500, format!("op log write: {e}")),
1390 }
1391 }
1392
1393 json_response(200, &serde_json::json!({
1394 "received": records.len(),
1395 "added": added,
1396 "skipped": records.len() - added,
1397 "added_ids": added_ids,
1398 }))
1399}
1400
1401pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1423 -> Response<std::io::Cursor<Vec<u8>>>
1424{
1425 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1426 Ok(a) => a,
1427 Err(e) => return error_response(400,
1428 format!("body must be a JSON array of Attestation: {e}")),
1429 };
1430 let store = state.store.lock().unwrap();
1431 let log = match store.attestation_log() {
1432 Ok(l) => l,
1433 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1434 };
1435 let op_log = match lex_vcs::OpLog::open(store.root()) {
1436 Ok(l) => l,
1437 Err(e) => return error_response(500, format!("opening op log: {e}")),
1438 };
1439
1440 for att in &attestations {
1442 let expected = lex_vcs::Attestation::with_timestamp(
1445 att.stage_id.clone(),
1446 att.op_id.clone(),
1447 att.intent_id.clone(),
1448 att.kind.clone(),
1449 att.result.clone(),
1450 att.produced_by.clone(),
1451 att.cost.clone(),
1452 att.timestamp,
1453 ).attestation_id;
1454 if expected != att.attestation_id {
1455 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1456 "supplied": att.attestation_id,
1457 "expected": expected,
1458 }));
1459 }
1460 if let Some(op_id) = &att.op_id {
1464 match op_log.get(op_id) {
1465 Ok(Some(_)) => {}
1466 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1467 "attestation_id": att.attestation_id,
1468 "op_id": op_id,
1469 })),
1470 Err(e) => return error_response(500, format!("op log read: {e}")),
1471 }
1472 }
1473 }
1474
1475 let mut added = 0usize;
1479 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1480 for att in &attestations {
1481 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1482 match log.put(att) {
1483 Ok(()) => {
1484 if !already_present {
1485 added += 1;
1486 added_ids.push(&att.attestation_id);
1487 }
1488 }
1489 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1490 }
1491 }
1492
1493 json_response(200, &serde_json::json!({
1494 "received": attestations.len(),
1495 "added": added,
1496 "skipped": attestations.len() - added,
1497 "added_ids": added_ids,
1498 }))
1499}
1500
1501pub(crate) fn ops_since_handler(state: &State, query: &str)
1533 -> Response<std::io::Cursor<Vec<u8>>>
1534{
1535 let mut after: Option<String> = None;
1536 let mut branch = String::from("main");
1537 let mut limit: Option<usize> = None;
1538 for kv in query.split('&') {
1539 let Some((k, v)) = kv.split_once('=') else { continue };
1540 match k {
1541 "after" => after = Some(v.to_string()),
1542 "branch" => branch = v.to_string(),
1543 "limit" => {
1544 limit = Some(match v.parse::<usize>() {
1545 Ok(n) => n,
1546 Err(_) => return error_response(400,
1547 format!("limit must be a positive integer, got `{v}`")),
1548 });
1549 }
1550 _ => {}
1551 }
1552 }
1553
1554 let store = state.store.lock().unwrap();
1555 let log = match lex_vcs::OpLog::open(store.root()) {
1556 Ok(l) => l,
1557 Err(e) => return error_response(500, format!("opening op log: {e}")),
1558 };
1559 let head = match store.get_branch(&branch) {
1560 Ok(Some(b)) => b.head_op,
1561 Ok(None) => None,
1562 Err(e) => return error_response(500, format!("get_branch: {e}")),
1563 };
1564 let Some(head) = head else {
1565 return json_response(200, &serde_json::json!([]));
1566 };
1567
1568 let ops_since = match log.ops_since(&head, after.as_ref()) {
1569 Ok(o) => o,
1570 Err(e) => return error_response(500, format!("ops_since: {e}")),
1571 };
1572 let mut ops = ops_since;
1576 ops.reverse();
1577 if let Some(n) = limit {
1578 ops.truncate(n);
1579 }
1580
1581 json_response(200, &serde_json::to_value(&ops).unwrap_or_default())
1582}
1583
1584pub(crate) fn attestations_since_handler(state: &State, query: &str)
1597 -> Response<std::io::Cursor<Vec<u8>>>
1598{
1599 let mut after_op: Option<String> = None;
1600 let mut limit: Option<usize> = None;
1601 for kv in query.split('&') {
1602 let Some((k, v)) = kv.split_once('=') else { continue };
1603 match k {
1604 "after-op" => after_op = Some(v.to_string()),
1605 "limit" => {
1606 limit = Some(match v.parse::<usize>() {
1607 Ok(n) => n,
1608 Err(_) => return error_response(400,
1609 format!("limit must be a positive integer, got `{v}`")),
1610 });
1611 }
1612 _ => {}
1613 }
1614 }
1615
1616 let store = state.store.lock().unwrap();
1617 let log = match store.attestation_log() {
1618 Ok(l) => l,
1619 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1620 };
1621
1622 let exclude: std::collections::BTreeSet<String> = match &after_op {
1626 None => std::collections::BTreeSet::new(),
1627 Some(cutoff) => {
1628 let op_log = match lex_vcs::OpLog::open(store.root()) {
1629 Ok(l) => l,
1630 Err(e) => return error_response(500, format!("opening op log: {e}")),
1631 };
1632 match op_log.walk_back(cutoff, None) {
1633 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1634 Err(_) => {
1635 std::collections::BTreeSet::new()
1639 }
1640 }
1641 }
1642 };
1643
1644 let all = match log.list_all() {
1645 Ok(v) => v,
1646 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1647 };
1648 let mut filtered: Vec<lex_vcs::Attestation> = all
1649 .into_iter()
1650 .filter(|a| match &a.op_id {
1651 Some(op_id) => !exclude.contains(op_id),
1652 None => true,
1656 })
1657 .collect();
1658 filtered.sort_by(|a, b| {
1662 a.timestamp.cmp(&b.timestamp)
1663 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1664 });
1665 if let Some(n) = limit {
1666 filtered.truncate(n);
1667 }
1668
1669 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1670}
1671
1672#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
1681struct DepSpec {
1682 #[serde(default, skip_serializing_if = "Option::is_none")]
1683 registry: Option<String>,
1684 #[serde(default, skip_serializing_if = "Option::is_none")]
1685 version: Option<String>,
1686 #[serde(default, skip_serializing_if = "Option::is_none")]
1687 git: Option<String>,
1688 #[serde(default, skip_serializing_if = "Option::is_none")]
1689 branch: Option<String>,
1690 #[serde(default, skip_serializing_if = "Option::is_none")]
1691 tag: Option<String>,
1692 #[serde(default, skip_serializing_if = "Option::is_none")]
1693 rev: Option<String>,
1694 #[serde(default, skip_serializing_if = "Option::is_none")]
1695 path: Option<String>,
1696}
1697
1698impl DepSpec {
1699 fn to_toml_inline(&self) -> Option<String> {
1704 let mut parts: Vec<String> = Vec::new();
1705 let mut push = |k: &str, v: &Option<String>| {
1706 if let Some(val) = v {
1707 parts.push(format!("{k} = {}", toml_str(val)));
1708 }
1709 };
1710 push("registry", &self.registry);
1711 push("version", &self.version);
1712 push("git", &self.git);
1713 push("branch", &self.branch);
1714 push("tag", &self.tag);
1715 push("rev", &self.rev);
1716 push("path", &self.path);
1717 if parts.is_empty() {
1718 None
1719 } else {
1720 Some(format!("{{ {} }}", parts.join(", ")))
1721 }
1722 }
1723}
1724
1725fn toml_str(s: &str) -> String {
1727 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
1728}
1729
1730#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1732struct PkgRecord {
1733 name: String,
1734 version: String,
1735 head_op: Option<String>,
1736 published_at: u64,
1737 function_names: Vec<String>,
1739 #[serde(default)]
1744 dependencies: Vec<String>,
1745 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
1751 dependency_specs: std::collections::BTreeMap<String, DepSpec>,
1752 ops: Vec<serde_json::Value>,
1754}
1755
1756#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
1763#[serde(rename_all = "lowercase")]
1764pub enum Visibility {
1765 #[default]
1766 Private,
1767 Public,
1768}
1769
1770#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1775struct PkgIndex {
1776 latest: Option<String>,
1778 versions: Vec<PkgVersionSummary>,
1780 #[serde(default)]
1784 visibility: Visibility,
1785}
1786
1787#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1788struct PkgVersionSummary {
1789 version: String,
1790 head_op: Option<String>,
1791 published_at: u64,
1792}
1793
1794fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1795 root.join("packages").join(name)
1796}
1797
1798fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
1799 pkg_name_dir(root, name).join("index.json")
1800}
1801
1802fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1803 pkg_name_dir(root, name).join(format!("{version}.json"))
1804}
1805
1806fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1807 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
1808}
1809
1810fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
1811 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
1812 serde_json::from_slice(&bytes).ok()
1813}
1814
1815fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
1816 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
1817 serde_json::from_slice(&bytes).ok()
1818}
1819
1820fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1821 let index = load_pkg_index(root, name)?;
1822 let latest = index.latest.clone()?;
1823 load_pkg_record(root, name, &latest)
1824}
1825
1826fn pkg_is_public(root: &std::path::Path, name: &str) -> bool {
1830 load_pkg_index(root, name).map(|i| i.visibility) == Some(Visibility::Public)
1831}
1832
1833fn valid_pkg_segment(s: &str) -> bool {
1838 !s.is_empty()
1839 && s.len() <= 128
1840 && s != "."
1841 && s != ".."
1842 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
1843}
1844
1845#[derive(Deserialize)]
1846struct VisibilityReq {
1847 visibility: Visibility,
1848}
1849
1850fn pkg_set_visibility_handler(
1857 state: &State,
1858 name: &str,
1859 body: &str,
1860) -> Response<std::io::Cursor<Vec<u8>>> {
1861 if !valid_pkg_segment(name) {
1862 return error_response(400, format!("invalid package name {name:?}"));
1863 }
1864 let req: VisibilityReq = match serde_json::from_str(body) {
1865 Ok(r) => r,
1866 Err(e) => return error_response(400, format!("bad request: {e}")),
1867 };
1868 let mut index = match load_pkg_index(&state.root, name) {
1869 Some(i) => i,
1870 None => return error_response(404, format!("package {name:?} not found")),
1871 };
1872 index.visibility = req.visibility;
1873 let bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1874 match std::fs::write(pkg_index_path(&state.root, name), bytes) {
1875 Ok(()) => json_response(
1876 200,
1877 &serde_json::json!({ "name": name, "visibility": index.visibility }),
1878 ),
1879 Err(e) => error_response(500, format!("write index: {e}")),
1880 }
1881}
1882
1883#[derive(serde::Deserialize)]
1884struct ReleaseReq {
1885 version: String,
1886 #[serde(default)]
1887 branch: Option<String>,
1888 #[serde(default)]
1891 dependencies: Vec<String>,
1892 #[serde(default)]
1897 dependency_specs: std::collections::BTreeMap<String, DepSpec>,
1898}
1899
1900fn pkg_release_handler(state: &State, name: &str, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1909 if !valid_pkg_segment(name) {
1910 return error_response(400, format!("invalid package name {name:?}"));
1911 }
1912 let req: ReleaseReq = match serde_json::from_str(body) {
1913 Ok(r) => r,
1914 Err(e) => return error_response(400, format!("bad request: {e}")),
1915 };
1916 let version = req.version.trim().to_string();
1917 if version.is_empty() || !valid_pkg_segment(&version) {
1918 return error_response(400, "version must be a non-empty, path-safe string (e.g. 1.2.0)");
1919 }
1920 if load_pkg_record(&state.root, name, &version).is_some() {
1922 return error_response(
1923 409,
1924 format!("{name}@{version} already released; releases are immutable — bump the version"),
1925 );
1926 }
1927
1928 let store = state.store.lock().unwrap();
1929 let branch = req.branch.unwrap_or_else(|| store.current_branch());
1930 let head_op = match store.get_branch(&branch) {
1931 Ok(Some(b)) => b.head_op,
1932 Ok(None) => return error_response(404, format!("unknown branch {branch:?}")),
1933 Err(e) => return error_response(500, format!("get_branch: {e}")),
1934 };
1935 let Some(head_op) = head_op else {
1936 return error_response(400, format!("branch {branch:?} has no commits to release"));
1937 };
1938
1939 let predecessor = load_pkg_index(&state.root, name)
1950 .map(|i| i.versions)
1951 .unwrap_or_default()
1952 .into_iter()
1953 .filter_map(|v| lex_syntax::semver::parse_exact(&v.version).map(|p| (p, v)))
1954 .filter(|(p, _)| lex_syntax::semver::parse_exact(&version).map(|n| *p < n).unwrap_or(false))
1955 .max_by_key(|(p, _)| *p)
1956 .map(|(_, v)| v);
1957 if let Some(prev) = predecessor {
1958 if let (Some(prev_head), Some(declared)) = (
1959 prev.head_op.clone(),
1960 lex_syntax::semver::bump_between(&prev.version, &version),
1961 ) {
1962 if let (Ok(prev_api), Ok(new_api)) = (
1963 lex_store::api::public_api_at_op(&store, &prev_head),
1964 lex_store::api::public_api_at_op(&store, &head_op),
1965 ) {
1966 use lex_store::api::ApiChange;
1967 use lex_syntax::semver::Bump;
1968 let (required, why) = match lex_store::api::classify_api_change(&prev_api, &new_api) {
1969 ApiChange::Breaking(d) => (Bump::Major, d),
1970 ApiChange::Additive(d) => (Bump::Minor, d),
1971 ApiChange::None => (Bump::Patch, String::new()),
1972 };
1973 if declared < required {
1974 let need = match required {
1975 Bump::Major => "major",
1976 Bump::Minor => "minor",
1977 Bump::Patch => "patch",
1978 };
1979 return error_response(
1980 422,
1981 format!(
1982 "version bump too small: {} → {version} is a {declared:?} bump, \
1983 but the API change ({why}) requires a {need} bump",
1984 prev.version
1985 ),
1986 );
1987 }
1988 }
1989 }
1990 }
1991
1992 let head = store.branch_head(&branch).unwrap_or_default();
1995 let pairs: Vec<(String, String)> = head.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
1996 let function_names: Vec<String> = store
1997 .get_asts_for_sigs_bulk(&pairs)
1998 .into_iter()
1999 .filter_map(|r| r.ok())
2000 .filter_map(|s| match s {
2001 lex_ast::Stage::FnDecl(fd) => Some(fd.name),
2002 _ => None,
2003 })
2004 .collect();
2005 let mut deps: std::collections::BTreeSet<String> = req.dependencies.into_iter().collect();
2009 if let Ok(extracted) = lex_store::api::external_dependencies_at_op(&store, &head_op) {
2010 deps.extend(extracted);
2011 }
2012 deps.extend(req.dependency_specs.keys().cloned());
2015 let dependencies: Vec<String> = deps.into_iter().collect();
2016 drop(store);
2017
2018 let published_at = std::time::SystemTime::now()
2019 .duration_since(std::time::UNIX_EPOCH)
2020 .map(|d| d.as_secs())
2021 .unwrap_or(0);
2022 let record = PkgRecord {
2023 name: name.to_string(),
2024 version: version.clone(),
2025 head_op: Some(head_op.clone()),
2026 published_at,
2027 function_names,
2028 dependencies,
2029 dependency_specs: req.dependency_specs,
2030 ops: Vec::new(),
2031 };
2032 if let Err(e) = save_pkg_record(&state.root, &record, None) {
2033 return error_response(500, format!("write release: {e}"));
2034 }
2035 json_response(
2036 201,
2037 &serde_json::json!({
2038 "name": name,
2039 "version": version,
2040 "head_op": head_op,
2041 "branch": branch,
2042 }),
2043 )
2044}
2045
2046fn public_pkg_names(root: &std::path::Path) -> Vec<String> {
2049 list_pkg_names(root)
2050 .into_iter()
2051 .filter(|name| pkg_is_public(root, name))
2052 .collect()
2053}
2054
2055fn public_pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2059 let packages: Vec<serde_json::Value> = public_pkg_names(&state.root)
2060 .iter()
2061 .filter_map(|name| {
2062 let r = load_latest_pkg_record(&state.root, name)?;
2063 Some(serde_json::json!({
2064 "name": r.name,
2065 "version": r.version,
2066 "head_op": r.head_op,
2067 "published_at": r.published_at,
2068 }))
2069 })
2070 .collect();
2071 json_response(200, &serde_json::json!({ "packages": packages }))
2072}
2073
2074#[derive(Debug, PartialEq, Eq)]
2079enum PublicTarget {
2080 List,
2081 Latest(String),
2082 Versions(String),
2083 ApiDiff(String),
2084 Head(String),
2085 Version(String, String),
2086 Archive(String, String),
2087}
2088
2089impl PublicTarget {
2090 fn pkg_name(&self) -> Option<&str> {
2092 match self {
2093 PublicTarget::List => None,
2094 PublicTarget::Latest(n)
2095 | PublicTarget::Versions(n)
2096 | PublicTarget::ApiDiff(n)
2097 | PublicTarget::Head(n)
2098 | PublicTarget::Version(n, _)
2099 | PublicTarget::Archive(n, _) => Some(n),
2100 }
2101 }
2102}
2103
2104fn resolve_public(method: &Method, path: &str) -> Result<PublicTarget, u16> {
2109 if !matches!(method, Method::Get) {
2110 return Err(405);
2111 }
2112 let rest = path.trim_matches('/');
2113 if rest.is_empty() {
2114 return Ok(PublicTarget::List);
2115 }
2116 let segs: Vec<&str> = rest.split('/').collect();
2117 if !segs.iter().all(|s| valid_pkg_segment(s)) {
2118 return Err(404);
2119 }
2120 match segs.as_slice() {
2121 [n] => Ok(PublicTarget::Latest(n.to_string())),
2122 [n, "versions"] => Ok(PublicTarget::Versions(n.to_string())),
2123 [n, "api-diff"] => Ok(PublicTarget::ApiDiff(n.to_string())),
2124 [n, "head"] => Ok(PublicTarget::Head(n.to_string())),
2125 [n, v, "archive"] => Ok(PublicTarget::Archive(n.to_string(), v.to_string())),
2126 [n, v] => Ok(PublicTarget::Version(n.to_string(), v.to_string())),
2127 _ => Err(404),
2128 }
2129}
2130
2131pub fn route_public(
2142 state: &State,
2143 method: &Method,
2144 path: &str,
2145 query: &str,
2146) -> Response<std::io::Cursor<Vec<u8>>> {
2147 let target = match resolve_public(method, path) {
2148 Ok(t) => t,
2149 Err(405) => return error_response(405, "public read is GET-only"),
2150 Err(_) => return error_response(404, "not found"),
2151 };
2152 if let PublicTarget::List = target {
2154 return public_pkg_list_handler(state);
2155 }
2156 if let Some(name) = target.pkg_name() {
2158 if !pkg_is_public(&state.root, name) {
2159 return error_response(404, format!("package {name:?} not found"));
2160 }
2161 }
2162 match target {
2163 PublicTarget::List => unreachable!("handled above"),
2164 PublicTarget::Latest(n) => pkg_get_handler(state, &n),
2165 PublicTarget::Versions(n) => pkg_versions_handler(state, &n),
2166 PublicTarget::ApiDiff(n) => pkg_api_diff_handler(state, &n, query),
2167 PublicTarget::Head(n) => pkg_head_handler(state, &n),
2168 PublicTarget::Version(n, v) => pkg_get_version_handler(state, &n, &v),
2169 PublicTarget::Archive(n, v) => pkg_archive_handler(state, &n, &v),
2170 }
2171}
2172
2173fn save_pkg_record(
2174 root: &std::path::Path,
2175 record: &PkgRecord,
2176 archive: Option<&[u8]>,
2179) -> std::io::Result<()> {
2180 let dir = pkg_name_dir(root, &record.name);
2181 std::fs::create_dir_all(&dir)?;
2182
2183 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
2185 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
2186
2187 if let Some(archive) = archive {
2189 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
2190 }
2191
2192 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
2194 index.latest = Some(record.version.clone());
2195 if !index.versions.iter().any(|v| v.version == record.version) {
2196 index.versions.push(PkgVersionSummary {
2197 version: record.version.clone(),
2198 head_op: record.head_op.clone(),
2199 published_at: record.published_at,
2200 });
2201 }
2202 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
2203 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
2204}
2205
2206fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
2207 let dir = root.join("packages");
2208 let Ok(entries) = std::fs::read_dir(&dir) else {
2209 return Vec::new();
2210 };
2211 let mut names: Vec<String> = entries
2212 .filter_map(|e| e.ok())
2213 .filter(|e| e.path().is_dir())
2214 .filter_map(|e| e.file_name().into_string().ok())
2215 .collect();
2216 names.sort();
2217 names
2218}
2219
2220fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
2221 let Ok(entries) = std::fs::read_dir(dir) else { return };
2222 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
2223 entries.sort_by_key(|e| e.path());
2224 for entry in entries {
2225 let path = entry.path();
2226 if path.is_dir() {
2227 collect_lex_files(&path, out);
2228 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
2229 out.push(path);
2230 }
2231 }
2232}
2233
2234fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
2237 let tmp = match tempfile::TempDir::new() {
2238 Ok(t) => t,
2239 Err(e) => return error_response(500, format!("create temp dir: {e}")),
2240 };
2241 {
2242 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
2243 let mut ar = tar::Archive::new(gz);
2244 if let Err(e) = ar.unpack(tmp.path()) {
2245 return error_response(400, format!("unpack archive: {e}"));
2246 }
2247 }
2248
2249 let toml_path = tmp.path().join("lex.toml");
2250 if !toml_path.exists() {
2251 return error_response(400, "archive must contain lex.toml at root");
2252 }
2253 let manifest = match Manifest::load(&toml_path) {
2254 Ok(m) => m,
2255 Err(e) => return error_response(400, format!("lex.toml: {e}")),
2256 };
2257 let (pkg_name, pkg_version) = match &manifest.package {
2258 Some(m) => (m.name.clone(), m.version.clone()),
2259 None => return error_response(400, "lex.toml must have a [package] section"),
2260 };
2261
2262 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
2266 return error_response(
2267 409,
2268 format!(
2269 "package {pkg_name}@{pkg_version} already published; \
2270 bump the version in lex.toml to publish a new release"
2271 ),
2272 );
2273 }
2274
2275 let src_dir = tmp.path().join("src");
2276 if !src_dir.exists() {
2277 return error_response(400, "archive must contain a src/ directory");
2278 }
2279 let mut lex_files: Vec<PathBuf> = Vec::new();
2280 collect_lex_files(&src_dir, &mut lex_files);
2281 if lex_files.is_empty() {
2282 return error_response(400, "no .lex files found in src/");
2283 }
2284
2285 let store = state.store.lock().unwrap();
2286 let branch = store.current_branch();
2287
2288 let old_head = match store.branch_head(&branch) {
2308 Ok(h) => h,
2309 Err(e) => return error_response(500, format!("branch_head: {e}")),
2310 };
2311 let old_pairs: Vec<(String, String)> =
2318 old_head.iter().map(|(sig, stage)| (sig.clone(), stage.clone())).collect();
2319 let mut old_fns_by_name: BTreeMap<String, Vec<lex_ast::FnDecl>> = BTreeMap::new();
2320 for fd in store.get_asts_for_sigs_bulk(&old_pairs)
2321 .into_iter()
2322 .filter_map(|r| r.ok())
2323 .filter_map(|s| match s { lex_ast::Stage::FnDecl(fd) => Some(fd), _ => None })
2324 {
2325 old_fns_by_name.entry(fd.name.clone()).or_default().push(fd);
2326 }
2327 let mut old_types_by_name: BTreeMap<String, lex_ast::TypeDecl> = BTreeMap::new();
2330 for td in store.get_asts_for_sigs_bulk(&old_pairs)
2331 .into_iter()
2332 .filter_map(|r| r.ok())
2333 .filter_map(|s| match s { lex_ast::Stage::TypeDecl(td) => Some(td), _ => None })
2334 {
2335 old_types_by_name.insert(td.name.clone(), td);
2336 }
2337 fn structural_key(fd: &lex_ast::FnDecl) -> Option<String> {
2345 let mut anon = fd.clone();
2346 anon.name = String::new();
2347 lex_ast::sig_id(&lex_ast::Stage::FnDecl(anon))
2348 }
2349
2350 fn take_matching(
2360 map: &mut BTreeMap<String, Vec<lex_ast::FnDecl>>,
2361 name: &str,
2362 new_fd: &lex_ast::FnDecl,
2363 ) -> Option<lex_ast::FnDecl> {
2364 let candidates = map.get_mut(name)?;
2365 let idx = match candidates.len() {
2366 0 => return None,
2367 1 => 0,
2368 _ => {
2369 let want = structural_key(new_fd);
2370 candidates.iter().position(|c| structural_key(c) == want)?
2371 }
2372 };
2373 let matched = candidates.remove(idx);
2374 if candidates.is_empty() {
2375 map.remove(name);
2376 }
2377 Some(matched)
2378 }
2379
2380 let loaded = match load_package(&lex_files, tmp.path(), &pkg_name, true) {
2404 Ok(p) => p,
2405 Err(e) => return error_response(400, format!("load package: {e}")),
2406 };
2407 let mut stages = canonicalize_program(&loaded.program);
2408 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
2414 return error_with_detail(
2415 422,
2416 format!("type errors in package {pkg_name}"),
2417 serde_json::to_value(&errs).unwrap(),
2418 );
2419 }
2420 let new_fns = stage_fns(&stages);
2421 let all_function_names: Vec<String> = new_fns.keys().cloned().collect();
2422
2423 let mut old_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2428 for (name, new_fd) in &new_fns {
2429 if let Some(fd) = take_matching(&mut old_fns_by_name, name, new_fd) {
2430 old_fns.insert(name.clone(), fd);
2431 }
2432 }
2433 let new_types = stage_types(&stages);
2434 let old_types: BTreeMap<String, lex_ast::TypeDecl> = new_types
2438 .keys()
2439 .filter_map(|n| old_types_by_name.get(n).map(|td| (n.clone(), td.clone())))
2440 .collect();
2441 let report =
2442 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
2443
2444 let mut new_imports = lex_vcs::ImportMap::new();
2452 for (file, modules) in &loaded.imports_by_file {
2453 let entry = new_imports.entry(file.clone()).or_default();
2454 for (reference, alias) in modules {
2455 entry.insert(lex_vcs::ImportRef {
2456 reference: reference.clone(),
2457 alias: alias.clone(),
2458 });
2459 }
2460 }
2461
2462 let outcome = match store.publish_program_with_intent(
2465 &branch,
2466 &stages,
2467 &report,
2468 &new_imports,
2469 false,
2470 None,
2471 None,
2472 &loaded.module_prefixes,
2473 ) {
2474 Ok(outcome) => outcome,
2475 Err(lex_store::StoreError::TypeError(errs)) => {
2476 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
2477 }
2478 Err(e) => return write_error_response("publish_program", e),
2479 };
2480 let all_ops: Vec<serde_json::Value> = match serde_json::to_value(&outcome.ops) {
2481 Ok(serde_json::Value::Array(arr)) => arr,
2482 _ => Vec::new(),
2483 };
2484 let final_head_op = outcome.head_op;
2485
2486 let now = SystemTime::now()
2511 .duration_since(UNIX_EPOCH)
2512 .map(|d| d.as_secs())
2513 .unwrap_or(0);
2514 let dependencies: Vec<String> = final_head_op
2517 .as_ref()
2518 .and_then(|h| lex_store::api::external_dependencies_at_op(&store, h).ok())
2519 .unwrap_or_default();
2520 let record = PkgRecord {
2521 name: pkg_name.clone(),
2522 version: pkg_version,
2523 head_op: final_head_op.clone(),
2524 published_at: now,
2525 function_names: all_function_names,
2526 dependencies,
2527 dependency_specs: Default::default(),
2531 ops: all_ops.clone(),
2532 };
2533 if let Err(e) = save_pkg_record(&state.root, &record, Some(body)) {
2534 return error_response(500, format!("save package index: {e}"));
2535 }
2536
2537 json_response(200, &serde_json::json!({
2538 "package": pkg_name,
2539 "ops": all_ops,
2540 "head_op": final_head_op,
2541 }))
2542}
2543
2544fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2546 let names = list_pkg_names(&state.root);
2547 let packages: Vec<serde_json::Value> = names.iter()
2548 .filter_map(|name| {
2549 let idx = load_pkg_index(&state.root, name)?;
2550 let latest = idx.latest.as_deref()?;
2551 let r = load_pkg_record(&state.root, name, latest)?;
2552 Some(serde_json::json!({
2553 "name": r.name,
2554 "version": r.version,
2555 "head_op": r.head_op,
2556 "published_at": r.published_at,
2557 }))
2558 })
2559 .collect();
2560 json_response(200, &serde_json::json!({ "packages": packages }))
2561}
2562
2563fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2565 match load_latest_pkg_record(&state.root, name) {
2566 Some(r) => json_response(200, &serde_json::json!({
2567 "name": r.name,
2568 "version": r.version,
2569 "head_op": r.head_op,
2570 "published_at": r.published_at,
2571 "function_names": r.function_names,
2572 "ops": r.ops,
2573 })),
2574 None => error_response(404, format!("package {name:?} not found")),
2575 }
2576}
2577
2578fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2580 match load_pkg_index(&state.root, name) {
2581 Some(idx) => json_response(200, &serde_json::json!({
2582 "name": name,
2583 "latest": idx.latest,
2584 "versions": idx.versions,
2585 })),
2586 None => error_response(404, format!("package {name:?} not found")),
2587 }
2588}
2589
2590fn pkg_api_diff_handler(state: &State, name: &str, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2595 let mut from: Option<String> = None;
2596 let mut to: Option<String> = None;
2597 for kv in query.split('&') {
2598 match kv.split_once('=') {
2599 Some(("from", v)) => from = Some(v.to_string()),
2600 Some(("to", v)) => to = Some(v.to_string()),
2601 _ => {}
2602 }
2603 }
2604 let (Some(from), Some(to)) = (from, to) else {
2605 return error_response(400, "api-diff requires ?from=<version>&to=<version>");
2606 };
2607 let head_of = |v: &str| load_pkg_record(&state.root, name, v).and_then(|r| r.head_op);
2608 let (Some(from_head), Some(to_head)) = (head_of(&from), head_of(&to)) else {
2609 return error_response(404, format!("{name}: unknown release in {from}..{to}"));
2610 };
2611
2612 let store = state.store.lock().unwrap();
2613 let (prev_api, new_api) = match (
2614 lex_store::api::public_api_at_op(&store, &from_head),
2615 lex_store::api::public_api_at_op(&store, &to_head),
2616 ) {
2617 (Ok(a), Ok(b)) => (a, b),
2618 _ => return error_response(500, "could not read package APIs for the given releases"),
2619 };
2620 let (change, detail) = match lex_store::api::classify_api_change(&prev_api, &new_api) {
2621 lex_store::api::ApiChange::Breaking(d) => ("breaking", d),
2622 lex_store::api::ApiChange::Additive(d) => ("additive", d),
2623 lex_store::api::ApiChange::None => ("none", String::new()),
2624 };
2625 let renames = lex_store::api::detect_renames(&prev_api, &new_api);
2626 json_response(200, &serde_json::json!({
2627 "name": name, "from": from, "to": to,
2628 "change": change, "detail": detail, "renames": renames,
2629 }))
2630}
2631
2632fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2634 match load_pkg_record(&state.root, name, version) {
2635 Some(r) => json_response(200, &serde_json::json!({
2636 "name": r.name,
2637 "version": r.version,
2638 "head_op": r.head_op,
2639 "published_at": r.published_at,
2640 "function_names": r.function_names,
2641 "dependencies": r.dependencies,
2642 "ops": r.ops,
2643 })),
2644 None => error_response(404, format!("package {name:?}@{version:?} not found")),
2645 }
2646}
2647
2648fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2650 let gzip = |bytes: Vec<u8>| {
2651 Response::from_data(bytes).with_status_code(200).with_header(
2652 tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"application/gzip"[..]).unwrap(),
2653 )
2654 };
2655
2656 if let Ok(bytes) = std::fs::read(pkg_archive_path(&state.root, name, version)) {
2658 return gzip(bytes);
2659 }
2660
2661 if let Some(record) = load_pkg_record(&state.root, name, version) {
2665 if let Some(head_op) = record.head_op.clone() {
2666 match render_op_log_archive(state, name, version, &head_op, &record.dependency_specs) {
2667 Ok(bytes) => return gzip(bytes),
2668 Err(e) => {
2669 return error_response(500, format!("rendering archive for {name:?}@{version:?}: {e}"));
2670 }
2671 }
2672 }
2673 }
2674
2675 error_response(404, format!("archive for {name:?}@{version:?} not found"))
2676}
2677
2678const DEFAULT_ARCHIVE_MODULE: &str = "src/lib.lex";
2683
2684fn render_op_log_archive(
2689 state: &State,
2690 name: &str,
2691 version: &str,
2692 head_op: &str,
2693 dependency_specs: &std::collections::BTreeMap<String, DepSpec>,
2694) -> Result<Vec<u8>, String> {
2695 let files: Vec<(String, String)> = {
2701 let store = state.store.lock().unwrap();
2702 let head = lex_store::render::package_head_at_op(&store, head_op)
2703 .map_err(|e| format!("reading head {head_op}: {e}"))?;
2704 match lex_store::render::render_source(&store, &head)
2705 .map_err(|e| format!("rendering source at {head_op}: {e}"))?
2706 {
2707 lex_store::render::RenderedSource::Single { path, src } => {
2708 vec![(path.unwrap_or_else(|| DEFAULT_ARCHIVE_MODULE.to_string()), src)]
2711 }
2712 lex_store::render::RenderedSource::Multi(tree) => tree.into_iter().collect(),
2713 }
2714 };
2715
2716 let mut manifest = format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\n");
2721 let dep_lines: Vec<String> = dependency_specs
2722 .iter()
2723 .filter_map(|(dep_name, spec)| spec.to_toml_inline().map(|inline| format!("{dep_name} = {inline}")))
2724 .collect();
2725 if !dep_lines.is_empty() {
2726 manifest.push_str("\n[dependencies]\n");
2727 for line in dep_lines {
2728 manifest.push_str(&line);
2729 manifest.push('\n');
2730 }
2731 }
2732 let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2733 {
2734 let mut ar = tar::Builder::new(&mut enc);
2735 let mut append = |p: &str, data: &[u8]| -> std::io::Result<()> {
2736 let mut h = tar::Header::new_gnu();
2737 h.set_size(data.len() as u64);
2738 h.set_mode(0o644);
2739 h.set_cksum();
2740 ar.append_data(&mut h, p, data)
2741 };
2742 append("lex.toml", manifest.as_bytes()).map_err(|e| e.to_string())?;
2743 for (path, src) in &files {
2744 append(path, src.as_bytes()).map_err(|e| e.to_string())?;
2745 }
2746 ar.finish().map_err(|e| e.to_string())?;
2747 }
2748 enc.finish().map_err(|e| e.to_string())
2749}
2750
2751fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2753 match load_latest_pkg_record(&state.root, name) {
2754 Some(r) => json_response(200, &serde_json::json!({
2755 "name": r.name,
2756 "version": r.version,
2757 "head_op": r.head_op,
2758 })),
2759 None => error_response(404, format!("package {name:?} not found")),
2760 }
2761}
2762
2763fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2765 let record = match load_latest_pkg_record(&state.root, name) {
2766 Some(r) => r,
2767 None => return error_response(404, format!("package {name:?} not found")),
2768 };
2769
2770 let store = state.store.lock().unwrap();
2771 let branch = store.current_branch();
2772
2773 let head = match store.branch_head(&branch) {
2774 Ok(h) => h,
2775 Err(e) => return error_response(500, format!("branch_head: {e}")),
2776 };
2777
2778 let head_pairs: Vec<(String, String)> = head
2785 .iter()
2786 .map(|(sig, stage)| (sig.clone(), stage.clone()))
2787 .collect();
2788 let old_fns: BTreeMap<String, lex_ast::FnDecl> = store
2789 .get_asts_for_sigs_bulk(&head_pairs)
2790 .into_iter()
2791 .filter_map(|r| r.ok())
2792 .filter_map(|s| match s {
2793 lex_ast::Stage::FnDecl(fd)
2794 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
2795 _ => None,
2796 })
2797 .collect();
2798
2799 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2800 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2801 let empty_imports = lex_vcs::ImportMap::new();
2802
2803 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
2804 Ok(outcome) => {
2805 let ver = record.version.clone();
2807 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
2808 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
2809 if let Some(mut idx) = load_pkg_index(&state.root, name) {
2811 idx.versions.retain(|v| v.version != ver);
2812 idx.latest = idx.versions.last().map(|v| v.version.clone());
2813 if idx.versions.is_empty() {
2814 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
2815 } else {
2816 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
2817 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
2818 }
2819 }
2820 json_response(200, &serde_json::json!({
2821 "deleted": name,
2822 "version": ver,
2823 "ops": outcome.ops,
2824 "head_op": outcome.head_op,
2825 }))
2826 }
2827 Err(lex_store::StoreError::TypeError(errs)) => {
2828 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
2829 }
2830 Err(e) => write_error_response("retract package", e),
2831 }
2832}
2833
2834#[cfg(test)]
2835mod dep_spec_tests {
2836 use super::DepSpec;
2837
2838 #[test]
2839 fn a_dual_spec_renders_both_git_and_vcs_refs_vcs_first() {
2840 let spec = DepSpec {
2841 registry: Some("vcs.lexlang.org/lex-official/lex-schema".into()),
2842 version: Some("^0.9".into()),
2843 git: Some("https://github.com/alpibrusl/lex-schema".into()),
2844 ..Default::default()
2845 };
2846 assert_eq!(
2847 spec.to_toml_inline().as_deref(),
2848 Some("{ registry = \"vcs.lexlang.org/lex-official/lex-schema\", version = \"^0.9\", git = \"https://github.com/alpibrusl/lex-schema\" }"),
2849 );
2850 }
2851
2852 #[test]
2853 fn bare_git_and_bare_registry_specs_render_their_own_keys() {
2854 let git = DepSpec { git: Some("https://x/g".into()), tag: Some("v1".into()), ..Default::default() };
2855 assert_eq!(git.to_toml_inline().as_deref(), Some("{ git = \"https://x/g\", tag = \"v1\" }"));
2856 let reg = DepSpec { registry: Some("vcs/r".into()), version: Some("1.0.0".into()), ..Default::default() };
2857 assert_eq!(reg.to_toml_inline().as_deref(), Some("{ registry = \"vcs/r\", version = \"1.0.0\" }"));
2858 }
2859
2860 #[test]
2861 fn an_empty_spec_renders_nothing() {
2862 assert_eq!(DepSpec::default().to_toml_inline(), None);
2863 }
2864}
2865
2866#[cfg(test)]
2867mod policy_ceiling_tests {
2868 use super::*;
2869 use lex_runtime::Policy;
2870 use std::path::PathBuf;
2871
2872 fn permissive_request() -> Policy {
2876 Policy {
2877 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
2878 .iter()
2879 .map(|s| s.to_string())
2880 .collect(),
2881 allow_fs_read: vec![PathBuf::from("/")],
2882 allow_fs_write: vec![PathBuf::from("/")],
2883 allow_net_host: Vec::new(),
2884 allow_proc: Vec::new(),
2885 allow_approval: Vec::new(),
2886 budget: None,
2887 }
2888 }
2889
2890 #[test]
2891 fn ceiling_drops_effects_the_caller_was_not_granted() {
2892 let ceiling = Policy {
2893 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
2894 ..Policy::default()
2895 };
2896 let got = clamp_policy(permissive_request(), &ceiling);
2897 assert!(got.allow_effects.contains("io"));
2898 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
2899 assert!(!got.allow_effects.contains("fs_write"));
2900 assert!(!got.allow_effects.contains("net"));
2901 assert!(!got.allow_effects.contains("time"));
2903 }
2904
2905 #[test]
2906 fn ceiling_scopes_override_caller_scopes() {
2907 let ceiling = Policy {
2908 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
2909 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
2910 ..Policy::default()
2911 };
2912 let got = clamp_policy(permissive_request(), &ceiling);
2913 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
2916 assert!(got.allow_fs_write.is_empty());
2917 assert!(got.allow_proc.is_empty());
2918 assert!(got.allow_net_host.is_empty());
2919 }
2920
2921 #[test]
2922 fn ceiling_caps_budget_and_prefers_the_smaller() {
2923 let mut req = permissive_request();
2925 req.budget = None;
2926 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
2927 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
2928
2929 let mut req2 = permissive_request();
2931 req2.budget = Some(50);
2932 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
2933 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
2934 }
2935
2936 #[test]
2937 fn empty_ceiling_is_pure_only() {
2938 let got = clamp_policy(permissive_request(), &Policy::default());
2939 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
2940 assert!(got.allow_proc.is_empty());
2941 assert!(got.allow_fs_write.is_empty());
2942 }
2943}
2944
2945#[cfg(test)]
2946mod public_read_tests {
2947 use super::*;
2948
2949 fn seed_pkg(root: &std::path::Path, name: &str, version: &str) {
2952 let record = PkgRecord {
2953 name: name.to_string(),
2954 version: version.to_string(),
2955 head_op: Some(format!("op-{name}")),
2956 published_at: 1,
2957 function_names: vec![format!("{name}.f")],
2958 dependencies: vec![],
2959 dependency_specs: Default::default(),
2960 ops: vec![],
2961 };
2962 save_pkg_record(root, &record, Some(format!("ARCHIVE:{name}@{version}").as_bytes()))
2963 .expect("seed package");
2964 }
2965
2966 #[test]
2967 fn new_package_defaults_to_private() {
2968 let tmp = tempfile::TempDir::new().unwrap();
2969 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2970 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2971 assert!(!pkg_is_public(tmp.path(), "does-not-exist"));
2973 }
2974
2975 #[test]
2976 fn set_visibility_round_trips_and_index_persists() {
2977 let tmp = tempfile::TempDir::new().unwrap();
2978 let state = State::open(tmp.path().to_path_buf()).unwrap();
2979 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2980
2981 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"public"}"#);
2982 assert!(pkg_is_public(tmp.path(), "lex-schema"));
2983 let idx = load_pkg_index(tmp.path(), "lex-schema").unwrap();
2985 assert_eq!(idx.latest.as_deref(), Some("0.9.2"));
2986 assert_eq!(idx.versions.len(), 1);
2987
2988 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"private"}"#);
2989 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2990 }
2991
2992 #[test]
2993 fn set_visibility_on_unknown_package_is_a_noop() {
2994 let tmp = tempfile::TempDir::new().unwrap();
2995 let state = State::open(tmp.path().to_path_buf()).unwrap();
2996 let _ = pkg_set_visibility_handler(&state, "ghost", r#"{"visibility":"public"}"#);
2998 assert!(load_pkg_index(tmp.path(), "ghost").is_none());
2999 }
3000
3001 #[test]
3002 fn public_listing_omits_private_packages() {
3003 let tmp = tempfile::TempDir::new().unwrap();
3004 let state = State::open(tmp.path().to_path_buf()).unwrap();
3005 seed_pkg(tmp.path(), "pub-pkg", "1.0.0");
3006 seed_pkg(tmp.path(), "priv-pkg", "1.0.0");
3007 let _ = pkg_set_visibility_handler(&state, "pub-pkg", r#"{"visibility":"public"}"#);
3008
3009 let names = public_pkg_names(tmp.path());
3010 assert_eq!(names, vec!["pub-pkg".to_string()]);
3011 }
3012
3013 #[test]
3014 fn resolve_public_maps_routes() {
3015 let get = Method::Get;
3016 assert_eq!(resolve_public(&get, "").unwrap(), PublicTarget::List);
3017 assert_eq!(resolve_public(&get, "/").unwrap(), PublicTarget::List);
3018 assert_eq!(
3019 resolve_public(&get, "/lex-schema").unwrap(),
3020 PublicTarget::Latest("lex-schema".into())
3021 );
3022 assert_eq!(
3023 resolve_public(&get, "/lex-schema/versions").unwrap(),
3024 PublicTarget::Versions("lex-schema".into())
3025 );
3026 assert_eq!(
3027 resolve_public(&get, "/lex-schema/head").unwrap(),
3028 PublicTarget::Head("lex-schema".into())
3029 );
3030 assert_eq!(
3031 resolve_public(&get, "/lex-schema/0.9.2").unwrap(),
3032 PublicTarget::Version("lex-schema".into(), "0.9.2".into())
3033 );
3034 assert_eq!(
3035 resolve_public(&get, "/lex-schema/0.9.2/archive").unwrap(),
3036 PublicTarget::Archive("lex-schema".into(), "0.9.2".into())
3037 );
3038 }
3039
3040 #[test]
3041 fn resolve_public_rejects_bad_method_and_traversal() {
3042 assert_eq!(resolve_public(&Method::Put, "/lex-schema"), Err(405));
3044 assert_eq!(resolve_public(&Method::Post, "").err(), Some(405));
3045 assert_eq!(resolve_public(&Method::Get, "/.."), Err(404));
3047 assert_eq!(resolve_public(&Method::Get, "/lex-schema/../etc"), Err(404));
3048 assert_eq!(resolve_public(&Method::Get, "/a/b/c/d"), Err(404));
3049 assert!(resolve_public(&Method::Get, "/lex schema").is_err());
3051 }
3052}