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 pub(crate) ops_since: Mutex<crate::ops_since_http::OpsSinceCache>,
79 pub blob_limits: Option<BlobLimits>,
87 pub reserved_producers: Vec<String>,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct BlobLimits {
110 pub max_blob_bytes: u64,
112 pub store_quota_bytes: u64,
114 pub max_manifest_entries: usize,
116}
117
118pub const CAPS: &[&str] = &[CAP_FILES_V1, CAP_INTENT_ORIGIN_V1];
122
123pub const CAP_FILES_V1: &str = "files-v1";
125
126pub const CAP_INTENT_ORIGIN_V1: &str = "intent-origin-v1";
132
133pub(crate) fn has_cap(header: Option<&str>, cap: &str) -> bool {
135 header.is_some_and(|h| h.split(',').any(|c| c.trim().eq_ignore_ascii_case(cap)))
136}
137
138pub struct ApiMergeSession {
145 pub inner: MergeSession,
146 pub src_branch: String,
147 pub dst_branch: String,
148}
149
150impl State {
151 pub fn open(root: PathBuf) -> anyhow::Result<Self> {
152 Self::open_with_ceiling(root, None)
153 }
154
155 pub fn open_with_ceiling(
160 root: PathBuf,
161 policy_ceiling: Option<Policy>,
162 ) -> anyhow::Result<Self> {
163 Ok(Self {
164 store: Mutex::new(Store::open(&root)?),
165 root,
166 sessions: Mutex::new(HashMap::new()),
167 policy_ceiling,
168 ops_since: Mutex::new(Default::default()),
169 blob_limits: None,
170 reserved_producers: Vec::new(),
171 })
172 }
173
174 pub fn with_blob_limits(mut self, limits: Option<BlobLimits>) -> Self {
176 self.blob_limits = limits;
177 self
178 }
179
180 pub fn with_reserved_producers(mut self, tools: Vec<String>) -> Self {
184 self.reserved_producers = tools;
185 self
186 }
187
188 fn reserved_producer_claimed(&self, att: &lex_vcs::Attestation) -> Option<&str> {
190 let claimed = att.produced_by.tool.trim().to_ascii_lowercase();
191 self.reserved_producers.iter().map(String::as_str).find(|entry| {
192 let entry = entry.trim().to_ascii_lowercase();
193 match entry.strip_suffix('*') {
194 Some(prefix) => !prefix.is_empty() && claimed.starts_with(prefix),
195 None => !entry.is_empty() && claimed == entry,
196 }
197 })
198 }
199
200 pub fn new_with_tenant(tenant_id: &str, store_root: PathBuf) -> anyhow::Result<Self> {
210 validate_tenant_id(tenant_id)?;
211 Self::open(store_root.join(tenant_id))
212 }
213
214 pub fn new_with_tenant_and_ceiling(
219 tenant_id: &str,
220 store_root: PathBuf,
221 policy_ceiling: Option<Policy>,
222 ) -> anyhow::Result<Self> {
223 validate_tenant_id(tenant_id)?;
224 Self::open_with_ceiling(store_root.join(tenant_id), policy_ceiling)
225 }
226}
227
228fn clamp_policy(requested: Policy, ceiling: &Policy) -> Policy {
244 let allow_effects: BTreeSet<String> = requested
245 .allow_effects
246 .intersection(&ceiling.allow_effects)
247 .cloned()
248 .collect();
249 let budget = match (requested.budget, ceiling.budget) {
250 (Some(r), Some(c)) => Some(r.min(c)),
251 (None, Some(c)) => Some(c),
252 (Some(r), None) => Some(r),
253 (None, None) => None,
254 };
255 Policy {
256 allow_effects,
257 allow_fs_read: ceiling.allow_fs_read.clone(),
258 allow_fs_write: ceiling.allow_fs_write.clone(),
259 allow_net_host: ceiling.allow_net_host.clone(),
260 allow_proc: ceiling.allow_proc.clone(),
261 allow_approval: ceiling.allow_approval.clone(),
262 budget,
263 }
264}
265
266fn validate_tenant_id(tenant_id: &str) -> anyhow::Result<()> {
267 if tenant_id.is_empty() {
268 anyhow::bail!("tenant_id must not be empty");
269 }
270 if tenant_id.len() > 64 {
271 anyhow::bail!("tenant_id must be at most 64 bytes");
272 }
273 if !tenant_id
274 .bytes()
275 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
276 {
277 anyhow::bail!(
278 "tenant_id {tenant_id:?} contains characters outside [A-Za-z0-9_-]"
279 );
280 }
281 Ok(())
282}
283
284#[derive(Debug, Serialize, Deserialize)]
285struct ErrorEnvelope {
286 error: String,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 detail: Option<serde_json::Value>,
289}
290
291pub(crate) fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
292 let bytes = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
293 Response::from_data(bytes)
294 .with_status_code(status)
295 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
296}
297
298pub(crate) fn error_response(status: u16, msg: impl Into<String>) -> Response<std::io::Cursor<Vec<u8>>> {
299 json_response(status, &serde_json::to_value(ErrorEnvelope {
300 error: msg.into(), detail: None,
301 }).unwrap())
302}
303
304pub(crate) fn error_with_detail(status: u16, msg: impl Into<String>, detail: serde_json::Value)
305 -> Response<std::io::Cursor<Vec<u8>>>
306{
307 json_response(status, &serde_json::to_value(ErrorEnvelope {
308 error: msg.into(), detail: Some(detail),
309 }).unwrap())
310}
311
312pub const UNSATISFIABLE_PAIR_HINT: &str =
315 "republish from source to retire the stranded entry (#995)";
316
317pub(crate) fn unsatisfiable_pair_response(err: &lex_store::StoreError)
330 -> Option<Response<std::io::Cursor<Vec<u8>>>>
331{
332 let lex_store::StoreError::UnsatisfiablePair { sig_id, stage_id, filed_under } = err else {
333 return None;
334 };
335 Some(error_with_detail(422, "UnsatisfiablePair", serde_json::json!({
336 "sig_id": sig_id,
337 "stage_id": stage_id,
338 "filed_under": filed_under,
339 "message": err.to_string(),
340 "hint": UNSATISFIABLE_PAIR_HINT,
341 })))
342}
343
344pub(crate) fn write_error_response(prefix: &str, err: lex_store::StoreError)
350 -> Response<std::io::Cursor<Vec<u8>>>
351{
352 if let Some(resp) = unsatisfiable_pair_response(&err) {
353 return resp;
354 }
355 if let lex_store::StoreError::Contention { branch, attempts } = &err {
356 let body = serde_json::to_vec(&ErrorEnvelope {
357 error: format!("{prefix}: branch '{branch}' is contended (attempts={attempts})"),
358 detail: Some(serde_json::json!({
359 "kind": "contention",
360 "branch": branch,
361 "attempts": attempts,
362 })),
363 }).unwrap_or_else(|_| b"{}".to_vec());
364 return Response::from_data(body)
365 .with_status_code(503)
366 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
367 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"1"[..]).unwrap());
368 }
369 if let lex_store::StoreError::BudgetExceeded { session_id, cap, spent_after } = &err {
377 let body = serde_json::to_vec(&ErrorEnvelope {
378 error: format!(
379 "{prefix}: session `{session_id}` budget exceeded \
380 (spent_after={spent_after}, cap={cap})"
381 ),
382 detail: Some(serde_json::json!({
383 "kind": "budget_exceeded",
384 "session_id": session_id,
385 "cap": cap,
386 "spent_after": spent_after,
387 })),
388 }).unwrap_or_else(|_| b"{}".to_vec());
389 return Response::from_data(body)
390 .with_status_code(503)
391 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
392 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"0"[..]).unwrap());
393 }
394 error_response(500, format!("{prefix}: {err}"))
395}
396
397pub fn handle(state: Arc<State>, mut req: Request) -> std::io::Result<()> {
398 let method = req.method().clone();
399 let url = req.url().to_string();
400 let path = url.split('?').next().unwrap_or("").to_string();
401 let query = url.split_once('?').map(|(_, q)| q.to_string()).unwrap_or_default();
402
403 let x_lex_user = req.headers().iter()
408 .find(|h| h.field.equiv("x-lex-user"))
409 .map(|h| h.value.as_str().to_string());
410 let x_lex_caps = req.headers().iter()
413 .find(|h| h.field.equiv("x-lex-caps"))
414 .map(|h| h.value.as_str().to_string());
415
416 if matches!(method, Method::Post) && path == "/v1/pkg/publish" {
418 let mut body_bytes: Vec<u8> = Vec::new();
419 let _ = req.as_reader().read_to_end(&mut body_bytes);
420 let resp = pkg_publish_handler(&state, &body_bytes);
421 return req.respond(resp);
422 }
423
424 let mut body = String::new();
425 let _ = req.as_reader().read_to_string(&mut body);
426
427 let resp = route(&state, &method, &path, &query, &body, x_lex_user.as_deref(), x_lex_caps.as_deref());
428 req.respond(resp)
429}
430
431pub fn handle_with_auth<F>(state: Arc<State>, req: Request, auth: F) -> std::io::Result<()>
435where
436 F: FnOnce(&str, &[Header]) -> bool,
437{
438 let path = req.url().split('?').next().unwrap_or("").to_string();
439 if !auth(&path, req.headers()) {
440 return req.respond(
441 Response::from_data(br#"{"error":"unauthorized"}"#.to_vec())
442 .with_status_code(401)
443 .with_header(
444 Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
445 ),
446 );
447 }
448 handle(state, req)
449}
450
451fn route(
452 state: &State,
453 method: &Method,
454 path: &str,
455 query: &str,
456 body: &str,
457 x_lex_user: Option<&str>,
458 x_lex_caps: Option<&str>,
459) -> Response<std::io::Cursor<Vec<u8>>> {
460 match (method, path) {
461 (Method::Get, "/") => crate::web::activity_handler(state),
463 (Method::Get, "/web/branches") => crate::web::branches_handler(state),
464 (Method::Get, "/web/trust") => crate::web::trust_handler(state),
465 (Method::Get, "/web/attention") => crate::web::attention_handler(state),
466 (Method::Get, p) if p.starts_with("/web/branch/") => {
467 let name = &p["/web/branch/".len()..];
468 crate::web::branch_handler(state, name)
469 }
470 (Method::Get, p) if p.starts_with("/web/stage/") => {
471 let id = &p["/web/stage/".len()..];
472 crate::web::stage_html_handler(state, id)
473 }
474 (Method::Post, p) if p.starts_with("/web/stage/") && (
479 p.ends_with("/pin") || p.ends_with("/defer")
480 || p.ends_with("/block") || p.ends_with("/unblock")
481 ) => {
482 let prefix_len = "/web/stage/".len();
483 let last_slash = p.rfind('/').unwrap_or(p.len());
484 let id = &p[prefix_len..last_slash];
485 let verb = &p[last_slash + 1..];
486 let decision = match verb {
487 "pin" => crate::web::WebStageDecision::Pin,
488 "defer" => crate::web::WebStageDecision::Defer,
489 "block" => crate::web::WebStageDecision::Block,
490 "unblock" => crate::web::WebStageDecision::Unblock,
491 _ => unreachable!("matched in outer guard"),
492 };
493 crate::web::stage_decision_handler(state, id, body, decision, x_lex_user)
494 }
495 (Method::Get, "/v1/health") => json_response(200, &serde_json::json!({"ok": true, "caps": CAPS})),
497 (Method::Post, "/v1/parse") => parse_handler(body),
498 (Method::Post, "/v1/check") => check_handler(body),
499 (Method::Post, "/v1/publish") => publish_handler(state, body),
500 (Method::Post, "/v1/patch") => patch_handler(state, body),
501 (Method::Post, "/v1/transform") => crate::transform_http::transform_handler(state, body),
502 (Method::Get, p) if p.starts_with("/v1/stage/") => {
503 let suffix = &p["/v1/stage/".len()..];
504 if let Some(id) = suffix.strip_suffix("/attestations") {
507 stage_attestations_handler(state, id)
508 } else {
509 stage_handler(state, suffix)
510 }
511 }
512 (Method::Post, "/v1/run") => run_handler(state, body, false),
513 (Method::Post, "/v1/replay") => run_handler(state, body, true),
514 (Method::Get, p) if p.starts_with("/v1/trace/") => {
515 let id = &p["/v1/trace/".len()..];
516 trace_handler(state, id)
517 }
518 (Method::Get, "/v1/diff") => diff_handler(state, query),
519 (Method::Post, "/v1/merge/start") => merge_start_handler(state, body),
520 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/resolve") => {
521 let id = &p["/v1/merge/".len()..p.len() - "/resolve".len()];
522 merge_resolve_handler(state, id, body)
523 }
524 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/commit") => {
525 let id = &p["/v1/merge/".len()..p.len() - "/commit".len()];
526 merge_commit_handler(state, id)
527 }
528 (Method::Post, "/v1/ops/batch") => ops_batch_handler(state, body),
530 (Method::Post, "/v1/attestations/batch") => attestations_batch_handler(state, body),
531 (Method::Post, "/v1/stages/batch") => crate::sync_http::stages_batch_handler(state, body),
534 (Method::Post, "/v1/stages/fetch") => crate::sync_http::stages_fetch_handler(state, body),
535 (Method::Post, "/v1/stages/missing") => crate::sync_http::stages_missing_handler(state, body),
536 (Method::Post, "/v1/intents/batch") => crate::sync_http::intents_batch_handler(state, body),
537 (Method::Post, "/v1/intents/fetch") => crate::sync_http::intents_fetch_handler(state, body),
538 (Method::Post, "/v1/locks/batch") => crate::sync_http::locks_batch_handler(state, body),
541 (Method::Post, "/v1/locks/fetch") => crate::sync_http::locks_fetch_handler(state, body),
542 (Method::Post, "/v1/blobs/missing") => crate::sync_http::blobs_missing_handler(state, body),
545 (Method::Post, "/v1/blobs/batch") => crate::sync_http::blobs_batch_handler(state, body),
546 (Method::Post, "/v1/blobs/fetch") => crate::sync_http::blobs_fetch_handler(state, body),
547 (Method::Post, "/v1/issues/batch") => crate::sync_http::issues_batch_handler(state, body),
550 (Method::Post, "/v1/issues/fetch") => crate::sync_http::issues_fetch_handler(state, body),
551 (Method::Get, "/v1/issues/list") => crate::sync_http::issues_list_handler(state),
552 (Method::Get, "/v1/issues") => crate::issues_http::issues_state_handler(state),
556 (Method::Get, "/v1/projects") => crate::issues_http::projects_handler(state),
557 (Method::Get, p) if p.starts_with("/v1/issues/") => {
558 crate::issues_http::issue_detail_handler(state, &p["/v1/issues/".len()..])
559 }
560 (Method::Get, "/v1/review/inbox") => crate::review_http::review_inbox_handler(state, query),
564 (Method::Post, "/v1/review/verdict") => crate::review_http::review_verdict_handler(state, body),
565 (Method::Get, "/v1/branches") => crate::branches_http::branches_list_handler(state),
566 (Method::Post, "/v1/branches") => crate::branches_http::branch_create_handler(state, body),
567 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/checkout") => {
568 let name = &p["/v1/branches/".len()..p.len() - "/checkout".len()];
569 crate::branches_http::branch_checkout_handler(state, name)
570 }
571 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
574 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
575 crate::branches_http::branch_head_handler(state, name)
576 }
577 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
578 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
579 crate::branches_http::branch_advance_head_handler(state, name, body)
580 }
581 (Method::Get, "/v1/ops/since") => crate::ops_since_http::ops_since_handler(state, query, x_lex_caps),
585 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
586 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
590 (Method::Put, p) if p.starts_with("/v1/pkg/") && p.ends_with("/visibility") => {
594 let name = &p["/v1/pkg/".len()..p.len() - "/visibility".len()];
595 pkg_set_visibility_handler(state, name, body)
596 }
597 (Method::Post, p) if p.starts_with("/v1/pkg/") && p.ends_with("/release") => {
600 let name = &p["/v1/pkg/".len()..p.len() - "/release".len()];
601 pkg_release_handler(state, name, body)
602 }
603 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
604 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
605 pkg_head_handler(state, name)
606 }
607 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/versions") => {
608 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
609 pkg_versions_handler(state, name)
610 }
611 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/api-diff") => {
612 let name = &p["/v1/pkg/".len()..p.len() - "/api-diff".len()];
613 pkg_api_diff_handler(state, name, query)
614 }
615 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
617 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
618 if let Some((name, version)) = inner.split_once('/') {
620 pkg_archive_handler(state, name, version)
621 } else {
622 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
623 }
624 }
625 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
627 let inner = &p["/v1/pkg/".len()..];
628 if let Some((name, version)) = inner.split_once('/') {
629 pkg_get_version_handler(state, name, version)
630 } else {
631 error_response(400, "expected /v1/pkg/{name}/{version}")
632 }
633 }
634 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
635 let name = &p["/v1/pkg/".len()..];
636 pkg_get_handler(state, name)
637 }
638 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
639 let name = &p["/v1/pkg/".len()..];
640 pkg_delete_handler(state, name)
641 }
642 _ => error_response(404, format!("unknown route: {method:?} {path}")),
643 }
644}
645
646#[derive(Deserialize)]
647struct ParseReq { source: String }
648
649fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
650 let req: ParseReq = match serde_json::from_str(body) {
651 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
652 };
653 match load_program_from_str(&req.source) {
654 Ok(prog) => {
655 let stages = canonicalize_program(&prog);
656 json_response(200, &serde_json::to_value(&stages).unwrap())
657 }
658 Err(e) => error_response(400, format!("syntax error: {e}")),
659 }
660}
661
662pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
663 let req: ParseReq = match serde_json::from_str(body) {
664 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
665 };
666 let prog = match load_program_from_str(&req.source) {
667 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
668 };
669 let stages = canonicalize_program(&prog);
670 match lex_types::check_program(&stages) {
671 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
672 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
673 }
674}
675
676#[derive(Deserialize)]
677struct PublishReq { source: String, #[serde(default)] activate: bool }
678
679pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
680 let req: PublishReq = match serde_json::from_str(body) {
681 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
682 };
683 let prog = match load_program_from_str(&req.source) {
684 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
685 };
686 let mut stages = canonicalize_program(&prog);
690 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
691 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
692 }
693 let example_errors = lex_runtime::evaluate_examples(&stages);
699 if !example_errors.is_empty() {
700 return error_with_detail(422, "example mismatch",
701 serde_json::to_value(&example_errors).unwrap_or_default());
702 }
703
704 let store = state.store.lock().unwrap();
705 let branch = store.current_branch();
706
707 let old_head = match store.branch_head(&branch) {
709 Ok(h) => h,
710 Err(e) => return error_response(500, format!("branch_head: {e}")),
711 };
712 let old_pairs: Vec<(String, String)> =
719 old_head.iter().map(|(sig, stg)| (sig.clone(), stg.clone())).collect();
720 let old_head_stages: Vec<lex_ast::Stage> =
721 store.get_asts_for_sigs_bulk(&old_pairs).into_iter().filter_map(Result::ok).collect();
722 let old_fns = stage_fns(&old_head_stages);
723 let new_fns = stage_fns(&stages);
724 let old_types = stage_types(&old_head_stages);
725 let new_types = stage_types(&stages);
726 let report =
727 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
728
729 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
731 {
732 let entry = new_imports.entry("<source>".into()).or_default();
733 for s in &stages {
734 if let lex_ast::Stage::Import(im) = s {
735 entry.insert(lex_vcs::ImportRef {
736 reference: im.reference.clone(),
737 alias: im.alias.clone(),
738 });
739 }
740 }
741 }
742
743 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
744 Ok(outcome) => {
745 record_examples_for_publish(&store, &stages, &outcome);
749 json_response(200, &serde_json::json!({
750 "ops": outcome.ops,
751 "head_op": outcome.head_op,
752 }))
753 }
754 Err(lex_store::StoreError::TypeError(errs)) => {
762 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
763 }
764 Err(e) => write_error_response("publish_program", e),
765 }
766}
767
768#[derive(Deserialize)]
769struct PatchReq {
770 stage_id: String,
771 patch: lex_ast::Patch,
772 #[serde(default)] activate: bool,
773 #[serde(default)] branch: Option<String>,
777 #[serde(default)] intent: Option<crate::transform_http::IntentSpec>,
779}
780
781fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
789 let req: PatchReq = match serde_json::from_str(body) {
790 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
791 };
792 let intent = match req.intent.clone() {
793 Some(spec) => match spec.into_intent(crate::transform_http::default_http_session) {
794 Ok(i) => Some(i),
795 Err(e) => return error_response(400, format!("bad request: {e}")),
796 },
797 None => None,
798 };
799 let store = state.store.lock().unwrap();
800 let explicit_branch = req.branch.is_some();
801 if let Some(b) = &req.branch {
802 match store.list_branches() {
803 Ok(bs) if bs.iter().any(|x| x == b) => {}
804 Ok(_) => return error_response(404, format!("unknown branch `{b}`")),
805 Err(e) => return error_response(500, format!("list_branches: {e}")),
806 }
807 }
808
809 let original = match store.get_ast(&req.stage_id) {
811 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
812 };
813
814 let patched = match lex_ast::apply_patch(&original, &req.patch) {
816 Ok(s) => s,
817 Err(e) => return error_with_detail(422, "patch failed",
818 serde_json::to_value(&e).unwrap_or_default()),
819 };
820
821 let branch = req.branch.clone().unwrap_or_else(|| store.current_branch());
832
833 let sig = match lex_ast::sig_id(&patched) {
835 Some(s) => s,
836 None => return error_response(500, "patched stage has no sig_id"),
837 };
838
839 let new_id = match store.publish(&patched) {
842 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
843 };
844
845 let original_effects: std::collections::BTreeSet<String> = match &original {
847 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
848 _ => std::collections::BTreeSet::new(),
849 };
850 let patched_effects: std::collections::BTreeSet<String> = match &patched {
851 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
852 _ => std::collections::BTreeSet::new(),
853 };
854 let head_now = match store.get_branch(&branch) {
855 Ok(b) => b.and_then(|b| b.head_op),
856 Err(e) => return error_response(500, format!("get_branch: {e}")),
857 };
858 let kind = if original_effects != patched_effects {
859 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
866 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
867 let to_sig_id = lex_ast::sig_id(&patched).filter(|s| *s != sig);
871 lex_vcs::OperationKind::ChangeEffectSig {
872 sig_id: sig.clone(),
873 from_stage_id: req.stage_id.clone(),
874 to_stage_id: new_id.clone(),
875 from_effects: original_effects,
876 to_effects: patched_effects,
877 from_budget,
878 to_budget,
879 to_sig_id,
880 }
881 } else {
882 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
883 lex_vcs::OperationKind::ModifyBody {
884 sig_id: sig.clone(),
885 from_stage_id: req.stage_id.clone(),
886 to_stage_id: new_id.clone(),
887 from_budget: budget,
888 to_budget: budget,
889 to_sig_id: lex_ast::sig_id(&patched).filter(|s| *s != sig),
892 }
893 };
894 let transition = lex_store::transition_for_kind(&kind);
898 let op = lex_vcs::Operation::new(
899 kind,
900 head_now.into_iter().collect::<Vec<_>>(),
901 );
902 let op = match &intent {
903 Some(i) => op.with_intent(i.intent_id.clone()),
904 None => op,
905 };
906 let op_id = match store.apply_operation_gated_with_intent(&branch, op, transition, intent.as_ref()) {
907 Ok(id) => id,
908 Err(lex_store::StoreError::TypeError(errs)) => return error_with_detail(
909 422, "type errors after patch", serde_json::to_value(&errs).unwrap_or_default()),
910 Err(e) => return write_error_response("apply_operation_gated", e),
911 };
912 if req.activate {
913 if let Err(e) = store.activate(&new_id) {
914 return error_response(500, format!("activate: {e}"));
915 }
916 }
917
918 let status = format!("{:?}",
919 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
920 let mut resp = serde_json::json!({
921 "old_stage_id": req.stage_id,
922 "new_stage_id": new_id,
923 "sig_id": sig,
924 "status": status,
925 "op_id": op_id,
926 });
927 if explicit_branch {
930 resp["branch"] = serde_json::json!(branch);
931 }
932 if let Some(i) = &intent {
933 resp["intent_id"] = serde_json::json!(i.intent_id);
934 }
935 json_response(200, &resp)
936}
937
938pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
939 let store = state.store.lock().unwrap();
940 let meta = match store.get_metadata(id) {
941 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
942 };
943 let ast = match store.get_ast(id) {
944 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
945 };
946 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
947 json_response(200, &serde_json::json!({
948 "metadata": meta,
949 "ast": ast,
950 "status": status,
951 }))
952}
953
954pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
963 let store = state.store.lock().unwrap();
964 if let Err(e) = store.get_metadata(id) {
965 return error_response(404, format!("{e}"));
966 }
967 let log = match store.attestation_log() {
968 Ok(l) => l,
969 Err(e) => return error_response(500, format!("attestation log: {e}")),
970 };
971 let mut listing = match log.list_for_stage_by_arrival(&id.to_string()) {
976 Ok(v) => v,
977 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
978 };
979 listing.reverse();
980 json_response(200, &serde_json::json!({"attestations": listing}))
981}
982
983#[derive(Deserialize, Default)]
984struct PolicyJson {
985 #[serde(default)] allow_effects: Vec<String>,
986 #[serde(default)] allow_fs_read: Vec<String>,
987 #[serde(default)] allow_fs_write: Vec<String>,
988 #[serde(default)] budget: Option<u64>,
989}
990
991impl PolicyJson {
992 fn into_policy(self) -> Policy {
993 Policy {
994 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
995 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
996 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
997 allow_net_host: Vec::new(),
998 allow_proc: Vec::new(),
999 allow_approval: Vec::new(),
1000 budget: self.budget,
1001 }
1002 }
1003}
1004
1005#[derive(Deserialize)]
1006struct RunReq {
1007 source: String,
1008 #[serde(rename = "fn")] func: String,
1009 #[serde(default)] args: Vec<serde_json::Value>,
1010 #[serde(default)] policy: PolicyJson,
1011 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
1012}
1013
1014pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
1015 let req: RunReq = match serde_json::from_str(body) {
1016 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
1017 };
1018 let prog = match load_program_from_str(&req.source) {
1019 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
1020 };
1021 let stages = canonicalize_program(&prog);
1022 if let Err(errs) = lex_types::check_program(&stages) {
1023 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
1024 }
1025 let bc = compile_program(&stages);
1026 let mut policy = req.policy.into_policy();
1027 if let Some(ceiling) = &state.policy_ceiling {
1033 policy = clamp_policy(policy, ceiling);
1034 }
1035 if let Err(violations) = check_policy(&bc, &policy) {
1036 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
1037 }
1038
1039 let mut recorder = lex_trace::Recorder::new();
1040 if with_overrides && !req.overrides.is_empty() {
1041 recorder = recorder.with_overrides(req.overrides);
1042 }
1043 let handle = recorder.handle();
1044 let handler = DefaultHandler::new(policy);
1045 let mut vm = Vm::with_handler(&bc, Box::new(handler));
1046 vm.set_tracer(Box::new(recorder));
1047
1048 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
1049 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
1050 let result = vm.call(&req.func, vargs);
1051 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
1052
1053 let store = state.store.lock().unwrap();
1054 let (root_out, root_err, status) = match &result {
1055 Ok(v) => (Some(value_to_json(v)), None, 200u16),
1056 Err(e) => (None, Some(format!("{e}")), 200u16),
1057 };
1058 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
1059 root_out.clone(), root_err.clone(), started, ended);
1060 let run_id = match store.save_trace(&tree) {
1061 Ok(id) => id,
1062 Err(e) => return error_response(500, format!("save_trace: {e}")),
1063 };
1064
1065 let mut body = serde_json::json!({
1066 "run_id": run_id,
1067 "output": root_out,
1068 });
1069 if let Some(err) = root_err {
1070 body["error"] = serde_json::Value::String(err);
1071 }
1072 json_response(status, &body)
1073}
1074
1075fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1076 let store = state.store.lock().unwrap();
1077 match store.load_trace(id) {
1078 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
1079 Err(e) => error_response(404, format!("{e}")),
1080 }
1081}
1082
1083fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1084 let mut a = None;
1085 let mut b = None;
1086 for kv in query.split('&') {
1087 if let Some((k, v)) = kv.split_once('=') {
1088 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
1089 }
1090 }
1091 let (Some(a), Some(b)) = (a, b) else {
1092 return error_response(400, "missing a or b query params");
1093 };
1094 let store = state.store.lock().unwrap();
1095 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
1096 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
1097 match lex_trace::diff_runs(&ta, &tb) {
1098 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
1099 None => json_response(200, &serde_json::json!({"divergence": null})),
1100 }
1101}
1102
1103fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
1104
1105fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
1106
1107#[derive(Deserialize)]
1108struct MergeStartReq {
1109 src_branch: String,
1110 dst_branch: String,
1111}
1112
1113fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
1124 let req: MergeStartReq = match serde_json::from_str(body) {
1125 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
1126 };
1127 let store = state.store.lock().unwrap();
1128 let src_head = match store.get_branch(&req.src_branch) {
1129 Ok(Some(b)) => b.head_op,
1130 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
1131 Err(e) => return error_response(500, format!("src branch read: {e}")),
1132 };
1133 let dst_head = match store.get_branch(&req.dst_branch) {
1134 Ok(Some(b)) => b.head_op,
1135 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
1136 Err(e) => return error_response(500, format!("dst branch read: {e}")),
1137 };
1138 let log = match lex_vcs::OpLog::open(store.root()) {
1139 Ok(l) => l,
1140 Err(e) => return error_response(500, format!("op log: {e}")),
1141 };
1142 let merge_id = mint_merge_id();
1146 let mut session = match MergeSession::start(
1147 merge_id.clone(),
1148 &log,
1149 src_head.as_ref(),
1150 dst_head.as_ref(),
1151 ) {
1152 Ok(s) => s,
1153 Err(e) => return error_response(500, format!("merge start: {e}")),
1154 };
1155 let manifest_outcome = match store.manifest_merge(
1161 session.lca.as_deref(),
1162 dst_head.as_deref(),
1163 src_head.as_deref(),
1164 ) {
1165 Ok(o) => o,
1166 Err(lex_store::StoreError::AmbiguousManifest { op_id }) => {
1167 return error_with_detail(422, "AmbiguousManifest", serde_json::json!({
1168 "op_id": op_id,
1169 "reason": "a merge ancestor has an ambiguous files manifest; \
1170 append a set_files op resolving it before merging",
1171 }));
1172 }
1173 Err(e) => return error_response(500, format!("manifest merge: {e}")),
1174 };
1175 let (file_conflicts, needs_setfiles): (Vec<lex_vcs::FileConflict>, bool) =
1176 match manifest_outcome {
1177 lex_store::ManifestMergeOutcome::NoChange => (Vec::new(), false),
1178 lex_store::ManifestMergeOutcome::Needed { conflicts, .. } => (conflicts, true),
1179 };
1180 session.attach_file_conflicts(file_conflicts, needs_setfiles);
1181
1182 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
1183 let auto_resolved_count = session.auto_resolved.len();
1184 let remaining_file_conflicts: Vec<&lex_vcs::FileConflict> = session.remaining_file_conflicts();
1185 let body = serde_json::json!({
1186 "merge_id": merge_id,
1187 "src_head": session.src_head,
1188 "dst_head": session.dst_head,
1189 "lca": session.lca,
1190 "conflicts": conflicts,
1191 "auto_resolved_count": auto_resolved_count,
1192 "file_conflicts": remaining_file_conflicts,
1193 "needs_setfiles": session.needs_setfiles(),
1194 });
1195 drop(conflicts);
1196 drop(remaining_file_conflicts);
1197 drop(store);
1198 let wrapped = ApiMergeSession {
1199 inner: session,
1200 src_branch: req.src_branch,
1201 dst_branch: req.dst_branch,
1202 };
1203 state.sessions.lock().unwrap().insert(merge_id, wrapped);
1204 json_response(200, &body)
1205}
1206
1207#[derive(Deserialize)]
1208struct MergeResolveReq {
1209 #[serde(default)]
1214 resolutions: Vec<MergeResolveEntry>,
1215 #[serde(default)]
1221 file_resolutions: Vec<MergeFileResolveEntry>,
1222}
1223
1224#[derive(Deserialize)]
1225struct MergeResolveEntry {
1226 conflict_id: String,
1227 resolution: lex_vcs::Resolution,
1228}
1229
1230#[derive(Deserialize)]
1231struct MergeFileResolveEntry {
1232 path: lex_vcs::FilePath,
1233 resolution: lex_vcs::FileResolution,
1234}
1235
1236fn merge_resolve_handler(
1247 state: &State,
1248 merge_id: &str,
1249 body: &str,
1250) -> Response<std::io::Cursor<Vec<u8>>> {
1251 let req: MergeResolveReq = match serde_json::from_str(body) {
1252 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
1253 };
1254 let mut sessions = state.sessions.lock().unwrap();
1255 let Some(wrapped) = sessions.get_mut(merge_id) else {
1256 return error_response(404, format!("unknown merge_id `{merge_id}`"));
1257 };
1258 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
1259 .map(|e| (e.conflict_id, e.resolution))
1260 .collect();
1261 let store = state.store.lock().unwrap();
1266 let checker = lex_store::MergeResolutionChecker::new(&store, wrapped.dst_branch.clone());
1267 let verdicts = wrapped.inner.resolve_checked(pairs, &checker);
1268 drop(store);
1269 let file_pairs: Vec<(lex_vcs::FilePath, lex_vcs::FileResolution)> = req.file_resolutions
1270 .into_iter()
1271 .map(|e| (e.path, e.resolution))
1272 .collect();
1273 let file_verdicts = wrapped.inner.resolve_files(file_pairs);
1274 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
1275 let remaining_files: Vec<&lex_vcs::FileConflict> = wrapped.inner.remaining_file_conflicts();
1276 let body = serde_json::json!({
1277 "verdicts": verdicts,
1278 "remaining_conflicts": remaining,
1279 "file_verdicts": file_verdicts,
1280 "remaining_file_conflicts": remaining_files,
1281 });
1282 json_response(200, &body)
1283}
1284
1285fn merge_commit_handler(
1307 state: &State,
1308 merge_id: &str,
1309) -> Response<std::io::Cursor<Vec<u8>>> {
1310 use std::collections::BTreeMap;
1311 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
1312 Some(w) => w,
1313 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
1314 };
1315 let dst_branch = wrapped.dst_branch.clone();
1316 let src_head = wrapped.inner.src_head.clone();
1317 let dst_head = wrapped.inner.dst_head.clone();
1318 let lca = wrapped.inner.lca.clone();
1319 let auto_resolved = wrapped.inner.auto_resolved.clone();
1320
1321 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
1324
1325 for outcome in &auto_resolved {
1327 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
1328 entries.insert(sig_id.clone(), stage_id.clone());
1329 }
1330 }
1331
1332 let commit_out = match wrapped.inner.commit() {
1334 Ok(r) => r,
1335 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
1336 return error_with_detail(
1340 422,
1341 "conflicts remaining",
1342 serde_json::json!({"unresolved": ids}),
1343 );
1344 }
1345 Err(lex_vcs::CommitError::FileConflictsRemaining(paths)) => {
1346 return error_with_detail(
1347 422,
1348 "file conflicts remaining",
1349 serde_json::json!({"unresolved_files": paths}),
1350 );
1351 }
1352 };
1353
1354 for (conflict_id, resolution) in commit_out.resolved {
1355 match resolution {
1356 lex_vcs::Resolution::TakeOurs => {
1357 }
1359 lex_vcs::Resolution::TakeTheirs => {
1360 match resolve_take_theirs(state, &src_head, &conflict_id) {
1370 Ok(stage_id) => {
1371 entries.insert(conflict_id.clone(), stage_id);
1372 }
1373 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1374 }
1375 }
1376 lex_vcs::Resolution::Custom { op } => {
1377 match op.kind.merge_target() {
1386 Some((sig, stage)) => {
1387 if sig != conflict_id {
1388 return error_with_detail(
1389 422,
1390 "custom op targets a different sig than the conflict",
1391 serde_json::json!({
1392 "conflict_id": conflict_id,
1393 "op_targets": sig,
1394 }),
1395 );
1396 }
1397 entries.insert(conflict_id, stage);
1398 }
1399 None => {
1400 return error_with_detail(
1401 422,
1402 "custom op kind doesn't yield a single sig→stage delta",
1403 serde_json::json!({
1404 "conflict_id": conflict_id,
1405 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1406 }),
1407 );
1408 }
1409 }
1410 }
1411 lex_vcs::Resolution::Defer => {
1412 return error_response(500, "internal: Defer slipped past commit gate");
1414 }
1415 }
1416 }
1417
1418 let resolved_count = entries.len();
1419 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1420 if let Some(d) = dst_head.clone() { parents.push(d); }
1421 if let Some(s) = src_head.clone() { parents.push(s); }
1422 let op = lex_vcs::Operation::new(
1423 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1424 parents,
1425 );
1426 let transition = lex_vcs::StageTransition::Merge { entries };
1427 let store = state.store.lock().unwrap();
1428
1429 let manifest_blob: Option<String> = if commit_out.needs_setfiles {
1438 match store.manifest_merge(lca.as_deref(), dst_head.as_deref(), src_head.as_deref()) {
1439 Ok(lex_store::ManifestMergeOutcome::NoChange) => None,
1440 Ok(lex_store::ManifestMergeOutcome::Needed { auto_entries, conflicts }) => {
1441 let file_resolutions: BTreeMap<lex_vcs::FilePath, lex_vcs::FileResolution> =
1442 commit_out.resolved_files.into_iter().collect();
1443 match store.build_merged_manifest(
1444 auto_entries,
1445 &conflicts,
1446 &file_resolutions,
1447 "pending-merge",
1448 ) {
1449 Ok(blob_id) => Some(blob_id),
1450 Err(e) => return write_error_response("build merged manifest", e),
1451 }
1452 }
1453 Err(lex_store::StoreError::AmbiguousManifest { op_id }) => {
1454 return error_with_detail(422, "AmbiguousManifest", serde_json::json!({
1455 "op_id": op_id,
1456 }));
1457 }
1458 Err(e) => return write_error_response("manifest merge", e),
1459 }
1460 } else {
1461 None
1462 };
1463
1464 match store.apply_merge_op_gated_with_manifest(&dst_branch, op, transition, manifest_blob.as_deref(), None) {
1470 Ok(new_head_op) => json_response(200, &serde_json::json!({
1471 "new_head_op": new_head_op,
1472 "dst_branch": dst_branch,
1473 })),
1474 Err(lex_store::StoreError::TypeError(errs)) => error_with_detail(
1475 422, "merged program has type errors", serde_json::to_value(&errs).unwrap_or_default()),
1476 Err(e @ lex_store::StoreError::DependencyConflict { .. }) => {
1482 let detail = match &e {
1483 lex_store::StoreError::DependencyConflict { package, dst_version, src_version } => {
1484 serde_json::json!({
1485 "kind": "dependency_conflict",
1486 "package": package,
1487 "dst_branch": dst_branch,
1488 "dst_version": dst_version,
1489 "src_branch": wrapped.src_branch,
1490 "src_version": src_version,
1491 })
1492 }
1493 _ => serde_json::Value::Null,
1494 };
1495 error_with_detail(409, e.to_string(), detail)
1496 }
1497 Err(e @ lex_store::StoreError::AmbiguousManifest { .. }) => {
1498 error_with_detail(422, "AmbiguousManifest", serde_json::json!({"detail": e.to_string()}))
1499 }
1500 Err(e @ lex_store::StoreError::InvalidManifest(_))
1501 | Err(e @ lex_store::StoreError::MissingBlobs(_)) => {
1502 error_with_detail(422, e.to_string(), serde_json::Value::Null)
1503 }
1504 Err(e) => write_error_response("apply merge op", e),
1505 }
1506}
1507
1508fn resolve_take_theirs(
1513 state: &State,
1514 src_head: &Option<lex_vcs::OpId>,
1515 sig: &lex_vcs::SigId,
1516) -> std::io::Result<Option<lex_vcs::StageId>> {
1517 let store = state.store.lock().unwrap();
1518 let log = lex_vcs::OpLog::open(store.root())?;
1519 let Some(head) = src_head.as_ref() else { return Ok(None); };
1520 let mut current: Option<lex_vcs::StageId> = None;
1523 for record in log.walk_forward(head, None)? {
1524 match &record.produces {
1525 lex_vcs::StageTransition::Create { sig_id, stage_id }
1526 if sig_id == sig => { current = Some(stage_id.clone()); }
1527 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1528 if sig_id == sig => { current = Some(to.clone()); }
1529 lex_vcs::StageTransition::Remove { sig_id, .. }
1530 if sig_id == sig => { current = None; }
1531 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1532 if from == sig || to == sig => {
1533 if from == sig { current = None; }
1534 if to == sig { current = Some(body_stage_id.clone()); }
1535 }
1536 lex_vcs::StageTransition::Merge { entries } => {
1537 if let Some(opt) = entries.get(sig) {
1538 current = opt.clone();
1539 }
1540 }
1541 _ => {}
1542 }
1543 }
1544 Ok(current)
1545}
1546
1547fn mint_merge_id() -> MergeSessionId {
1548 use std::sync::atomic::{AtomicU64, Ordering};
1549 static COUNTER: AtomicU64 = AtomicU64::new(0);
1550 let nanos = SystemTime::now()
1551 .duration_since(UNIX_EPOCH)
1552 .map(|d| d.as_nanos())
1553 .unwrap_or(0);
1554 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1555 format!("merge_{nanos:x}_{n:x}")
1556}
1557
1558pub(crate) fn ops_batch_handler(state: &State, body: &str)
1594 -> Response<std::io::Cursor<Vec<u8>>>
1595{
1596 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1597 Ok(r) => r,
1598 Err(e) => return error_response(400,
1599 format!("body must be a JSON array of OperationRecord: {e}")),
1600 };
1601 let store = state.store.lock().unwrap();
1602 let log = match lex_vcs::OpLog::open(store.root()) {
1603 Ok(l) => l,
1604 Err(e) => return error_response(500, format!("opening op log: {e}")),
1605 };
1606
1607 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1615 std::collections::BTreeSet::new();
1616 for rec in &records {
1617 let expected = rec.op.op_id();
1618 if expected != rec.op_id {
1619 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1620 "supplied": rec.op_id,
1621 "expected": expected,
1622 }));
1623 }
1624 for parent in &rec.op.parents {
1625 let known = match log.get(parent) {
1626 Ok(Some(_)) => true,
1627 Ok(None) => false,
1628 Err(e) => return error_response(500, format!("op log read: {e}")),
1629 };
1630 if !known && !batch_ids.contains(parent) {
1631 return error_with_detail(422, "MissingParent", serde_json::json!({
1632 "op_id": rec.op_id,
1633 "missing_parent": parent,
1634 }));
1635 }
1636 }
1637 if let Err(resp) = crate::sync_http::check_set_files(&store, state.blob_limits, rec) {
1638 return resp;
1639 }
1640 batch_ids.insert(rec.op_id.clone());
1641 }
1642
1643 let mut added = 0usize;
1646 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1647 for rec in &records {
1648 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1649 match log.put(rec) {
1650 Ok(()) => {
1651 if !already_present {
1652 added += 1;
1653 added_ids.push(&rec.op_id);
1654 }
1655 }
1656 Err(e) => return error_response(500, format!("op log write: {e}")),
1657 }
1658 }
1659
1660 json_response(200, &serde_json::json!({
1661 "received": records.len(),
1662 "added": added,
1663 "skipped": records.len() - added,
1664 "added_ids": added_ids,
1665 }))
1666}
1667
1668pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1693 -> Response<std::io::Cursor<Vec<u8>>>
1694{
1695 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1696 Ok(a) => a,
1697 Err(e) => return error_response(400,
1698 format!("body must be a JSON array of Attestation: {e}")),
1699 };
1700 let store = state.store.lock().unwrap();
1701 let log = match store.attestation_log() {
1702 Ok(l) => l,
1703 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1704 };
1705 let op_log = match lex_vcs::OpLog::open(store.root()) {
1706 Ok(l) => l,
1707 Err(e) => return error_response(500, format!("opening op log: {e}")),
1708 };
1709
1710 for att in &attestations {
1715 if let Some(reserved) = state.reserved_producer_claimed(att) {
1716 return error_with_detail(403, "ReservedProducer", serde_json::json!({
1717 "attestation_id": att.attestation_id,
1718 "produced_by_tool": att.produced_by.tool,
1719 "reserved_tool": reserved,
1720 "message": "this producer name is reserved for the server; \
1721 clients may not submit attestations claiming it",
1722 }));
1723 }
1724 }
1725
1726 for att in &attestations {
1728 let expected = lex_vcs::Attestation::with_timestamp(
1731 att.stage_id.clone(),
1732 att.op_id.clone(),
1733 att.intent_id.clone(),
1734 att.kind.clone(),
1735 att.result.clone(),
1736 att.produced_by.clone(),
1737 att.cost.clone(),
1738 att.timestamp,
1739 ).attestation_id;
1740 if expected != att.attestation_id {
1741 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1742 "supplied": att.attestation_id,
1743 "expected": expected,
1744 }));
1745 }
1746 if let Some(op_id) = &att.op_id {
1750 match op_log.get(op_id) {
1751 Ok(Some(_)) => {}
1752 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1753 "attestation_id": att.attestation_id,
1754 "op_id": op_id,
1755 })),
1756 Err(e) => return error_response(500, format!("op log read: {e}")),
1757 }
1758 }
1759 }
1760
1761 let mut added = 0usize;
1765 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1766 for att in &attestations {
1767 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1768 match log.put(att) {
1769 Ok(()) => {
1770 if !already_present {
1771 added += 1;
1772 added_ids.push(&att.attestation_id);
1773 }
1774 }
1775 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1776 }
1777 }
1778
1779 json_response(200, &serde_json::json!({
1780 "received": attestations.len(),
1781 "added": added,
1782 "skipped": attestations.len() - added,
1783 "added_ids": added_ids,
1784 }))
1785}
1786
1787pub(crate) fn attestations_since_handler(state: &State, query: &str)
1800 -> Response<std::io::Cursor<Vec<u8>>>
1801{
1802 let mut after_op: Option<String> = None;
1803 let mut limit: Option<usize> = None;
1804 for kv in query.split('&') {
1805 let Some((k, v)) = kv.split_once('=') else { continue };
1806 match k {
1807 "after-op" => after_op = Some(v.to_string()),
1808 "limit" => {
1809 limit = Some(match v.parse::<usize>() {
1810 Ok(n) => n,
1811 Err(_) => return error_response(400,
1812 format!("limit must be a positive integer, got `{v}`")),
1813 });
1814 }
1815 _ => {}
1816 }
1817 }
1818
1819 let store = state.store.lock().unwrap();
1820 let log = match store.attestation_log() {
1821 Ok(l) => l,
1822 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1823 };
1824
1825 let exclude: std::collections::BTreeSet<String> = match &after_op {
1829 None => std::collections::BTreeSet::new(),
1830 Some(cutoff) => {
1831 let op_log = match lex_vcs::OpLog::open(store.root()) {
1832 Ok(l) => l,
1833 Err(e) => return error_response(500, format!("opening op log: {e}")),
1834 };
1835 match op_log.walk_back(cutoff, None) {
1836 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1837 Err(_) => {
1838 std::collections::BTreeSet::new()
1842 }
1843 }
1844 }
1845 };
1846
1847 let all = match log.list_all() {
1848 Ok(v) => v,
1849 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1850 };
1851 let mut filtered: Vec<lex_vcs::Attestation> = all
1852 .into_iter()
1853 .filter(|a| match &a.op_id {
1854 Some(op_id) => !exclude.contains(op_id),
1855 None => true,
1859 })
1860 .collect();
1861 filtered.sort_by(|a, b| {
1865 a.timestamp.cmp(&b.timestamp)
1866 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1867 });
1868 if let Some(n) = limit {
1869 filtered.truncate(n);
1870 }
1871
1872 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1873}
1874
1875#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
1884struct DepSpec {
1885 #[serde(default, skip_serializing_if = "Option::is_none")]
1886 registry: Option<String>,
1887 #[serde(default, skip_serializing_if = "Option::is_none")]
1888 version: Option<String>,
1889 #[serde(default, skip_serializing_if = "Option::is_none")]
1890 git: Option<String>,
1891 #[serde(default, skip_serializing_if = "Option::is_none")]
1892 branch: Option<String>,
1893 #[serde(default, skip_serializing_if = "Option::is_none")]
1894 tag: Option<String>,
1895 #[serde(default, skip_serializing_if = "Option::is_none")]
1896 rev: Option<String>,
1897 #[serde(default, skip_serializing_if = "Option::is_none")]
1898 path: Option<String>,
1899}
1900
1901impl DepSpec {
1902 fn to_toml_inline(&self) -> Option<String> {
1907 let mut parts: Vec<String> = Vec::new();
1908 let mut push = |k: &str, v: &Option<String>| {
1909 if let Some(val) = v {
1910 parts.push(format!("{k} = {}", toml_str(val)));
1911 }
1912 };
1913 push("registry", &self.registry);
1914 push("version", &self.version);
1915 push("git", &self.git);
1916 push("branch", &self.branch);
1917 push("tag", &self.tag);
1918 push("rev", &self.rev);
1919 push("path", &self.path);
1920 if parts.is_empty() {
1921 None
1922 } else {
1923 Some(format!("{{ {} }}", parts.join(", ")))
1924 }
1925 }
1926}
1927
1928fn toml_str(s: &str) -> String {
1930 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
1931}
1932
1933#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1935struct PkgRecord {
1936 name: String,
1937 version: String,
1938 head_op: Option<String>,
1939 published_at: u64,
1940 function_names: Vec<String>,
1942 #[serde(default)]
1947 dependencies: Vec<String>,
1948 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
1954 dependency_specs: std::collections::BTreeMap<String, DepSpec>,
1955 ops: Vec<serde_json::Value>,
1957}
1958
1959#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
1966#[serde(rename_all = "lowercase")]
1967pub enum Visibility {
1968 #[default]
1969 Private,
1970 Public,
1971}
1972
1973#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1978struct PkgIndex {
1979 latest: Option<String>,
1981 versions: Vec<PkgVersionSummary>,
1983 #[serde(default)]
1987 visibility: Visibility,
1988}
1989
1990#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1991struct PkgVersionSummary {
1992 version: String,
1993 head_op: Option<String>,
1994 published_at: u64,
1995}
1996
1997fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1998 root.join("packages").join(name)
1999}
2000
2001fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
2002 pkg_name_dir(root, name).join("index.json")
2003}
2004
2005fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
2006 pkg_name_dir(root, name).join(format!("{version}.json"))
2007}
2008
2009fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
2010 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
2011}
2012
2013fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
2014 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
2015 serde_json::from_slice(&bytes).ok()
2016}
2017
2018fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
2019 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
2020 serde_json::from_slice(&bytes).ok()
2021}
2022
2023fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
2024 let index = load_pkg_index(root, name)?;
2025 let latest = index.latest.clone()?;
2026 load_pkg_record(root, name, &latest)
2027}
2028
2029fn pkg_is_public(root: &std::path::Path, name: &str) -> bool {
2033 load_pkg_index(root, name).map(|i| i.visibility) == Some(Visibility::Public)
2034}
2035
2036fn valid_pkg_segment(s: &str) -> bool {
2041 !s.is_empty()
2042 && s.len() <= 128
2043 && s != "."
2044 && s != ".."
2045 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
2046}
2047
2048#[derive(Deserialize)]
2049struct VisibilityReq {
2050 visibility: Visibility,
2051}
2052
2053fn pkg_set_visibility_handler(
2060 state: &State,
2061 name: &str,
2062 body: &str,
2063) -> Response<std::io::Cursor<Vec<u8>>> {
2064 if !valid_pkg_segment(name) {
2065 return error_response(400, format!("invalid package name {name:?}"));
2066 }
2067 let req: VisibilityReq = match serde_json::from_str(body) {
2068 Ok(r) => r,
2069 Err(e) => return error_response(400, format!("bad request: {e}")),
2070 };
2071 let mut index = match load_pkg_index(&state.root, name) {
2072 Some(i) => i,
2073 None => return error_response(404, format!("package {name:?} not found")),
2074 };
2075 index.visibility = req.visibility;
2076 let bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
2077 match std::fs::write(pkg_index_path(&state.root, name), bytes) {
2078 Ok(()) => json_response(
2079 200,
2080 &serde_json::json!({ "name": name, "visibility": index.visibility }),
2081 ),
2082 Err(e) => error_response(500, format!("write index: {e}")),
2083 }
2084}
2085
2086#[derive(serde::Deserialize)]
2087struct ReleaseReq {
2088 version: String,
2089 #[serde(default)]
2090 branch: Option<String>,
2091 #[serde(default)]
2094 dependencies: Vec<String>,
2095 #[serde(default)]
2100 dependency_specs: std::collections::BTreeMap<String, DepSpec>,
2101}
2102
2103fn pkg_release_handler(state: &State, name: &str, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2112 if !valid_pkg_segment(name) {
2113 return error_response(400, format!("invalid package name {name:?}"));
2114 }
2115 let req: ReleaseReq = match serde_json::from_str(body) {
2116 Ok(r) => r,
2117 Err(e) => return error_response(400, format!("bad request: {e}")),
2118 };
2119 let version = req.version.trim().to_string();
2120 if version.is_empty() || !valid_pkg_segment(&version) {
2121 return error_response(400, "version must be a non-empty, path-safe string (e.g. 1.2.0)");
2122 }
2123 if load_pkg_record(&state.root, name, &version).is_some() {
2125 return error_response(
2126 409,
2127 format!("{name}@{version} already released; releases are immutable — bump the version"),
2128 );
2129 }
2130
2131 let store = state.store.lock().unwrap();
2132 let branch = req.branch.unwrap_or_else(|| store.current_branch());
2133 let head_op = match store.get_branch(&branch) {
2134 Ok(Some(b)) => b.head_op,
2135 Ok(None) => return error_response(404, format!("unknown branch {branch:?}")),
2136 Err(e) => return error_response(500, format!("get_branch: {e}")),
2137 };
2138 let Some(head_op) = head_op else {
2139 return error_response(400, format!("branch {branch:?} has no commits to release"));
2140 };
2141
2142 let predecessor = load_pkg_index(&state.root, name)
2153 .map(|i| i.versions)
2154 .unwrap_or_default()
2155 .into_iter()
2156 .filter_map(|v| lex_syntax::semver::parse_exact(&v.version).map(|p| (p, v)))
2157 .filter(|(p, _)| lex_syntax::semver::parse_exact(&version).map(|n| *p < n).unwrap_or(false))
2158 .max_by_key(|(p, _)| *p)
2159 .map(|(_, v)| v);
2160 if let Some(prev) = predecessor {
2161 if let (Some(prev_head), Some(declared)) = (
2162 prev.head_op.clone(),
2163 lex_syntax::semver::bump_between(&prev.version, &version),
2164 ) {
2165 if let (Ok(prev_api), Ok(new_api)) = (
2166 lex_store::api::public_api_at_op(&store, &prev_head),
2167 lex_store::api::public_api_at_op(&store, &head_op),
2168 ) {
2169 use lex_store::api::ApiChange;
2170 use lex_syntax::semver::Bump;
2171 let (required, why) = match lex_store::api::classify_api_change(&prev_api, &new_api) {
2172 ApiChange::Breaking(d) => (Bump::Major, d),
2173 ApiChange::Additive(d) => (Bump::Minor, d),
2174 ApiChange::None => (Bump::Patch, String::new()),
2175 };
2176 if declared < required {
2177 let need = match required {
2178 Bump::Major => "major",
2179 Bump::Minor => "minor",
2180 Bump::Patch => "patch",
2181 };
2182 return error_response(
2183 422,
2184 format!(
2185 "version bump too small: {} → {version} is a {declared:?} bump, \
2186 but the API change ({why}) requires a {need} bump",
2187 prev.version
2188 ),
2189 );
2190 }
2191 }
2192 }
2193 }
2194
2195 let head = store.branch_head(&branch).unwrap_or_default();
2198 let pairs: Vec<(String, String)> = head.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
2199 let function_names: Vec<String> = store
2200 .get_asts_for_sigs_bulk(&pairs)
2201 .into_iter()
2202 .filter_map(|r| r.ok())
2203 .filter_map(|s| match s {
2204 lex_ast::Stage::FnDecl(fd) => Some(fd.name),
2205 _ => None,
2206 })
2207 .collect();
2208 let mut deps: std::collections::BTreeSet<String> = req.dependencies.into_iter().collect();
2212 if let Ok(extracted) = lex_store::api::external_dependencies_at_op(&store, &head_op) {
2213 deps.extend(extracted);
2214 }
2215 deps.extend(req.dependency_specs.keys().cloned());
2218 let dependencies: Vec<String> = deps.into_iter().collect();
2219 drop(store);
2220
2221 let published_at = std::time::SystemTime::now()
2222 .duration_since(std::time::UNIX_EPOCH)
2223 .map(|d| d.as_secs())
2224 .unwrap_or(0);
2225 let record = PkgRecord {
2226 name: name.to_string(),
2227 version: version.clone(),
2228 head_op: Some(head_op.clone()),
2229 published_at,
2230 function_names,
2231 dependencies,
2232 dependency_specs: req.dependency_specs,
2233 ops: Vec::new(),
2234 };
2235 if let Err(e) = save_pkg_record(&state.root, &record, None) {
2236 return error_response(500, format!("write release: {e}"));
2237 }
2238 json_response(
2239 201,
2240 &serde_json::json!({
2241 "name": name,
2242 "version": version,
2243 "head_op": head_op,
2244 "branch": branch,
2245 }),
2246 )
2247}
2248
2249fn public_pkg_names(root: &std::path::Path) -> Vec<String> {
2252 list_pkg_names(root)
2253 .into_iter()
2254 .filter(|name| pkg_is_public(root, name))
2255 .collect()
2256}
2257
2258fn public_pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2262 let packages: Vec<serde_json::Value> = public_pkg_names(&state.root)
2263 .iter()
2264 .filter_map(|name| {
2265 let r = load_latest_pkg_record(&state.root, name)?;
2266 Some(serde_json::json!({
2267 "name": r.name,
2268 "version": r.version,
2269 "head_op": r.head_op,
2270 "published_at": r.published_at,
2271 }))
2272 })
2273 .collect();
2274 json_response(200, &serde_json::json!({ "packages": packages }))
2275}
2276
2277#[derive(Debug, PartialEq, Eq)]
2282enum PublicTarget {
2283 List,
2284 Latest(String),
2285 Versions(String),
2286 ApiDiff(String),
2287 Head(String),
2288 Version(String, String),
2289 Archive(String, String),
2290}
2291
2292impl PublicTarget {
2293 fn pkg_name(&self) -> Option<&str> {
2295 match self {
2296 PublicTarget::List => None,
2297 PublicTarget::Latest(n)
2298 | PublicTarget::Versions(n)
2299 | PublicTarget::ApiDiff(n)
2300 | PublicTarget::Head(n)
2301 | PublicTarget::Version(n, _)
2302 | PublicTarget::Archive(n, _) => Some(n),
2303 }
2304 }
2305}
2306
2307fn resolve_public(method: &Method, path: &str) -> Result<PublicTarget, u16> {
2312 if !matches!(method, Method::Get) {
2313 return Err(405);
2314 }
2315 let rest = path.trim_matches('/');
2316 if rest.is_empty() {
2317 return Ok(PublicTarget::List);
2318 }
2319 let segs: Vec<&str> = rest.split('/').collect();
2320 if !segs.iter().all(|s| valid_pkg_segment(s)) {
2321 return Err(404);
2322 }
2323 match segs.as_slice() {
2324 [n] => Ok(PublicTarget::Latest(n.to_string())),
2325 [n, "versions"] => Ok(PublicTarget::Versions(n.to_string())),
2326 [n, "api-diff"] => Ok(PublicTarget::ApiDiff(n.to_string())),
2327 [n, "head"] => Ok(PublicTarget::Head(n.to_string())),
2328 [n, v, "archive"] => Ok(PublicTarget::Archive(n.to_string(), v.to_string())),
2329 [n, v] => Ok(PublicTarget::Version(n.to_string(), v.to_string())),
2330 _ => Err(404),
2331 }
2332}
2333
2334pub fn route_public(
2345 state: &State,
2346 method: &Method,
2347 path: &str,
2348 query: &str,
2349) -> Response<std::io::Cursor<Vec<u8>>> {
2350 let target = match resolve_public(method, path) {
2351 Ok(t) => t,
2352 Err(405) => return error_response(405, "public read is GET-only"),
2353 Err(_) => return error_response(404, "not found"),
2354 };
2355 if let PublicTarget::List = target {
2357 return public_pkg_list_handler(state);
2358 }
2359 if let Some(name) = target.pkg_name() {
2361 if !pkg_is_public(&state.root, name) {
2362 return error_response(404, format!("package {name:?} not found"));
2363 }
2364 }
2365 match target {
2366 PublicTarget::List => unreachable!("handled above"),
2367 PublicTarget::Latest(n) => pkg_get_handler(state, &n),
2368 PublicTarget::Versions(n) => pkg_versions_handler(state, &n),
2369 PublicTarget::ApiDiff(n) => pkg_api_diff_handler(state, &n, query),
2370 PublicTarget::Head(n) => pkg_head_handler(state, &n),
2371 PublicTarget::Version(n, v) => pkg_get_version_handler(state, &n, &v),
2372 PublicTarget::Archive(n, v) => pkg_archive_handler(state, &n, &v),
2373 }
2374}
2375
2376fn save_pkg_record(
2377 root: &std::path::Path,
2378 record: &PkgRecord,
2379 archive: Option<&[u8]>,
2382) -> std::io::Result<()> {
2383 let dir = pkg_name_dir(root, &record.name);
2384 std::fs::create_dir_all(&dir)?;
2385
2386 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
2388 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
2389
2390 if let Some(archive) = archive {
2392 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
2393 }
2394
2395 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
2397 index.latest = Some(record.version.clone());
2398 if !index.versions.iter().any(|v| v.version == record.version) {
2399 index.versions.push(PkgVersionSummary {
2400 version: record.version.clone(),
2401 head_op: record.head_op.clone(),
2402 published_at: record.published_at,
2403 });
2404 }
2405 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
2406 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
2407}
2408
2409fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
2410 let dir = root.join("packages");
2411 let Ok(entries) = std::fs::read_dir(&dir) else {
2412 return Vec::new();
2413 };
2414 let mut names: Vec<String> = entries
2415 .filter_map(|e| e.ok())
2416 .filter(|e| e.path().is_dir())
2417 .filter_map(|e| e.file_name().into_string().ok())
2418 .collect();
2419 names.sort();
2420 names
2421}
2422
2423fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
2424 let Ok(entries) = std::fs::read_dir(dir) else { return };
2425 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
2426 entries.sort_by_key(|e| e.path());
2427 for entry in entries {
2428 let path = entry.path();
2429 if path.is_dir() {
2430 collect_lex_files(&path, out);
2431 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
2432 out.push(path);
2433 }
2434 }
2435}
2436
2437fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
2440 let tmp = match tempfile::TempDir::new() {
2441 Ok(t) => t,
2442 Err(e) => return error_response(500, format!("create temp dir: {e}")),
2443 };
2444 {
2445 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
2446 let mut ar = tar::Archive::new(gz);
2447 if let Err(e) = ar.unpack(tmp.path()) {
2448 return error_response(400, format!("unpack archive: {e}"));
2449 }
2450 }
2451
2452 let toml_path = tmp.path().join("lex.toml");
2453 if !toml_path.exists() {
2454 return error_response(400, "archive must contain lex.toml at root");
2455 }
2456 let manifest = match Manifest::load(&toml_path) {
2457 Ok(m) => m,
2458 Err(e) => return error_response(400, format!("lex.toml: {e}")),
2459 };
2460 let (pkg_name, pkg_version) = match &manifest.package {
2461 Some(m) => (m.name.clone(), m.version.clone()),
2462 None => return error_response(400, "lex.toml must have a [package] section"),
2463 };
2464
2465 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
2469 return error_response(
2470 409,
2471 format!(
2472 "package {pkg_name}@{pkg_version} already published; \
2473 bump the version in lex.toml to publish a new release"
2474 ),
2475 );
2476 }
2477
2478 let src_dir = tmp.path().join("src");
2479 if !src_dir.exists() {
2480 return error_response(400, "archive must contain a src/ directory");
2481 }
2482 let mut lex_files: Vec<PathBuf> = Vec::new();
2483 collect_lex_files(&src_dir, &mut lex_files);
2484 if lex_files.is_empty() {
2485 return error_response(400, "no .lex files found in src/");
2486 }
2487
2488 let store = state.store.lock().unwrap();
2489 let branch = store.current_branch();
2490
2491 let old_head = match store.branch_head(&branch) {
2511 Ok(h) => h,
2512 Err(e) => return error_response(500, format!("branch_head: {e}")),
2513 };
2514 let old_pairs: Vec<(String, String)> =
2521 old_head.iter().map(|(sig, stage)| (sig.clone(), stage.clone())).collect();
2522 let mut old_fns_by_name: BTreeMap<String, Vec<lex_ast::FnDecl>> = BTreeMap::new();
2523 for fd in store.get_asts_for_sigs_bulk(&old_pairs)
2524 .into_iter()
2525 .filter_map(|r| r.ok())
2526 .filter_map(|s| match s { lex_ast::Stage::FnDecl(fd) => Some(fd), _ => None })
2527 {
2528 old_fns_by_name.entry(fd.name.clone()).or_default().push(fd);
2529 }
2530 let mut old_types_by_name: BTreeMap<String, lex_ast::TypeDecl> = BTreeMap::new();
2533 for td in store.get_asts_for_sigs_bulk(&old_pairs)
2534 .into_iter()
2535 .filter_map(|r| r.ok())
2536 .filter_map(|s| match s { lex_ast::Stage::TypeDecl(td) => Some(td), _ => None })
2537 {
2538 old_types_by_name.insert(td.name.clone(), td);
2539 }
2540 fn structural_key(fd: &lex_ast::FnDecl) -> Option<String> {
2548 let mut anon = fd.clone();
2549 anon.name = String::new();
2550 lex_ast::sig_id(&lex_ast::Stage::FnDecl(anon))
2551 }
2552
2553 fn take_matching(
2563 map: &mut BTreeMap<String, Vec<lex_ast::FnDecl>>,
2564 name: &str,
2565 new_fd: &lex_ast::FnDecl,
2566 ) -> Option<lex_ast::FnDecl> {
2567 let candidates = map.get_mut(name)?;
2568 let idx = match candidates.len() {
2569 0 => return None,
2570 1 => 0,
2571 _ => {
2572 let want = structural_key(new_fd);
2573 candidates.iter().position(|c| structural_key(c) == want)?
2574 }
2575 };
2576 let matched = candidates.remove(idx);
2577 if candidates.is_empty() {
2578 map.remove(name);
2579 }
2580 Some(matched)
2581 }
2582
2583 let loaded = match load_package(&lex_files, tmp.path(), &pkg_name, true) {
2607 Ok(p) => p,
2608 Err(e) => return error_response(400, format!("load package: {e}")),
2609 };
2610 let mut stages = canonicalize_program(&loaded.program);
2611 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
2617 return error_with_detail(
2618 422,
2619 format!("type errors in package {pkg_name}"),
2620 serde_json::to_value(&errs).unwrap(),
2621 );
2622 }
2623 let new_fns = stage_fns(&stages);
2624 let all_function_names: Vec<String> = new_fns.keys().cloned().collect();
2625
2626 let mut old_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2631 for (name, new_fd) in &new_fns {
2632 if let Some(fd) = take_matching(&mut old_fns_by_name, name, new_fd) {
2633 old_fns.insert(name.clone(), fd);
2634 }
2635 }
2636 let new_types = stage_types(&stages);
2637 let old_types: BTreeMap<String, lex_ast::TypeDecl> = new_types
2641 .keys()
2642 .filter_map(|n| old_types_by_name.get(n).map(|td| (n.clone(), td.clone())))
2643 .collect();
2644 let report =
2645 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
2646
2647 let mut new_imports = lex_vcs::ImportMap::new();
2655 for (file, modules) in &loaded.imports_by_file {
2656 let entry = new_imports.entry(file.clone()).or_default();
2657 for (reference, alias) in modules {
2658 entry.insert(lex_vcs::ImportRef {
2659 reference: reference.clone(),
2660 alias: alias.clone(),
2661 });
2662 }
2663 }
2664
2665 let outcome = match store.publish_program_with_intent(
2668 &branch,
2669 &stages,
2670 &report,
2671 &new_imports,
2672 false,
2673 None,
2674 None,
2675 &loaded.module_prefixes,
2676 ) {
2677 Ok(outcome) => outcome,
2678 Err(lex_store::StoreError::TypeError(errs)) => {
2679 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
2680 }
2681 Err(e) => return write_error_response("publish_program", e),
2682 };
2683 let all_ops: Vec<serde_json::Value> = match serde_json::to_value(&outcome.ops) {
2684 Ok(serde_json::Value::Array(arr)) => arr,
2685 _ => Vec::new(),
2686 };
2687 let final_head_op = outcome.head_op;
2688
2689 let now = SystemTime::now()
2714 .duration_since(UNIX_EPOCH)
2715 .map(|d| d.as_secs())
2716 .unwrap_or(0);
2717 let dependencies: Vec<String> = final_head_op
2720 .as_ref()
2721 .and_then(|h| lex_store::api::external_dependencies_at_op(&store, h).ok())
2722 .unwrap_or_default();
2723 let record = PkgRecord {
2724 name: pkg_name.clone(),
2725 version: pkg_version,
2726 head_op: final_head_op.clone(),
2727 published_at: now,
2728 function_names: all_function_names,
2729 dependencies,
2730 dependency_specs: Default::default(),
2734 ops: all_ops.clone(),
2735 };
2736 if let Err(e) = save_pkg_record(&state.root, &record, Some(body)) {
2737 return error_response(500, format!("save package index: {e}"));
2738 }
2739
2740 json_response(200, &serde_json::json!({
2741 "package": pkg_name,
2742 "ops": all_ops,
2743 "head_op": final_head_op,
2744 }))
2745}
2746
2747fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2749 let names = list_pkg_names(&state.root);
2750 let packages: Vec<serde_json::Value> = names.iter()
2751 .filter_map(|name| {
2752 let idx = load_pkg_index(&state.root, name)?;
2753 let latest = idx.latest.as_deref()?;
2754 let r = load_pkg_record(&state.root, name, latest)?;
2755 Some(serde_json::json!({
2756 "name": r.name,
2757 "version": r.version,
2758 "head_op": r.head_op,
2759 "published_at": r.published_at,
2760 }))
2761 })
2762 .collect();
2763 json_response(200, &serde_json::json!({ "packages": packages }))
2764}
2765
2766fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2768 match load_latest_pkg_record(&state.root, name) {
2769 Some(r) => json_response(200, &serde_json::json!({
2770 "name": r.name,
2771 "version": r.version,
2772 "head_op": r.head_op,
2773 "published_at": r.published_at,
2774 "function_names": r.function_names,
2775 "ops": r.ops,
2776 })),
2777 None => error_response(404, format!("package {name:?} not found")),
2778 }
2779}
2780
2781fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2783 match load_pkg_index(&state.root, name) {
2784 Some(idx) => json_response(200, &serde_json::json!({
2785 "name": name,
2786 "latest": idx.latest,
2787 "versions": idx.versions,
2788 })),
2789 None => error_response(404, format!("package {name:?} not found")),
2790 }
2791}
2792
2793fn pkg_api_diff_handler(state: &State, name: &str, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2798 let mut from: Option<String> = None;
2799 let mut to: Option<String> = None;
2800 for kv in query.split('&') {
2801 match kv.split_once('=') {
2802 Some(("from", v)) => from = Some(v.to_string()),
2803 Some(("to", v)) => to = Some(v.to_string()),
2804 _ => {}
2805 }
2806 }
2807 let (Some(from), Some(to)) = (from, to) else {
2808 return error_response(400, "api-diff requires ?from=<version>&to=<version>");
2809 };
2810 let head_of = |v: &str| load_pkg_record(&state.root, name, v).and_then(|r| r.head_op);
2811 let (Some(from_head), Some(to_head)) = (head_of(&from), head_of(&to)) else {
2812 return error_response(404, format!("{name}: unknown release in {from}..{to}"));
2813 };
2814
2815 let store = state.store.lock().unwrap();
2816 let (prev_api, new_api) = match (
2817 lex_store::api::public_api_at_op(&store, &from_head),
2818 lex_store::api::public_api_at_op(&store, &to_head),
2819 ) {
2820 (Ok(a), Ok(b)) => (a, b),
2821 _ => return error_response(500, "could not read package APIs for the given releases"),
2822 };
2823 let (change, detail) = match lex_store::api::classify_api_change(&prev_api, &new_api) {
2824 lex_store::api::ApiChange::Breaking(d) => ("breaking", d),
2825 lex_store::api::ApiChange::Additive(d) => ("additive", d),
2826 lex_store::api::ApiChange::None => ("none", String::new()),
2827 };
2828 let renames = lex_store::api::detect_renames(&prev_api, &new_api);
2829 json_response(200, &serde_json::json!({
2830 "name": name, "from": from, "to": to,
2831 "change": change, "detail": detail, "renames": renames,
2832 }))
2833}
2834
2835fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2837 match load_pkg_record(&state.root, name, version) {
2838 Some(r) => json_response(200, &serde_json::json!({
2839 "name": r.name,
2840 "version": r.version,
2841 "head_op": r.head_op,
2842 "published_at": r.published_at,
2843 "function_names": r.function_names,
2844 "dependencies": r.dependencies,
2845 "ops": r.ops,
2846 })),
2847 None => error_response(404, format!("package {name:?}@{version:?} not found")),
2848 }
2849}
2850
2851fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2853 let gzip = |bytes: Vec<u8>| {
2854 Response::from_data(bytes).with_status_code(200).with_header(
2855 tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"application/gzip"[..]).unwrap(),
2856 )
2857 };
2858
2859 if let Ok(bytes) = std::fs::read(pkg_archive_path(&state.root, name, version)) {
2861 return gzip(bytes);
2862 }
2863
2864 if let Some(record) = load_pkg_record(&state.root, name, version) {
2868 if let Some(head_op) = record.head_op.clone() {
2869 match render_op_log_archive(state, name, version, &head_op, &record.dependency_specs) {
2870 Ok(bytes) => return gzip(bytes),
2871 Err(e) => {
2872 return error_response(500, format!("rendering archive for {name:?}@{version:?}: {e}"));
2873 }
2874 }
2875 }
2876 }
2877
2878 error_response(404, format!("archive for {name:?}@{version:?} not found"))
2879}
2880
2881const DEFAULT_ARCHIVE_MODULE: &str = "src/lib.lex";
2886
2887fn render_op_log_archive(
2892 state: &State,
2893 name: &str,
2894 version: &str,
2895 head_op: &str,
2896 dependency_specs: &std::collections::BTreeMap<String, DepSpec>,
2897) -> Result<Vec<u8>, String> {
2898 let (files, lock): (Vec<(String, String)>, Option<String>) = {
2904 let store = state.store.lock().unwrap();
2905 let lock = store.committed_lock_inherited(head_op).ok().flatten();
2911 let head = lex_store::render::package_head_at_op(&store, head_op)
2912 .map_err(|e| format!("reading head {head_op}: {e}"))?;
2913 let files = match lex_store::render::render_source(&store, &head)
2914 .map_err(|e| format!("rendering source at {head_op}: {e}"))?
2915 {
2916 lex_store::render::RenderedSource::Single { path, src } => {
2917 vec![(path.unwrap_or_else(|| DEFAULT_ARCHIVE_MODULE.to_string()), src)]
2920 }
2921 lex_store::render::RenderedSource::Multi(tree) => tree.into_iter().collect(),
2922 };
2923 (files, lock)
2924 };
2925
2926 let mut manifest = format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\n");
2931 let dep_lines: Vec<String> = dependency_specs
2932 .iter()
2933 .filter_map(|(dep_name, spec)| spec.to_toml_inline().map(|inline| format!("{dep_name} = {inline}")))
2934 .collect();
2935 if !dep_lines.is_empty() {
2936 manifest.push_str("\n[dependencies]\n");
2937 for line in dep_lines {
2938 manifest.push_str(&line);
2939 manifest.push('\n');
2940 }
2941 }
2942 let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2943 {
2944 let mut ar = tar::Builder::new(&mut enc);
2945 let mut append = |p: &str, data: &[u8]| -> std::io::Result<()> {
2946 let mut h = tar::Header::new_gnu();
2947 h.set_size(data.len() as u64);
2948 h.set_mode(0o644);
2949 h.set_cksum();
2950 ar.append_data(&mut h, p, data)
2951 };
2952 append("lex.toml", manifest.as_bytes()).map_err(|e| e.to_string())?;
2953 if let Some(lock) = &lock {
2954 append("lex.lock", lock.as_bytes()).map_err(|e| e.to_string())?;
2955 }
2956 for (path, src) in &files {
2957 append(path, src.as_bytes()).map_err(|e| e.to_string())?;
2958 }
2959 ar.finish().map_err(|e| e.to_string())?;
2960 }
2961 enc.finish().map_err(|e| e.to_string())
2962}
2963
2964fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2966 match load_latest_pkg_record(&state.root, name) {
2967 Some(r) => json_response(200, &serde_json::json!({
2968 "name": r.name,
2969 "version": r.version,
2970 "head_op": r.head_op,
2971 })),
2972 None => error_response(404, format!("package {name:?} not found")),
2973 }
2974}
2975
2976fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2978 let record = match load_latest_pkg_record(&state.root, name) {
2979 Some(r) => r,
2980 None => return error_response(404, format!("package {name:?} not found")),
2981 };
2982
2983 let store = state.store.lock().unwrap();
2984 let branch = store.current_branch();
2985
2986 let head = match store.branch_head(&branch) {
2987 Ok(h) => h,
2988 Err(e) => return error_response(500, format!("branch_head: {e}")),
2989 };
2990
2991 let head_pairs: Vec<(String, String)> = head
2998 .iter()
2999 .map(|(sig, stage)| (sig.clone(), stage.clone()))
3000 .collect();
3001 let old_fns: BTreeMap<String, lex_ast::FnDecl> = store
3002 .get_asts_for_sigs_bulk(&head_pairs)
3003 .into_iter()
3004 .filter_map(|r| r.ok())
3005 .filter_map(|s| match s {
3006 lex_ast::Stage::FnDecl(fd)
3007 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
3008 _ => None,
3009 })
3010 .collect();
3011
3012 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
3013 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
3014 let empty_imports = lex_vcs::ImportMap::new();
3015
3016 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
3017 Ok(outcome) => {
3018 let ver = record.version.clone();
3020 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
3021 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
3022 if let Some(mut idx) = load_pkg_index(&state.root, name) {
3024 idx.versions.retain(|v| v.version != ver);
3025 idx.latest = idx.versions.last().map(|v| v.version.clone());
3026 if idx.versions.is_empty() {
3027 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
3028 } else {
3029 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
3030 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
3031 }
3032 }
3033 json_response(200, &serde_json::json!({
3034 "deleted": name,
3035 "version": ver,
3036 "ops": outcome.ops,
3037 "head_op": outcome.head_op,
3038 }))
3039 }
3040 Err(lex_store::StoreError::TypeError(errs)) => {
3041 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
3042 }
3043 Err(e) => write_error_response("retract package", e),
3044 }
3045}
3046
3047#[cfg(test)]
3048mod dep_spec_tests {
3049 use super::DepSpec;
3050
3051 #[test]
3052 fn a_dual_spec_renders_both_git_and_vcs_refs_vcs_first() {
3053 let spec = DepSpec {
3054 registry: Some("vcs.lexlang.org/lex-official/lex-schema".into()),
3055 version: Some("^0.9".into()),
3056 git: Some("https://github.com/alpibrusl/lex-schema".into()),
3057 ..Default::default()
3058 };
3059 assert_eq!(
3060 spec.to_toml_inline().as_deref(),
3061 Some("{ registry = \"vcs.lexlang.org/lex-official/lex-schema\", version = \"^0.9\", git = \"https://github.com/alpibrusl/lex-schema\" }"),
3062 );
3063 }
3064
3065 #[test]
3066 fn bare_git_and_bare_registry_specs_render_their_own_keys() {
3067 let git = DepSpec { git: Some("https://x/g".into()), tag: Some("v1".into()), ..Default::default() };
3068 assert_eq!(git.to_toml_inline().as_deref(), Some("{ git = \"https://x/g\", tag = \"v1\" }"));
3069 let reg = DepSpec { registry: Some("vcs/r".into()), version: Some("1.0.0".into()), ..Default::default() };
3070 assert_eq!(reg.to_toml_inline().as_deref(), Some("{ registry = \"vcs/r\", version = \"1.0.0\" }"));
3071 }
3072
3073 #[test]
3074 fn an_empty_spec_renders_nothing() {
3075 assert_eq!(DepSpec::default().to_toml_inline(), None);
3076 }
3077}
3078
3079#[cfg(test)]
3080mod policy_ceiling_tests {
3081 use super::*;
3082 use lex_runtime::Policy;
3083 use std::path::PathBuf;
3084
3085 fn permissive_request() -> Policy {
3089 Policy {
3090 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
3091 .iter()
3092 .map(|s| s.to_string())
3093 .collect(),
3094 allow_fs_read: vec![PathBuf::from("/")],
3095 allow_fs_write: vec![PathBuf::from("/")],
3096 allow_net_host: Vec::new(),
3097 allow_proc: Vec::new(),
3098 allow_approval: Vec::new(),
3099 budget: None,
3100 }
3101 }
3102
3103 #[test]
3104 fn ceiling_drops_effects_the_caller_was_not_granted() {
3105 let ceiling = Policy {
3106 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
3107 ..Policy::default()
3108 };
3109 let got = clamp_policy(permissive_request(), &ceiling);
3110 assert!(got.allow_effects.contains("io"));
3111 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
3112 assert!(!got.allow_effects.contains("fs_write"));
3113 assert!(!got.allow_effects.contains("net"));
3114 assert!(!got.allow_effects.contains("time"));
3116 }
3117
3118 #[test]
3119 fn ceiling_scopes_override_caller_scopes() {
3120 let ceiling = Policy {
3121 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
3122 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
3123 ..Policy::default()
3124 };
3125 let got = clamp_policy(permissive_request(), &ceiling);
3126 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
3129 assert!(got.allow_fs_write.is_empty());
3130 assert!(got.allow_proc.is_empty());
3131 assert!(got.allow_net_host.is_empty());
3132 }
3133
3134 #[test]
3135 fn ceiling_caps_budget_and_prefers_the_smaller() {
3136 let mut req = permissive_request();
3138 req.budget = None;
3139 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
3140 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
3141
3142 let mut req2 = permissive_request();
3144 req2.budget = Some(50);
3145 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
3146 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
3147 }
3148
3149 #[test]
3150 fn empty_ceiling_is_pure_only() {
3151 let got = clamp_policy(permissive_request(), &Policy::default());
3152 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
3153 assert!(got.allow_proc.is_empty());
3154 assert!(got.allow_fs_write.is_empty());
3155 }
3156}
3157
3158#[cfg(test)]
3159mod public_read_tests {
3160 use super::*;
3161
3162 fn seed_pkg(root: &std::path::Path, name: &str, version: &str) {
3165 let record = PkgRecord {
3166 name: name.to_string(),
3167 version: version.to_string(),
3168 head_op: Some(format!("op-{name}")),
3169 published_at: 1,
3170 function_names: vec![format!("{name}.f")],
3171 dependencies: vec![],
3172 dependency_specs: Default::default(),
3173 ops: vec![],
3174 };
3175 save_pkg_record(root, &record, Some(format!("ARCHIVE:{name}@{version}").as_bytes()))
3176 .expect("seed package");
3177 }
3178
3179 #[test]
3180 fn new_package_defaults_to_private() {
3181 let tmp = tempfile::TempDir::new().unwrap();
3182 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
3183 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
3184 assert!(!pkg_is_public(tmp.path(), "does-not-exist"));
3186 }
3187
3188 #[test]
3189 fn set_visibility_round_trips_and_index_persists() {
3190 let tmp = tempfile::TempDir::new().unwrap();
3191 let state = State::open(tmp.path().to_path_buf()).unwrap();
3192 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
3193
3194 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"public"}"#);
3195 assert!(pkg_is_public(tmp.path(), "lex-schema"));
3196 let idx = load_pkg_index(tmp.path(), "lex-schema").unwrap();
3198 assert_eq!(idx.latest.as_deref(), Some("0.9.2"));
3199 assert_eq!(idx.versions.len(), 1);
3200
3201 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"private"}"#);
3202 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
3203 }
3204
3205 #[test]
3206 fn set_visibility_on_unknown_package_is_a_noop() {
3207 let tmp = tempfile::TempDir::new().unwrap();
3208 let state = State::open(tmp.path().to_path_buf()).unwrap();
3209 let _ = pkg_set_visibility_handler(&state, "ghost", r#"{"visibility":"public"}"#);
3211 assert!(load_pkg_index(tmp.path(), "ghost").is_none());
3212 }
3213
3214 #[test]
3215 fn public_listing_omits_private_packages() {
3216 let tmp = tempfile::TempDir::new().unwrap();
3217 let state = State::open(tmp.path().to_path_buf()).unwrap();
3218 seed_pkg(tmp.path(), "pub-pkg", "1.0.0");
3219 seed_pkg(tmp.path(), "priv-pkg", "1.0.0");
3220 let _ = pkg_set_visibility_handler(&state, "pub-pkg", r#"{"visibility":"public"}"#);
3221
3222 let names = public_pkg_names(tmp.path());
3223 assert_eq!(names, vec!["pub-pkg".to_string()]);
3224 }
3225
3226 #[test]
3227 fn resolve_public_maps_routes() {
3228 let get = Method::Get;
3229 assert_eq!(resolve_public(&get, "").unwrap(), PublicTarget::List);
3230 assert_eq!(resolve_public(&get, "/").unwrap(), PublicTarget::List);
3231 assert_eq!(
3232 resolve_public(&get, "/lex-schema").unwrap(),
3233 PublicTarget::Latest("lex-schema".into())
3234 );
3235 assert_eq!(
3236 resolve_public(&get, "/lex-schema/versions").unwrap(),
3237 PublicTarget::Versions("lex-schema".into())
3238 );
3239 assert_eq!(
3240 resolve_public(&get, "/lex-schema/head").unwrap(),
3241 PublicTarget::Head("lex-schema".into())
3242 );
3243 assert_eq!(
3244 resolve_public(&get, "/lex-schema/0.9.2").unwrap(),
3245 PublicTarget::Version("lex-schema".into(), "0.9.2".into())
3246 );
3247 assert_eq!(
3248 resolve_public(&get, "/lex-schema/0.9.2/archive").unwrap(),
3249 PublicTarget::Archive("lex-schema".into(), "0.9.2".into())
3250 );
3251 }
3252
3253 #[test]
3254 fn resolve_public_rejects_bad_method_and_traversal() {
3255 assert_eq!(resolve_public(&Method::Put, "/lex-schema"), Err(405));
3257 assert_eq!(resolve_public(&Method::Post, "").err(), Some(405));
3258 assert_eq!(resolve_public(&Method::Get, "/.."), Err(404));
3260 assert_eq!(resolve_public(&Method::Get, "/lex-schema/../etc"), Err(404));
3261 assert_eq!(resolve_public(&Method::Get, "/a/b/c/d"), Err(404));
3262 assert!(resolve_public(&Method::Get, "/lex schema").is_err());
3264 }
3265}