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::Post, "/v1/locks/batch") => crate::sync_http::locks_batch_handler(state, body),
408 (Method::Post, "/v1/locks/fetch") => crate::sync_http::locks_fetch_handler(state, body),
409 (Method::Post, "/v1/issues/batch") => crate::sync_http::issues_batch_handler(state, body),
412 (Method::Post, "/v1/issues/fetch") => crate::sync_http::issues_fetch_handler(state, body),
413 (Method::Get, "/v1/issues/list") => crate::sync_http::issues_list_handler(state),
414 (Method::Get, "/v1/issues") => crate::issues_http::issues_state_handler(state),
418 (Method::Get, "/v1/projects") => crate::issues_http::projects_handler(state),
419 (Method::Get, p) if p.starts_with("/v1/issues/") => {
420 crate::issues_http::issue_detail_handler(state, &p["/v1/issues/".len()..])
421 }
422 (Method::Get, "/v1/review/inbox") => crate::review_http::review_inbox_handler(state, query),
426 (Method::Post, "/v1/review/verdict") => crate::review_http::review_verdict_handler(state, body),
427 (Method::Get, "/v1/branches") => crate::branches_http::branches_list_handler(state),
428 (Method::Post, "/v1/branches") => crate::branches_http::branch_create_handler(state, body),
429 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/checkout") => {
430 let name = &p["/v1/branches/".len()..p.len() - "/checkout".len()];
431 crate::branches_http::branch_checkout_handler(state, name)
432 }
433 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
436 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
437 crate::branches_http::branch_head_handler(state, name)
438 }
439 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
440 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
441 crate::branches_http::branch_advance_head_handler(state, name, body)
442 }
443 (Method::Get, "/v1/ops/since") => ops_since_handler(state, query),
447 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
448 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
452 (Method::Put, p) if p.starts_with("/v1/pkg/") && p.ends_with("/visibility") => {
456 let name = &p["/v1/pkg/".len()..p.len() - "/visibility".len()];
457 pkg_set_visibility_handler(state, name, body)
458 }
459 (Method::Post, p) if p.starts_with("/v1/pkg/") && p.ends_with("/release") => {
462 let name = &p["/v1/pkg/".len()..p.len() - "/release".len()];
463 pkg_release_handler(state, name, body)
464 }
465 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
466 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
467 pkg_head_handler(state, name)
468 }
469 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/versions") => {
470 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
471 pkg_versions_handler(state, name)
472 }
473 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/api-diff") => {
474 let name = &p["/v1/pkg/".len()..p.len() - "/api-diff".len()];
475 pkg_api_diff_handler(state, name, query)
476 }
477 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
479 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
480 if let Some((name, version)) = inner.split_once('/') {
482 pkg_archive_handler(state, name, version)
483 } else {
484 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
485 }
486 }
487 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
489 let inner = &p["/v1/pkg/".len()..];
490 if let Some((name, version)) = inner.split_once('/') {
491 pkg_get_version_handler(state, name, version)
492 } else {
493 error_response(400, "expected /v1/pkg/{name}/{version}")
494 }
495 }
496 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
497 let name = &p["/v1/pkg/".len()..];
498 pkg_get_handler(state, name)
499 }
500 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
501 let name = &p["/v1/pkg/".len()..];
502 pkg_delete_handler(state, name)
503 }
504 _ => error_response(404, format!("unknown route: {method:?} {path}")),
505 }
506}
507
508#[derive(Deserialize)]
509struct ParseReq { source: String }
510
511fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
512 let req: ParseReq = match serde_json::from_str(body) {
513 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
514 };
515 match load_program_from_str(&req.source) {
516 Ok(prog) => {
517 let stages = canonicalize_program(&prog);
518 json_response(200, &serde_json::to_value(&stages).unwrap())
519 }
520 Err(e) => error_response(400, format!("syntax error: {e}")),
521 }
522}
523
524pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
525 let req: ParseReq = match serde_json::from_str(body) {
526 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
527 };
528 let prog = match load_program_from_str(&req.source) {
529 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
530 };
531 let stages = canonicalize_program(&prog);
532 match lex_types::check_program(&stages) {
533 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
534 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
535 }
536}
537
538#[derive(Deserialize)]
539struct PublishReq { source: String, #[serde(default)] activate: bool }
540
541pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
542 let req: PublishReq = match serde_json::from_str(body) {
543 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
544 };
545 let prog = match load_program_from_str(&req.source) {
546 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
547 };
548 let mut stages = canonicalize_program(&prog);
552 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
553 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
554 }
555 let example_errors = lex_runtime::evaluate_examples(&stages);
561 if !example_errors.is_empty() {
562 return error_with_detail(422, "example mismatch",
563 serde_json::to_value(&example_errors).unwrap_or_default());
564 }
565
566 let store = state.store.lock().unwrap();
567 let branch = store.current_branch();
568
569 let old_head = match store.branch_head(&branch) {
571 Ok(h) => h,
572 Err(e) => return error_response(500, format!("branch_head: {e}")),
573 };
574 let old_head_stages: Vec<lex_ast::Stage> =
576 old_head.values().filter_map(|stg| store.get_ast(stg).ok()).collect();
577 let old_fns = stage_fns(&old_head_stages);
578 let new_fns = stage_fns(&stages);
579 let old_types = stage_types(&old_head_stages);
580 let new_types = stage_types(&stages);
581 let report =
582 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
583
584 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
586 {
587 let entry = new_imports.entry("<source>".into()).or_default();
588 for s in &stages {
589 if let lex_ast::Stage::Import(im) = s {
590 entry.insert(lex_vcs::ImportRef {
591 reference: im.reference.clone(),
592 alias: im.alias.clone(),
593 });
594 }
595 }
596 }
597
598 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
599 Ok(outcome) => {
600 record_examples_for_publish(&store, &stages, &outcome);
604 json_response(200, &serde_json::json!({
605 "ops": outcome.ops,
606 "head_op": outcome.head_op,
607 }))
608 }
609 Err(lex_store::StoreError::TypeError(errs)) => {
617 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
618 }
619 Err(e) => write_error_response("publish_program", e),
620 }
621}
622
623#[derive(Deserialize)]
624struct PatchReq {
625 stage_id: String,
626 patch: lex_ast::Patch,
627 #[serde(default)] activate: bool,
628}
629
630fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
633 let req: PatchReq = match serde_json::from_str(body) {
634 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
635 };
636 let store = state.store.lock().unwrap();
637
638 let original = match store.get_ast(&req.stage_id) {
640 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
641 };
642
643 let patched = match lex_ast::apply_patch(&original, &req.patch) {
645 Ok(s) => s,
646 Err(e) => return error_with_detail(422, "patch failed",
647 serde_json::to_value(&e).unwrap_or_default()),
648 };
649
650 let branch = store.current_branch();
661
662 let sig = match lex_ast::sig_id(&patched) {
664 Some(s) => s,
665 None => return error_response(500, "patched stage has no sig_id"),
666 };
667
668 let new_id = match store.publish(&patched) {
671 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
672 };
673
674 let original_effects: std::collections::BTreeSet<String> = match &original {
676 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
677 _ => std::collections::BTreeSet::new(),
678 };
679 let patched_effects: std::collections::BTreeSet<String> = match &patched {
680 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
681 _ => std::collections::BTreeSet::new(),
682 };
683 let head_now = match store.get_branch(&branch) {
684 Ok(b) => b.and_then(|b| b.head_op),
685 Err(e) => return error_response(500, format!("get_branch: {e}")),
686 };
687 let kind = if original_effects != patched_effects {
688 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
695 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
696 lex_vcs::OperationKind::ChangeEffectSig {
697 sig_id: sig.clone(),
698 from_stage_id: req.stage_id.clone(),
699 to_stage_id: new_id.clone(),
700 from_effects: original_effects,
701 to_effects: patched_effects,
702 from_budget,
703 to_budget,
704 }
705 } else {
706 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
707 lex_vcs::OperationKind::ModifyBody {
708 sig_id: sig.clone(),
709 from_stage_id: req.stage_id.clone(),
710 to_stage_id: new_id.clone(),
711 from_budget: budget,
712 to_budget: budget,
713 }
714 };
715 let transition = lex_vcs::StageTransition::Replace {
716 sig_id: sig.clone(),
717 from: req.stage_id.clone(),
718 to: new_id.clone(),
719 };
720 let op = lex_vcs::Operation::new(
721 kind,
722 head_now.into_iter().collect::<Vec<_>>(),
723 );
724 let op_id = match store.apply_operation_gated(&branch, op, transition) {
725 Ok(id) => id,
726 Err(lex_store::StoreError::TypeError(errs)) => return error_with_detail(
727 422, "type errors after patch", serde_json::to_value(&errs).unwrap_or_default()),
728 Err(e) => return write_error_response("apply_operation_gated", e),
729 };
730 if req.activate {
731 if let Err(e) = store.activate(&new_id) {
732 return error_response(500, format!("activate: {e}"));
733 }
734 }
735
736 let status = format!("{:?}",
737 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
738 json_response(200, &serde_json::json!({
739 "old_stage_id": req.stage_id,
740 "new_stage_id": new_id,
741 "sig_id": sig,
742 "status": status,
743 "op_id": op_id,
744 }))
745}
746
747pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
748 let store = state.store.lock().unwrap();
749 let meta = match store.get_metadata(id) {
750 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
751 };
752 let ast = match store.get_ast(id) {
753 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
754 };
755 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
756 json_response(200, &serde_json::json!({
757 "metadata": meta,
758 "ast": ast,
759 "status": status,
760 }))
761}
762
763pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
772 let store = state.store.lock().unwrap();
773 if let Err(e) = store.get_metadata(id) {
774 return error_response(404, format!("{e}"));
775 }
776 let log = match store.attestation_log() {
777 Ok(l) => l,
778 Err(e) => return error_response(500, format!("attestation log: {e}")),
779 };
780 let mut listing = match log.list_for_stage(&id.to_string()) {
781 Ok(v) => v,
782 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
783 };
784 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
785 json_response(200, &serde_json::json!({"attestations": listing}))
786}
787
788#[derive(Deserialize, Default)]
789struct PolicyJson {
790 #[serde(default)] allow_effects: Vec<String>,
791 #[serde(default)] allow_fs_read: Vec<String>,
792 #[serde(default)] allow_fs_write: Vec<String>,
793 #[serde(default)] budget: Option<u64>,
794}
795
796impl PolicyJson {
797 fn into_policy(self) -> Policy {
798 Policy {
799 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
800 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
801 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
802 allow_net_host: Vec::new(),
803 allow_proc: Vec::new(),
804 allow_approval: Vec::new(),
805 budget: self.budget,
806 }
807 }
808}
809
810#[derive(Deserialize)]
811struct RunReq {
812 source: String,
813 #[serde(rename = "fn")] func: String,
814 #[serde(default)] args: Vec<serde_json::Value>,
815 #[serde(default)] policy: PolicyJson,
816 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
817}
818
819pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
820 let req: RunReq = match serde_json::from_str(body) {
821 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
822 };
823 let prog = match load_program_from_str(&req.source) {
824 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
825 };
826 let stages = canonicalize_program(&prog);
827 if let Err(errs) = lex_types::check_program(&stages) {
828 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
829 }
830 let bc = compile_program(&stages);
831 let mut policy = req.policy.into_policy();
832 if let Some(ceiling) = &state.policy_ceiling {
838 policy = clamp_policy(policy, ceiling);
839 }
840 if let Err(violations) = check_policy(&bc, &policy) {
841 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
842 }
843
844 let mut recorder = lex_trace::Recorder::new();
845 if with_overrides && !req.overrides.is_empty() {
846 recorder = recorder.with_overrides(req.overrides);
847 }
848 let handle = recorder.handle();
849 let handler = DefaultHandler::new(policy);
850 let mut vm = Vm::with_handler(&bc, Box::new(handler));
851 vm.set_tracer(Box::new(recorder));
852
853 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
854 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
855 let result = vm.call(&req.func, vargs);
856 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
857
858 let store = state.store.lock().unwrap();
859 let (root_out, root_err, status) = match &result {
860 Ok(v) => (Some(value_to_json(v)), None, 200u16),
861 Err(e) => (None, Some(format!("{e}")), 200u16),
862 };
863 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
864 root_out.clone(), root_err.clone(), started, ended);
865 let run_id = match store.save_trace(&tree) {
866 Ok(id) => id,
867 Err(e) => return error_response(500, format!("save_trace: {e}")),
868 };
869
870 let mut body = serde_json::json!({
871 "run_id": run_id,
872 "output": root_out,
873 });
874 if let Some(err) = root_err {
875 body["error"] = serde_json::Value::String(err);
876 }
877 json_response(status, &body)
878}
879
880fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
881 let store = state.store.lock().unwrap();
882 match store.load_trace(id) {
883 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
884 Err(e) => error_response(404, format!("{e}")),
885 }
886}
887
888fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
889 let mut a = None;
890 let mut b = None;
891 for kv in query.split('&') {
892 if let Some((k, v)) = kv.split_once('=') {
893 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
894 }
895 }
896 let (Some(a), Some(b)) = (a, b) else {
897 return error_response(400, "missing a or b query params");
898 };
899 let store = state.store.lock().unwrap();
900 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
901 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
902 match lex_trace::diff_runs(&ta, &tb) {
903 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
904 None => json_response(200, &serde_json::json!({"divergence": null})),
905 }
906}
907
908fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
909
910fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
911
912#[derive(Deserialize)]
913struct MergeStartReq {
914 src_branch: String,
915 dst_branch: String,
916}
917
918fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
929 let req: MergeStartReq = match serde_json::from_str(body) {
930 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
931 };
932 let store = state.store.lock().unwrap();
933 let src_head = match store.get_branch(&req.src_branch) {
934 Ok(Some(b)) => b.head_op,
935 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
936 Err(e) => return error_response(500, format!("src branch read: {e}")),
937 };
938 let dst_head = match store.get_branch(&req.dst_branch) {
939 Ok(Some(b)) => b.head_op,
940 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
941 Err(e) => return error_response(500, format!("dst branch read: {e}")),
942 };
943 let log = match lex_vcs::OpLog::open(store.root()) {
944 Ok(l) => l,
945 Err(e) => return error_response(500, format!("op log: {e}")),
946 };
947 let merge_id = mint_merge_id();
951 let session = match MergeSession::start(
952 merge_id.clone(),
953 &log,
954 src_head.as_ref(),
955 dst_head.as_ref(),
956 ) {
957 Ok(s) => s,
958 Err(e) => return error_response(500, format!("merge start: {e}")),
959 };
960 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
961 let auto_resolved_count = session.auto_resolved.len();
962 let body = serde_json::json!({
963 "merge_id": merge_id,
964 "src_head": session.src_head,
965 "dst_head": session.dst_head,
966 "lca": session.lca,
967 "conflicts": conflicts,
968 "auto_resolved_count": auto_resolved_count,
969 });
970 drop(conflicts);
971 drop(store);
972 let wrapped = ApiMergeSession {
973 inner: session,
974 src_branch: req.src_branch,
975 dst_branch: req.dst_branch,
976 };
977 state.sessions.lock().unwrap().insert(merge_id, wrapped);
978 json_response(200, &body)
979}
980
981#[derive(Deserialize)]
982struct MergeResolveReq {
983 resolutions: Vec<MergeResolveEntry>,
988}
989
990#[derive(Deserialize)]
991struct MergeResolveEntry {
992 conflict_id: String,
993 resolution: lex_vcs::Resolution,
994}
995
996fn merge_resolve_handler(
1007 state: &State,
1008 merge_id: &str,
1009 body: &str,
1010) -> Response<std::io::Cursor<Vec<u8>>> {
1011 let req: MergeResolveReq = match serde_json::from_str(body) {
1012 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
1013 };
1014 let mut sessions = state.sessions.lock().unwrap();
1015 let Some(wrapped) = sessions.get_mut(merge_id) else {
1016 return error_response(404, format!("unknown merge_id `{merge_id}`"));
1017 };
1018 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
1019 .map(|e| (e.conflict_id, e.resolution))
1020 .collect();
1021 let store = state.store.lock().unwrap();
1026 let checker = lex_store::MergeResolutionChecker::new(&store, wrapped.dst_branch.clone());
1027 let verdicts = wrapped.inner.resolve_checked(pairs, &checker);
1028 drop(store);
1029 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
1030 let body = serde_json::json!({
1031 "verdicts": verdicts,
1032 "remaining_conflicts": remaining,
1033 });
1034 json_response(200, &body)
1035}
1036
1037fn merge_commit_handler(
1056 state: &State,
1057 merge_id: &str,
1058) -> Response<std::io::Cursor<Vec<u8>>> {
1059 use std::collections::BTreeMap;
1060 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
1061 Some(w) => w,
1062 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
1063 };
1064 let dst_branch = wrapped.dst_branch.clone();
1065 let src_head = wrapped.inner.src_head.clone();
1066 let dst_head = wrapped.inner.dst_head.clone();
1067 let auto_resolved = wrapped.inner.auto_resolved.clone();
1068
1069 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
1072
1073 for outcome in &auto_resolved {
1075 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
1076 entries.insert(sig_id.clone(), stage_id.clone());
1077 }
1078 }
1079
1080 let resolved = match wrapped.inner.commit() {
1082 Ok(r) => r,
1083 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
1084 return error_with_detail(
1088 422,
1089 "conflicts remaining",
1090 serde_json::json!({"unresolved": ids}),
1091 );
1092 }
1093 };
1094
1095 for (conflict_id, resolution) in resolved {
1096 match resolution {
1097 lex_vcs::Resolution::TakeOurs => {
1098 }
1100 lex_vcs::Resolution::TakeTheirs => {
1101 match resolve_take_theirs(state, &src_head, &conflict_id) {
1111 Ok(stage_id) => {
1112 entries.insert(conflict_id.clone(), stage_id);
1113 }
1114 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1115 }
1116 }
1117 lex_vcs::Resolution::Custom { op } => {
1118 match op.kind.merge_target() {
1127 Some((sig, stage)) => {
1128 if sig != conflict_id {
1129 return error_with_detail(
1130 422,
1131 "custom op targets a different sig than the conflict",
1132 serde_json::json!({
1133 "conflict_id": conflict_id,
1134 "op_targets": sig,
1135 }),
1136 );
1137 }
1138 entries.insert(conflict_id, stage);
1139 }
1140 None => {
1141 return error_with_detail(
1142 422,
1143 "custom op kind doesn't yield a single sig→stage delta",
1144 serde_json::json!({
1145 "conflict_id": conflict_id,
1146 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1147 }),
1148 );
1149 }
1150 }
1151 }
1152 lex_vcs::Resolution::Defer => {
1153 return error_response(500, "internal: Defer slipped past commit gate");
1155 }
1156 }
1157 }
1158
1159 let resolved_count = entries.len();
1160 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1161 if let Some(d) = dst_head { parents.push(d); }
1162 if let Some(s) = src_head { parents.push(s); }
1163 let op = lex_vcs::Operation::new(
1164 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1165 parents,
1166 );
1167 let transition = lex_vcs::StageTransition::Merge { entries };
1168 let store = state.store.lock().unwrap();
1169 match store.apply_merge_op_gated(&dst_branch, op, transition) {
1172 Ok(new_head_op) => json_response(200, &serde_json::json!({
1173 "new_head_op": new_head_op,
1174 "dst_branch": dst_branch,
1175 })),
1176 Err(lex_store::StoreError::TypeError(errs)) => error_with_detail(
1177 422, "merged program has type errors", serde_json::to_value(&errs).unwrap_or_default()),
1178 Err(e) => write_error_response("apply merge op", e),
1179 }
1180}
1181
1182fn resolve_take_theirs(
1187 state: &State,
1188 src_head: &Option<lex_vcs::OpId>,
1189 sig: &lex_vcs::SigId,
1190) -> std::io::Result<Option<lex_vcs::StageId>> {
1191 let store = state.store.lock().unwrap();
1192 let log = lex_vcs::OpLog::open(store.root())?;
1193 let Some(head) = src_head.as_ref() else { return Ok(None); };
1194 let mut current: Option<lex_vcs::StageId> = None;
1197 for record in log.walk_forward(head, None)? {
1198 match &record.produces {
1199 lex_vcs::StageTransition::Create { sig_id, stage_id }
1200 if sig_id == sig => { current = Some(stage_id.clone()); }
1201 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1202 if sig_id == sig => { current = Some(to.clone()); }
1203 lex_vcs::StageTransition::Remove { sig_id, .. }
1204 if sig_id == sig => { current = None; }
1205 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1206 if from == sig || to == sig => {
1207 if from == sig { current = None; }
1208 if to == sig { current = Some(body_stage_id.clone()); }
1209 }
1210 lex_vcs::StageTransition::Merge { entries } => {
1211 if let Some(opt) = entries.get(sig) {
1212 current = opt.clone();
1213 }
1214 }
1215 _ => {}
1216 }
1217 }
1218 Ok(current)
1219}
1220
1221fn mint_merge_id() -> MergeSessionId {
1222 use std::sync::atomic::{AtomicU64, Ordering};
1223 static COUNTER: AtomicU64 = AtomicU64::new(0);
1224 let nanos = SystemTime::now()
1225 .duration_since(UNIX_EPOCH)
1226 .map(|d| d.as_nanos())
1227 .unwrap_or(0);
1228 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1229 format!("merge_{nanos:x}_{n:x}")
1230}
1231
1232pub(crate) fn ops_batch_handler(state: &State, body: &str)
1263 -> Response<std::io::Cursor<Vec<u8>>>
1264{
1265 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1266 Ok(r) => r,
1267 Err(e) => return error_response(400,
1268 format!("body must be a JSON array of OperationRecord: {e}")),
1269 };
1270 let store = state.store.lock().unwrap();
1271 let log = match lex_vcs::OpLog::open(store.root()) {
1272 Ok(l) => l,
1273 Err(e) => return error_response(500, format!("opening op log: {e}")),
1274 };
1275
1276 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1284 std::collections::BTreeSet::new();
1285 for rec in &records {
1286 let expected = rec.op.op_id();
1287 if expected != rec.op_id {
1288 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1289 "supplied": rec.op_id,
1290 "expected": expected,
1291 }));
1292 }
1293 for parent in &rec.op.parents {
1294 let known = match log.get(parent) {
1295 Ok(Some(_)) => true,
1296 Ok(None) => false,
1297 Err(e) => return error_response(500, format!("op log read: {e}")),
1298 };
1299 if !known && !batch_ids.contains(parent) {
1300 return error_with_detail(422, "MissingParent", serde_json::json!({
1301 "op_id": rec.op_id,
1302 "missing_parent": parent,
1303 }));
1304 }
1305 }
1306 batch_ids.insert(rec.op_id.clone());
1307 }
1308
1309 let mut added = 0usize;
1312 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1313 for rec in &records {
1314 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1315 match log.put(rec) {
1316 Ok(()) => {
1317 if !already_present {
1318 added += 1;
1319 added_ids.push(&rec.op_id);
1320 }
1321 }
1322 Err(e) => return error_response(500, format!("op log write: {e}")),
1323 }
1324 }
1325
1326 json_response(200, &serde_json::json!({
1327 "received": records.len(),
1328 "added": added,
1329 "skipped": records.len() - added,
1330 "added_ids": added_ids,
1331 }))
1332}
1333
1334pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1356 -> Response<std::io::Cursor<Vec<u8>>>
1357{
1358 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1359 Ok(a) => a,
1360 Err(e) => return error_response(400,
1361 format!("body must be a JSON array of Attestation: {e}")),
1362 };
1363 let store = state.store.lock().unwrap();
1364 let log = match store.attestation_log() {
1365 Ok(l) => l,
1366 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1367 };
1368 let op_log = match lex_vcs::OpLog::open(store.root()) {
1369 Ok(l) => l,
1370 Err(e) => return error_response(500, format!("opening op log: {e}")),
1371 };
1372
1373 for att in &attestations {
1375 let expected = lex_vcs::Attestation::with_timestamp(
1378 att.stage_id.clone(),
1379 att.op_id.clone(),
1380 att.intent_id.clone(),
1381 att.kind.clone(),
1382 att.result.clone(),
1383 att.produced_by.clone(),
1384 att.cost.clone(),
1385 att.timestamp,
1386 ).attestation_id;
1387 if expected != att.attestation_id {
1388 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1389 "supplied": att.attestation_id,
1390 "expected": expected,
1391 }));
1392 }
1393 if let Some(op_id) = &att.op_id {
1397 match op_log.get(op_id) {
1398 Ok(Some(_)) => {}
1399 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1400 "attestation_id": att.attestation_id,
1401 "op_id": op_id,
1402 })),
1403 Err(e) => return error_response(500, format!("op log read: {e}")),
1404 }
1405 }
1406 }
1407
1408 let mut added = 0usize;
1412 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1413 for att in &attestations {
1414 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1415 match log.put(att) {
1416 Ok(()) => {
1417 if !already_present {
1418 added += 1;
1419 added_ids.push(&att.attestation_id);
1420 }
1421 }
1422 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1423 }
1424 }
1425
1426 json_response(200, &serde_json::json!({
1427 "received": attestations.len(),
1428 "added": added,
1429 "skipped": attestations.len() - added,
1430 "added_ids": added_ids,
1431 }))
1432}
1433
1434pub(crate) fn ops_since_handler(state: &State, query: &str)
1466 -> Response<std::io::Cursor<Vec<u8>>>
1467{
1468 let mut after: Option<String> = None;
1469 let mut branch = String::from("main");
1470 let mut limit: Option<usize> = None;
1471 for kv in query.split('&') {
1472 let Some((k, v)) = kv.split_once('=') else { continue };
1473 match k {
1474 "after" => after = Some(v.to_string()),
1475 "branch" => branch = v.to_string(),
1476 "limit" => {
1477 limit = Some(match v.parse::<usize>() {
1478 Ok(n) => n,
1479 Err(_) => return error_response(400,
1480 format!("limit must be a positive integer, got `{v}`")),
1481 });
1482 }
1483 _ => {}
1484 }
1485 }
1486
1487 let store = state.store.lock().unwrap();
1488 let log = match lex_vcs::OpLog::open(store.root()) {
1489 Ok(l) => l,
1490 Err(e) => return error_response(500, format!("opening op log: {e}")),
1491 };
1492 let head = match store.get_branch(&branch) {
1493 Ok(Some(b)) => b.head_op,
1494 Ok(None) => None,
1495 Err(e) => return error_response(500, format!("get_branch: {e}")),
1496 };
1497 let Some(head) = head else {
1498 return json_response(200, &serde_json::json!([]));
1499 };
1500
1501 let ops_since = match log.ops_since(&head, after.as_ref()) {
1502 Ok(o) => o,
1503 Err(e) => return error_response(500, format!("ops_since: {e}")),
1504 };
1505 let mut ops = ops_since;
1509 ops.reverse();
1510 if let Some(n) = limit {
1511 ops.truncate(n);
1512 }
1513
1514 json_response(200, &serde_json::to_value(&ops).unwrap_or_default())
1515}
1516
1517pub(crate) fn attestations_since_handler(state: &State, query: &str)
1530 -> Response<std::io::Cursor<Vec<u8>>>
1531{
1532 let mut after_op: Option<String> = None;
1533 let mut limit: Option<usize> = None;
1534 for kv in query.split('&') {
1535 let Some((k, v)) = kv.split_once('=') else { continue };
1536 match k {
1537 "after-op" => after_op = Some(v.to_string()),
1538 "limit" => {
1539 limit = Some(match v.parse::<usize>() {
1540 Ok(n) => n,
1541 Err(_) => return error_response(400,
1542 format!("limit must be a positive integer, got `{v}`")),
1543 });
1544 }
1545 _ => {}
1546 }
1547 }
1548
1549 let store = state.store.lock().unwrap();
1550 let log = match store.attestation_log() {
1551 Ok(l) => l,
1552 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1553 };
1554
1555 let exclude: std::collections::BTreeSet<String> = match &after_op {
1559 None => std::collections::BTreeSet::new(),
1560 Some(cutoff) => {
1561 let op_log = match lex_vcs::OpLog::open(store.root()) {
1562 Ok(l) => l,
1563 Err(e) => return error_response(500, format!("opening op log: {e}")),
1564 };
1565 match op_log.walk_back(cutoff, None) {
1566 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1567 Err(_) => {
1568 std::collections::BTreeSet::new()
1572 }
1573 }
1574 }
1575 };
1576
1577 let all = match log.list_all() {
1578 Ok(v) => v,
1579 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1580 };
1581 let mut filtered: Vec<lex_vcs::Attestation> = all
1582 .into_iter()
1583 .filter(|a| match &a.op_id {
1584 Some(op_id) => !exclude.contains(op_id),
1585 None => true,
1589 })
1590 .collect();
1591 filtered.sort_by(|a, b| {
1595 a.timestamp.cmp(&b.timestamp)
1596 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1597 });
1598 if let Some(n) = limit {
1599 filtered.truncate(n);
1600 }
1601
1602 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1603}
1604
1605#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1609struct PkgRecord {
1610 name: String,
1611 version: String,
1612 head_op: Option<String>,
1613 published_at: u64,
1614 function_names: Vec<String>,
1616 #[serde(default)]
1621 dependencies: Vec<String>,
1622 ops: Vec<serde_json::Value>,
1624}
1625
1626#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
1633#[serde(rename_all = "lowercase")]
1634pub enum Visibility {
1635 #[default]
1636 Private,
1637 Public,
1638}
1639
1640#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1645struct PkgIndex {
1646 latest: Option<String>,
1648 versions: Vec<PkgVersionSummary>,
1650 #[serde(default)]
1654 visibility: Visibility,
1655}
1656
1657#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1658struct PkgVersionSummary {
1659 version: String,
1660 head_op: Option<String>,
1661 published_at: u64,
1662}
1663
1664fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1665 root.join("packages").join(name)
1666}
1667
1668fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
1669 pkg_name_dir(root, name).join("index.json")
1670}
1671
1672fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1673 pkg_name_dir(root, name).join(format!("{version}.json"))
1674}
1675
1676fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1677 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
1678}
1679
1680fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
1681 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
1682 serde_json::from_slice(&bytes).ok()
1683}
1684
1685fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
1686 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
1687 serde_json::from_slice(&bytes).ok()
1688}
1689
1690fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1691 let index = load_pkg_index(root, name)?;
1692 let latest = index.latest.clone()?;
1693 load_pkg_record(root, name, &latest)
1694}
1695
1696fn pkg_is_public(root: &std::path::Path, name: &str) -> bool {
1700 load_pkg_index(root, name).map(|i| i.visibility) == Some(Visibility::Public)
1701}
1702
1703fn valid_pkg_segment(s: &str) -> bool {
1708 !s.is_empty()
1709 && s.len() <= 128
1710 && s != "."
1711 && s != ".."
1712 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
1713}
1714
1715#[derive(Deserialize)]
1716struct VisibilityReq {
1717 visibility: Visibility,
1718}
1719
1720fn pkg_set_visibility_handler(
1727 state: &State,
1728 name: &str,
1729 body: &str,
1730) -> Response<std::io::Cursor<Vec<u8>>> {
1731 if !valid_pkg_segment(name) {
1732 return error_response(400, format!("invalid package name {name:?}"));
1733 }
1734 let req: VisibilityReq = match serde_json::from_str(body) {
1735 Ok(r) => r,
1736 Err(e) => return error_response(400, format!("bad request: {e}")),
1737 };
1738 let mut index = match load_pkg_index(&state.root, name) {
1739 Some(i) => i,
1740 None => return error_response(404, format!("package {name:?} not found")),
1741 };
1742 index.visibility = req.visibility;
1743 let bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1744 match std::fs::write(pkg_index_path(&state.root, name), bytes) {
1745 Ok(()) => json_response(
1746 200,
1747 &serde_json::json!({ "name": name, "visibility": index.visibility }),
1748 ),
1749 Err(e) => error_response(500, format!("write index: {e}")),
1750 }
1751}
1752
1753#[derive(serde::Deserialize)]
1754struct ReleaseReq {
1755 version: String,
1756 #[serde(default)]
1757 branch: Option<String>,
1758 #[serde(default)]
1761 dependencies: Vec<String>,
1762}
1763
1764fn pkg_release_handler(state: &State, name: &str, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1773 if !valid_pkg_segment(name) {
1774 return error_response(400, format!("invalid package name {name:?}"));
1775 }
1776 let req: ReleaseReq = match serde_json::from_str(body) {
1777 Ok(r) => r,
1778 Err(e) => return error_response(400, format!("bad request: {e}")),
1779 };
1780 let version = req.version.trim().to_string();
1781 if version.is_empty() || !valid_pkg_segment(&version) {
1782 return error_response(400, "version must be a non-empty, path-safe string (e.g. 1.2.0)");
1783 }
1784 if load_pkg_record(&state.root, name, &version).is_some() {
1786 return error_response(
1787 409,
1788 format!("{name}@{version} already released; releases are immutable — bump the version"),
1789 );
1790 }
1791
1792 let store = state.store.lock().unwrap();
1793 let branch = req.branch.unwrap_or_else(|| store.current_branch());
1794 let head_op = match store.get_branch(&branch) {
1795 Ok(Some(b)) => b.head_op,
1796 Ok(None) => return error_response(404, format!("unknown branch {branch:?}")),
1797 Err(e) => return error_response(500, format!("get_branch: {e}")),
1798 };
1799 let Some(head_op) = head_op else {
1800 return error_response(400, format!("branch {branch:?} has no commits to release"));
1801 };
1802
1803 let predecessor = load_pkg_index(&state.root, name)
1814 .map(|i| i.versions)
1815 .unwrap_or_default()
1816 .into_iter()
1817 .filter_map(|v| lex_syntax::semver::parse_exact(&v.version).map(|p| (p, v)))
1818 .filter(|(p, _)| lex_syntax::semver::parse_exact(&version).map(|n| *p < n).unwrap_or(false))
1819 .max_by_key(|(p, _)| *p)
1820 .map(|(_, v)| v);
1821 if let Some(prev) = predecessor {
1822 if let (Some(prev_head), Some(declared)) = (
1823 prev.head_op.clone(),
1824 lex_syntax::semver::bump_between(&prev.version, &version),
1825 ) {
1826 if let (Ok(prev_api), Ok(new_api)) = (
1827 lex_store::api::public_api_at_op(&store, &prev_head),
1828 lex_store::api::public_api_at_op(&store, &head_op),
1829 ) {
1830 use lex_store::api::ApiChange;
1831 use lex_syntax::semver::Bump;
1832 let (required, why) = match lex_store::api::classify_api_change(&prev_api, &new_api) {
1833 ApiChange::Breaking(d) => (Bump::Major, d),
1834 ApiChange::Additive(d) => (Bump::Minor, d),
1835 ApiChange::None => (Bump::Patch, String::new()),
1836 };
1837 if declared < required {
1838 let need = match required {
1839 Bump::Major => "major",
1840 Bump::Minor => "minor",
1841 Bump::Patch => "patch",
1842 };
1843 return error_response(
1844 422,
1845 format!(
1846 "version bump too small: {} → {version} is a {declared:?} bump, \
1847 but the API change ({why}) requires a {need} bump",
1848 prev.version
1849 ),
1850 );
1851 }
1852 }
1853 }
1854 }
1855
1856 let head = store.branch_head(&branch).unwrap_or_default();
1859 let pairs: Vec<(String, String)> = head.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
1860 let function_names: Vec<String> = store
1861 .get_asts_for_sigs_bulk(&pairs)
1862 .into_iter()
1863 .filter_map(|r| r.ok())
1864 .filter_map(|s| match s {
1865 lex_ast::Stage::FnDecl(fd) => Some(fd.name),
1866 _ => None,
1867 })
1868 .collect();
1869 let mut deps: std::collections::BTreeSet<String> = req.dependencies.into_iter().collect();
1873 if let Ok(extracted) = lex_store::api::external_dependencies_at_op(&store, &head_op) {
1874 deps.extend(extracted);
1875 }
1876 let dependencies: Vec<String> = deps.into_iter().collect();
1877 drop(store);
1878
1879 let published_at = std::time::SystemTime::now()
1880 .duration_since(std::time::UNIX_EPOCH)
1881 .map(|d| d.as_secs())
1882 .unwrap_or(0);
1883 let record = PkgRecord {
1884 name: name.to_string(),
1885 version: version.clone(),
1886 head_op: Some(head_op.clone()),
1887 published_at,
1888 function_names,
1889 dependencies,
1890 ops: Vec::new(),
1891 };
1892 if let Err(e) = save_pkg_record(&state.root, &record, None) {
1893 return error_response(500, format!("write release: {e}"));
1894 }
1895 json_response(
1896 201,
1897 &serde_json::json!({
1898 "name": name,
1899 "version": version,
1900 "head_op": head_op,
1901 "branch": branch,
1902 }),
1903 )
1904}
1905
1906fn public_pkg_names(root: &std::path::Path) -> Vec<String> {
1909 list_pkg_names(root)
1910 .into_iter()
1911 .filter(|name| pkg_is_public(root, name))
1912 .collect()
1913}
1914
1915fn public_pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
1919 let packages: Vec<serde_json::Value> = public_pkg_names(&state.root)
1920 .iter()
1921 .filter_map(|name| {
1922 let r = load_latest_pkg_record(&state.root, name)?;
1923 Some(serde_json::json!({
1924 "name": r.name,
1925 "version": r.version,
1926 "head_op": r.head_op,
1927 "published_at": r.published_at,
1928 }))
1929 })
1930 .collect();
1931 json_response(200, &serde_json::json!({ "packages": packages }))
1932}
1933
1934#[derive(Debug, PartialEq, Eq)]
1939enum PublicTarget {
1940 List,
1941 Latest(String),
1942 Versions(String),
1943 ApiDiff(String),
1944 Head(String),
1945 Version(String, String),
1946 Archive(String, String),
1947}
1948
1949impl PublicTarget {
1950 fn pkg_name(&self) -> Option<&str> {
1952 match self {
1953 PublicTarget::List => None,
1954 PublicTarget::Latest(n)
1955 | PublicTarget::Versions(n)
1956 | PublicTarget::ApiDiff(n)
1957 | PublicTarget::Head(n)
1958 | PublicTarget::Version(n, _)
1959 | PublicTarget::Archive(n, _) => Some(n),
1960 }
1961 }
1962}
1963
1964fn resolve_public(method: &Method, path: &str) -> Result<PublicTarget, u16> {
1969 if !matches!(method, Method::Get) {
1970 return Err(405);
1971 }
1972 let rest = path.trim_matches('/');
1973 if rest.is_empty() {
1974 return Ok(PublicTarget::List);
1975 }
1976 let segs: Vec<&str> = rest.split('/').collect();
1977 if !segs.iter().all(|s| valid_pkg_segment(s)) {
1978 return Err(404);
1979 }
1980 match segs.as_slice() {
1981 [n] => Ok(PublicTarget::Latest(n.to_string())),
1982 [n, "versions"] => Ok(PublicTarget::Versions(n.to_string())),
1983 [n, "api-diff"] => Ok(PublicTarget::ApiDiff(n.to_string())),
1984 [n, "head"] => Ok(PublicTarget::Head(n.to_string())),
1985 [n, v, "archive"] => Ok(PublicTarget::Archive(n.to_string(), v.to_string())),
1986 [n, v] => Ok(PublicTarget::Version(n.to_string(), v.to_string())),
1987 _ => Err(404),
1988 }
1989}
1990
1991pub fn route_public(
2002 state: &State,
2003 method: &Method,
2004 path: &str,
2005 query: &str,
2006) -> Response<std::io::Cursor<Vec<u8>>> {
2007 let target = match resolve_public(method, path) {
2008 Ok(t) => t,
2009 Err(405) => return error_response(405, "public read is GET-only"),
2010 Err(_) => return error_response(404, "not found"),
2011 };
2012 if let PublicTarget::List = target {
2014 return public_pkg_list_handler(state);
2015 }
2016 if let Some(name) = target.pkg_name() {
2018 if !pkg_is_public(&state.root, name) {
2019 return error_response(404, format!("package {name:?} not found"));
2020 }
2021 }
2022 match target {
2023 PublicTarget::List => unreachable!("handled above"),
2024 PublicTarget::Latest(n) => pkg_get_handler(state, &n),
2025 PublicTarget::Versions(n) => pkg_versions_handler(state, &n),
2026 PublicTarget::ApiDiff(n) => pkg_api_diff_handler(state, &n, query),
2027 PublicTarget::Head(n) => pkg_head_handler(state, &n),
2028 PublicTarget::Version(n, v) => pkg_get_version_handler(state, &n, &v),
2029 PublicTarget::Archive(n, v) => pkg_archive_handler(state, &n, &v),
2030 }
2031}
2032
2033fn save_pkg_record(
2034 root: &std::path::Path,
2035 record: &PkgRecord,
2036 archive: Option<&[u8]>,
2039) -> std::io::Result<()> {
2040 let dir = pkg_name_dir(root, &record.name);
2041 std::fs::create_dir_all(&dir)?;
2042
2043 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
2045 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
2046
2047 if let Some(archive) = archive {
2049 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
2050 }
2051
2052 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
2054 index.latest = Some(record.version.clone());
2055 if !index.versions.iter().any(|v| v.version == record.version) {
2056 index.versions.push(PkgVersionSummary {
2057 version: record.version.clone(),
2058 head_op: record.head_op.clone(),
2059 published_at: record.published_at,
2060 });
2061 }
2062 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
2063 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
2064}
2065
2066fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
2067 let dir = root.join("packages");
2068 let Ok(entries) = std::fs::read_dir(&dir) else {
2069 return Vec::new();
2070 };
2071 let mut names: Vec<String> = entries
2072 .filter_map(|e| e.ok())
2073 .filter(|e| e.path().is_dir())
2074 .filter_map(|e| e.file_name().into_string().ok())
2075 .collect();
2076 names.sort();
2077 names
2078}
2079
2080fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
2081 let Ok(entries) = std::fs::read_dir(dir) else { return };
2082 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
2083 entries.sort_by_key(|e| e.path());
2084 for entry in entries {
2085 let path = entry.path();
2086 if path.is_dir() {
2087 collect_lex_files(&path, out);
2088 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
2089 out.push(path);
2090 }
2091 }
2092}
2093
2094fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
2097 let tmp = match tempfile::TempDir::new() {
2098 Ok(t) => t,
2099 Err(e) => return error_response(500, format!("create temp dir: {e}")),
2100 };
2101 {
2102 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
2103 let mut ar = tar::Archive::new(gz);
2104 if let Err(e) = ar.unpack(tmp.path()) {
2105 return error_response(400, format!("unpack archive: {e}"));
2106 }
2107 }
2108
2109 let toml_path = tmp.path().join("lex.toml");
2110 if !toml_path.exists() {
2111 return error_response(400, "archive must contain lex.toml at root");
2112 }
2113 let manifest = match Manifest::load(&toml_path) {
2114 Ok(m) => m,
2115 Err(e) => return error_response(400, format!("lex.toml: {e}")),
2116 };
2117 let (pkg_name, pkg_version) = match &manifest.package {
2118 Some(m) => (m.name.clone(), m.version.clone()),
2119 None => return error_response(400, "lex.toml must have a [package] section"),
2120 };
2121
2122 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
2126 return error_response(
2127 409,
2128 format!(
2129 "package {pkg_name}@{pkg_version} already published; \
2130 bump the version in lex.toml to publish a new release"
2131 ),
2132 );
2133 }
2134
2135 let src_dir = tmp.path().join("src");
2136 if !src_dir.exists() {
2137 return error_response(400, "archive must contain a src/ directory");
2138 }
2139 let mut lex_files: Vec<PathBuf> = Vec::new();
2140 collect_lex_files(&src_dir, &mut lex_files);
2141 if lex_files.is_empty() {
2142 return error_response(400, "no .lex files found in src/");
2143 }
2144
2145 let store = state.store.lock().unwrap();
2146 let branch = store.current_branch();
2147
2148 let old_head = match store.branch_head(&branch) {
2168 Ok(h) => h,
2169 Err(e) => return error_response(500, format!("branch_head: {e}")),
2170 };
2171 let old_pairs: Vec<(String, String)> =
2178 old_head.iter().map(|(sig, stage)| (sig.clone(), stage.clone())).collect();
2179 let mut old_fns_by_name: BTreeMap<String, Vec<lex_ast::FnDecl>> = BTreeMap::new();
2180 for fd in store.get_asts_for_sigs_bulk(&old_pairs)
2181 .into_iter()
2182 .filter_map(|r| r.ok())
2183 .filter_map(|s| match s { lex_ast::Stage::FnDecl(fd) => Some(fd), _ => None })
2184 {
2185 old_fns_by_name.entry(fd.name.clone()).or_default().push(fd);
2186 }
2187 let mut old_types_by_name: BTreeMap<String, lex_ast::TypeDecl> = BTreeMap::new();
2190 for td in store.get_asts_for_sigs_bulk(&old_pairs)
2191 .into_iter()
2192 .filter_map(|r| r.ok())
2193 .filter_map(|s| match s { lex_ast::Stage::TypeDecl(td) => Some(td), _ => None })
2194 {
2195 old_types_by_name.insert(td.name.clone(), td);
2196 }
2197 fn structural_key(fd: &lex_ast::FnDecl) -> Option<String> {
2205 let mut anon = fd.clone();
2206 anon.name = String::new();
2207 lex_ast::sig_id(&lex_ast::Stage::FnDecl(anon))
2208 }
2209
2210 fn take_matching(
2220 map: &mut BTreeMap<String, Vec<lex_ast::FnDecl>>,
2221 name: &str,
2222 new_fd: &lex_ast::FnDecl,
2223 ) -> Option<lex_ast::FnDecl> {
2224 let candidates = map.get_mut(name)?;
2225 let idx = match candidates.len() {
2226 0 => return None,
2227 1 => 0,
2228 _ => {
2229 let want = structural_key(new_fd);
2230 candidates.iter().position(|c| structural_key(c) == want)?
2231 }
2232 };
2233 let matched = candidates.remove(idx);
2234 if candidates.is_empty() {
2235 map.remove(name);
2236 }
2237 Some(matched)
2238 }
2239
2240 let loaded = match load_package(&lex_files, tmp.path(), &pkg_name, true) {
2264 Ok(p) => p,
2265 Err(e) => return error_response(400, format!("load package: {e}")),
2266 };
2267 let mut stages = canonicalize_program(&loaded.program);
2268 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
2274 return error_with_detail(
2275 422,
2276 format!("type errors in package {pkg_name}"),
2277 serde_json::to_value(&errs).unwrap(),
2278 );
2279 }
2280 let new_fns = stage_fns(&stages);
2281 let all_function_names: Vec<String> = new_fns.keys().cloned().collect();
2282
2283 let mut old_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2288 for (name, new_fd) in &new_fns {
2289 if let Some(fd) = take_matching(&mut old_fns_by_name, name, new_fd) {
2290 old_fns.insert(name.clone(), fd);
2291 }
2292 }
2293 let new_types = stage_types(&stages);
2294 let old_types: BTreeMap<String, lex_ast::TypeDecl> = new_types
2298 .keys()
2299 .filter_map(|n| old_types_by_name.get(n).map(|td| (n.clone(), td.clone())))
2300 .collect();
2301 let report =
2302 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
2303
2304 let mut new_imports = lex_vcs::ImportMap::new();
2312 for (file, modules) in &loaded.imports_by_file {
2313 let entry = new_imports.entry(file.clone()).or_default();
2314 for (reference, alias) in modules {
2315 entry.insert(lex_vcs::ImportRef {
2316 reference: reference.clone(),
2317 alias: alias.clone(),
2318 });
2319 }
2320 }
2321
2322 let outcome = match store.publish_program_with_intent(
2325 &branch,
2326 &stages,
2327 &report,
2328 &new_imports,
2329 false,
2330 None,
2331 None,
2332 &loaded.module_prefixes,
2333 ) {
2334 Ok(outcome) => outcome,
2335 Err(lex_store::StoreError::TypeError(errs)) => {
2336 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
2337 }
2338 Err(e) => return write_error_response("publish_program", e),
2339 };
2340 let all_ops: Vec<serde_json::Value> = match serde_json::to_value(&outcome.ops) {
2341 Ok(serde_json::Value::Array(arr)) => arr,
2342 _ => Vec::new(),
2343 };
2344 let final_head_op = outcome.head_op;
2345
2346 let now = SystemTime::now()
2371 .duration_since(UNIX_EPOCH)
2372 .map(|d| d.as_secs())
2373 .unwrap_or(0);
2374 let dependencies: Vec<String> = final_head_op
2377 .as_ref()
2378 .and_then(|h| lex_store::api::external_dependencies_at_op(&store, h).ok())
2379 .unwrap_or_default();
2380 let record = PkgRecord {
2381 name: pkg_name.clone(),
2382 version: pkg_version,
2383 head_op: final_head_op.clone(),
2384 published_at: now,
2385 function_names: all_function_names,
2386 dependencies,
2387 ops: all_ops.clone(),
2388 };
2389 if let Err(e) = save_pkg_record(&state.root, &record, Some(body)) {
2390 return error_response(500, format!("save package index: {e}"));
2391 }
2392
2393 json_response(200, &serde_json::json!({
2394 "package": pkg_name,
2395 "ops": all_ops,
2396 "head_op": final_head_op,
2397 }))
2398}
2399
2400fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2402 let names = list_pkg_names(&state.root);
2403 let packages: Vec<serde_json::Value> = names.iter()
2404 .filter_map(|name| {
2405 let idx = load_pkg_index(&state.root, name)?;
2406 let latest = idx.latest.as_deref()?;
2407 let r = load_pkg_record(&state.root, name, latest)?;
2408 Some(serde_json::json!({
2409 "name": r.name,
2410 "version": r.version,
2411 "head_op": r.head_op,
2412 "published_at": r.published_at,
2413 }))
2414 })
2415 .collect();
2416 json_response(200, &serde_json::json!({ "packages": packages }))
2417}
2418
2419fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2421 match load_latest_pkg_record(&state.root, name) {
2422 Some(r) => json_response(200, &serde_json::json!({
2423 "name": r.name,
2424 "version": r.version,
2425 "head_op": r.head_op,
2426 "published_at": r.published_at,
2427 "function_names": r.function_names,
2428 "ops": r.ops,
2429 })),
2430 None => error_response(404, format!("package {name:?} not found")),
2431 }
2432}
2433
2434fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2436 match load_pkg_index(&state.root, name) {
2437 Some(idx) => json_response(200, &serde_json::json!({
2438 "name": name,
2439 "latest": idx.latest,
2440 "versions": idx.versions,
2441 })),
2442 None => error_response(404, format!("package {name:?} not found")),
2443 }
2444}
2445
2446fn pkg_api_diff_handler(state: &State, name: &str, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2451 let mut from: Option<String> = None;
2452 let mut to: Option<String> = None;
2453 for kv in query.split('&') {
2454 match kv.split_once('=') {
2455 Some(("from", v)) => from = Some(v.to_string()),
2456 Some(("to", v)) => to = Some(v.to_string()),
2457 _ => {}
2458 }
2459 }
2460 let (Some(from), Some(to)) = (from, to) else {
2461 return error_response(400, "api-diff requires ?from=<version>&to=<version>");
2462 };
2463 let head_of = |v: &str| load_pkg_record(&state.root, name, v).and_then(|r| r.head_op);
2464 let (Some(from_head), Some(to_head)) = (head_of(&from), head_of(&to)) else {
2465 return error_response(404, format!("{name}: unknown release in {from}..{to}"));
2466 };
2467
2468 let store = state.store.lock().unwrap();
2469 let (prev_api, new_api) = match (
2470 lex_store::api::public_api_at_op(&store, &from_head),
2471 lex_store::api::public_api_at_op(&store, &to_head),
2472 ) {
2473 (Ok(a), Ok(b)) => (a, b),
2474 _ => return error_response(500, "could not read package APIs for the given releases"),
2475 };
2476 let (change, detail) = match lex_store::api::classify_api_change(&prev_api, &new_api) {
2477 lex_store::api::ApiChange::Breaking(d) => ("breaking", d),
2478 lex_store::api::ApiChange::Additive(d) => ("additive", d),
2479 lex_store::api::ApiChange::None => ("none", String::new()),
2480 };
2481 let renames = lex_store::api::detect_renames(&prev_api, &new_api);
2482 json_response(200, &serde_json::json!({
2483 "name": name, "from": from, "to": to,
2484 "change": change, "detail": detail, "renames": renames,
2485 }))
2486}
2487
2488fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2490 match load_pkg_record(&state.root, name, version) {
2491 Some(r) => json_response(200, &serde_json::json!({
2492 "name": r.name,
2493 "version": r.version,
2494 "head_op": r.head_op,
2495 "published_at": r.published_at,
2496 "function_names": r.function_names,
2497 "dependencies": r.dependencies,
2498 "ops": r.ops,
2499 })),
2500 None => error_response(404, format!("package {name:?}@{version:?} not found")),
2501 }
2502}
2503
2504fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2506 let gzip = |bytes: Vec<u8>| {
2507 Response::from_data(bytes).with_status_code(200).with_header(
2508 tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"application/gzip"[..]).unwrap(),
2509 )
2510 };
2511
2512 if let Ok(bytes) = std::fs::read(pkg_archive_path(&state.root, name, version)) {
2514 return gzip(bytes);
2515 }
2516
2517 if let Some(head_op) = load_pkg_record(&state.root, name, version).and_then(|r| r.head_op) {
2521 match render_op_log_archive(state, name, version, &head_op) {
2522 Ok(bytes) => return gzip(bytes),
2523 Err(e) => {
2524 return error_response(500, format!("rendering archive for {name:?}@{version:?}: {e}"));
2525 }
2526 }
2527 }
2528
2529 error_response(404, format!("archive for {name:?}@{version:?} not found"))
2530}
2531
2532fn render_op_log_archive(
2538 state: &State,
2539 name: &str,
2540 version: &str,
2541 head_op: &str,
2542) -> Result<Vec<u8>, String> {
2543 let files: Vec<(String, String)> = {
2548 let store = state.store.lock().unwrap();
2549 let head = lex_store::render::package_head_at_op(&store, head_op)
2550 .map_err(|e| format!("reading head {head_op}: {e}"))?;
2551 match lex_store::render::render_source(&store, &head)
2552 .map_err(|e| format!("rendering source at {head_op}: {e}"))?
2553 {
2554 lex_store::render::RenderedSource::Single(src) => vec![("src/lib.lex".to_string(), src)],
2555 lex_store::render::RenderedSource::Multi(tree) => tree.into_iter().collect(),
2556 }
2557 };
2558
2559 let manifest = format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\n");
2560 let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2561 {
2562 let mut ar = tar::Builder::new(&mut enc);
2563 let mut append = |p: &str, data: &[u8]| -> std::io::Result<()> {
2564 let mut h = tar::Header::new_gnu();
2565 h.set_size(data.len() as u64);
2566 h.set_mode(0o644);
2567 h.set_cksum();
2568 ar.append_data(&mut h, p, data)
2569 };
2570 append("lex.toml", manifest.as_bytes()).map_err(|e| e.to_string())?;
2571 for (path, src) in &files {
2572 append(path, src.as_bytes()).map_err(|e| e.to_string())?;
2573 }
2574 ar.finish().map_err(|e| e.to_string())?;
2575 }
2576 enc.finish().map_err(|e| e.to_string())
2577}
2578
2579fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2581 match load_latest_pkg_record(&state.root, name) {
2582 Some(r) => json_response(200, &serde_json::json!({
2583 "name": r.name,
2584 "version": r.version,
2585 "head_op": r.head_op,
2586 })),
2587 None => error_response(404, format!("package {name:?} not found")),
2588 }
2589}
2590
2591fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2593 let record = match load_latest_pkg_record(&state.root, name) {
2594 Some(r) => r,
2595 None => return error_response(404, format!("package {name:?} not found")),
2596 };
2597
2598 let store = state.store.lock().unwrap();
2599 let branch = store.current_branch();
2600
2601 let head = match store.branch_head(&branch) {
2602 Ok(h) => h,
2603 Err(e) => return error_response(500, format!("branch_head: {e}")),
2604 };
2605
2606 let head_pairs: Vec<(String, String)> = head
2613 .iter()
2614 .map(|(sig, stage)| (sig.clone(), stage.clone()))
2615 .collect();
2616 let old_fns: BTreeMap<String, lex_ast::FnDecl> = store
2617 .get_asts_for_sigs_bulk(&head_pairs)
2618 .into_iter()
2619 .filter_map(|r| r.ok())
2620 .filter_map(|s| match s {
2621 lex_ast::Stage::FnDecl(fd)
2622 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
2623 _ => None,
2624 })
2625 .collect();
2626
2627 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2628 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2629 let empty_imports = lex_vcs::ImportMap::new();
2630
2631 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
2632 Ok(outcome) => {
2633 let ver = record.version.clone();
2635 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
2636 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
2637 if let Some(mut idx) = load_pkg_index(&state.root, name) {
2639 idx.versions.retain(|v| v.version != ver);
2640 idx.latest = idx.versions.last().map(|v| v.version.clone());
2641 if idx.versions.is_empty() {
2642 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
2643 } else {
2644 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
2645 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
2646 }
2647 }
2648 json_response(200, &serde_json::json!({
2649 "deleted": name,
2650 "version": ver,
2651 "ops": outcome.ops,
2652 "head_op": outcome.head_op,
2653 }))
2654 }
2655 Err(lex_store::StoreError::TypeError(errs)) => {
2656 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
2657 }
2658 Err(e) => write_error_response("retract package", e),
2659 }
2660}
2661
2662#[cfg(test)]
2663mod policy_ceiling_tests {
2664 use super::*;
2665 use lex_runtime::Policy;
2666 use std::path::PathBuf;
2667
2668 fn permissive_request() -> Policy {
2672 Policy {
2673 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
2674 .iter()
2675 .map(|s| s.to_string())
2676 .collect(),
2677 allow_fs_read: vec![PathBuf::from("/")],
2678 allow_fs_write: vec![PathBuf::from("/")],
2679 allow_net_host: Vec::new(),
2680 allow_proc: Vec::new(),
2681 allow_approval: Vec::new(),
2682 budget: None,
2683 }
2684 }
2685
2686 #[test]
2687 fn ceiling_drops_effects_the_caller_was_not_granted() {
2688 let ceiling = Policy {
2689 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
2690 ..Policy::default()
2691 };
2692 let got = clamp_policy(permissive_request(), &ceiling);
2693 assert!(got.allow_effects.contains("io"));
2694 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
2695 assert!(!got.allow_effects.contains("fs_write"));
2696 assert!(!got.allow_effects.contains("net"));
2697 assert!(!got.allow_effects.contains("time"));
2699 }
2700
2701 #[test]
2702 fn ceiling_scopes_override_caller_scopes() {
2703 let ceiling = Policy {
2704 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
2705 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
2706 ..Policy::default()
2707 };
2708 let got = clamp_policy(permissive_request(), &ceiling);
2709 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
2712 assert!(got.allow_fs_write.is_empty());
2713 assert!(got.allow_proc.is_empty());
2714 assert!(got.allow_net_host.is_empty());
2715 }
2716
2717 #[test]
2718 fn ceiling_caps_budget_and_prefers_the_smaller() {
2719 let mut req = permissive_request();
2721 req.budget = None;
2722 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
2723 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
2724
2725 let mut req2 = permissive_request();
2727 req2.budget = Some(50);
2728 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
2729 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
2730 }
2731
2732 #[test]
2733 fn empty_ceiling_is_pure_only() {
2734 let got = clamp_policy(permissive_request(), &Policy::default());
2735 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
2736 assert!(got.allow_proc.is_empty());
2737 assert!(got.allow_fs_write.is_empty());
2738 }
2739}
2740
2741#[cfg(test)]
2742mod public_read_tests {
2743 use super::*;
2744
2745 fn seed_pkg(root: &std::path::Path, name: &str, version: &str) {
2748 let record = PkgRecord {
2749 name: name.to_string(),
2750 version: version.to_string(),
2751 head_op: Some(format!("op-{name}")),
2752 published_at: 1,
2753 function_names: vec![format!("{name}.f")],
2754 dependencies: vec![],
2755 ops: vec![],
2756 };
2757 save_pkg_record(root, &record, Some(format!("ARCHIVE:{name}@{version}").as_bytes()))
2758 .expect("seed package");
2759 }
2760
2761 #[test]
2762 fn new_package_defaults_to_private() {
2763 let tmp = tempfile::TempDir::new().unwrap();
2764 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2765 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2766 assert!(!pkg_is_public(tmp.path(), "does-not-exist"));
2768 }
2769
2770 #[test]
2771 fn set_visibility_round_trips_and_index_persists() {
2772 let tmp = tempfile::TempDir::new().unwrap();
2773 let state = State::open(tmp.path().to_path_buf()).unwrap();
2774 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2775
2776 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"public"}"#);
2777 assert!(pkg_is_public(tmp.path(), "lex-schema"));
2778 let idx = load_pkg_index(tmp.path(), "lex-schema").unwrap();
2780 assert_eq!(idx.latest.as_deref(), Some("0.9.2"));
2781 assert_eq!(idx.versions.len(), 1);
2782
2783 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"private"}"#);
2784 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2785 }
2786
2787 #[test]
2788 fn set_visibility_on_unknown_package_is_a_noop() {
2789 let tmp = tempfile::TempDir::new().unwrap();
2790 let state = State::open(tmp.path().to_path_buf()).unwrap();
2791 let _ = pkg_set_visibility_handler(&state, "ghost", r#"{"visibility":"public"}"#);
2793 assert!(load_pkg_index(tmp.path(), "ghost").is_none());
2794 }
2795
2796 #[test]
2797 fn public_listing_omits_private_packages() {
2798 let tmp = tempfile::TempDir::new().unwrap();
2799 let state = State::open(tmp.path().to_path_buf()).unwrap();
2800 seed_pkg(tmp.path(), "pub-pkg", "1.0.0");
2801 seed_pkg(tmp.path(), "priv-pkg", "1.0.0");
2802 let _ = pkg_set_visibility_handler(&state, "pub-pkg", r#"{"visibility":"public"}"#);
2803
2804 let names = public_pkg_names(tmp.path());
2805 assert_eq!(names, vec!["pub-pkg".to_string()]);
2806 }
2807
2808 #[test]
2809 fn resolve_public_maps_routes() {
2810 let get = Method::Get;
2811 assert_eq!(resolve_public(&get, "").unwrap(), PublicTarget::List);
2812 assert_eq!(resolve_public(&get, "/").unwrap(), PublicTarget::List);
2813 assert_eq!(
2814 resolve_public(&get, "/lex-schema").unwrap(),
2815 PublicTarget::Latest("lex-schema".into())
2816 );
2817 assert_eq!(
2818 resolve_public(&get, "/lex-schema/versions").unwrap(),
2819 PublicTarget::Versions("lex-schema".into())
2820 );
2821 assert_eq!(
2822 resolve_public(&get, "/lex-schema/head").unwrap(),
2823 PublicTarget::Head("lex-schema".into())
2824 );
2825 assert_eq!(
2826 resolve_public(&get, "/lex-schema/0.9.2").unwrap(),
2827 PublicTarget::Version("lex-schema".into(), "0.9.2".into())
2828 );
2829 assert_eq!(
2830 resolve_public(&get, "/lex-schema/0.9.2/archive").unwrap(),
2831 PublicTarget::Archive("lex-schema".into(), "0.9.2".into())
2832 );
2833 }
2834
2835 #[test]
2836 fn resolve_public_rejects_bad_method_and_traversal() {
2837 assert_eq!(resolve_public(&Method::Put, "/lex-schema"), Err(405));
2839 assert_eq!(resolve_public(&Method::Post, "").err(), Some(405));
2840 assert_eq!(resolve_public(&Method::Get, "/.."), Err(404));
2842 assert_eq!(resolve_public(&Method::Get, "/lex-schema/../etc"), Err(404));
2843 assert_eq!(resolve_public(&Method::Get, "/a/b/c/d"), Err(404));
2844 assert!(resolve_public(&Method::Get, "/lex schema").is_err());
2846 }
2847}