1use indexmap::IndexMap;
9use lex_ast::canonicalize_program;
10use lex_bytecode::{compile_program, vm::Vm, Value};
11use lex_runtime::{check_program as check_policy, DefaultHandler, Policy};
12use lex_store::Store;
13use crate::publish_examples::record_examples_for_publish;
14use lex_syntax::{load_package, load_program_from_str, Manifest};
15use lex_vcs::{MergeSession, MergeSessionId};
16use serde::{Deserialize, Serialize};
17use std::collections::{BTreeMap, BTreeSet, HashMap};
18use std::path::PathBuf;
19use std::sync::{Arc, Mutex};
20use std::time::{SystemTime, UNIX_EPOCH};
21use tiny_http::{Header, Method, Request, Response};
22
23fn stage_fns(stages: &[lex_ast::Stage]) -> BTreeMap<String, lex_ast::FnDecl> {
25 stages.iter().filter_map(|s| match s {
26 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
27 _ => None,
28 }).collect()
29}
30
31fn stage_types(stages: &[lex_ast::Stage]) -> BTreeMap<String, lex_ast::TypeDecl> {
34 stages.iter().filter_map(|s| match s {
35 lex_ast::Stage::TypeDecl(td) => Some((td.name.clone(), td.clone())),
36 _ => None,
37 }).collect()
38}
39
40pub struct State {
41 pub store: Mutex<Store>,
42 pub root: PathBuf,
47 pub sessions: Mutex<HashMap<MergeSessionId, ApiMergeSession>>,
54 pub policy_ceiling: Option<Policy>,
75}
76
77pub struct ApiMergeSession {
84 pub inner: MergeSession,
85 pub src_branch: String,
86 pub dst_branch: String,
87}
88
89impl State {
90 pub fn open(root: PathBuf) -> anyhow::Result<Self> {
91 Self::open_with_ceiling(root, None)
92 }
93
94 pub fn open_with_ceiling(
99 root: PathBuf,
100 policy_ceiling: Option<Policy>,
101 ) -> anyhow::Result<Self> {
102 Ok(Self {
103 store: Mutex::new(Store::open(&root)?),
104 root,
105 sessions: Mutex::new(HashMap::new()),
106 policy_ceiling,
107 })
108 }
109
110 pub fn new_with_tenant(tenant_id: &str, store_root: PathBuf) -> anyhow::Result<Self> {
120 validate_tenant_id(tenant_id)?;
121 Self::open(store_root.join(tenant_id))
122 }
123
124 pub fn new_with_tenant_and_ceiling(
129 tenant_id: &str,
130 store_root: PathBuf,
131 policy_ceiling: Option<Policy>,
132 ) -> anyhow::Result<Self> {
133 validate_tenant_id(tenant_id)?;
134 Self::open_with_ceiling(store_root.join(tenant_id), policy_ceiling)
135 }
136}
137
138fn clamp_policy(requested: Policy, ceiling: &Policy) -> Policy {
154 let allow_effects: BTreeSet<String> = requested
155 .allow_effects
156 .intersection(&ceiling.allow_effects)
157 .cloned()
158 .collect();
159 let budget = match (requested.budget, ceiling.budget) {
160 (Some(r), Some(c)) => Some(r.min(c)),
161 (None, Some(c)) => Some(c),
162 (Some(r), None) => Some(r),
163 (None, None) => None,
164 };
165 Policy {
166 allow_effects,
167 allow_fs_read: ceiling.allow_fs_read.clone(),
168 allow_fs_write: ceiling.allow_fs_write.clone(),
169 allow_net_host: ceiling.allow_net_host.clone(),
170 allow_proc: ceiling.allow_proc.clone(),
171 allow_approval: ceiling.allow_approval.clone(),
172 budget,
173 }
174}
175
176fn validate_tenant_id(tenant_id: &str) -> anyhow::Result<()> {
177 if tenant_id.is_empty() {
178 anyhow::bail!("tenant_id must not be empty");
179 }
180 if tenant_id.len() > 64 {
181 anyhow::bail!("tenant_id must be at most 64 bytes");
182 }
183 if !tenant_id
184 .bytes()
185 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
186 {
187 anyhow::bail!(
188 "tenant_id {tenant_id:?} contains characters outside [A-Za-z0-9_-]"
189 );
190 }
191 Ok(())
192}
193
194#[derive(Debug, Serialize, Deserialize)]
195struct ErrorEnvelope {
196 error: String,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 detail: Option<serde_json::Value>,
199}
200
201pub(crate) fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
202 let bytes = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
203 Response::from_data(bytes)
204 .with_status_code(status)
205 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
206}
207
208pub(crate) fn error_response(status: u16, msg: impl Into<String>) -> Response<std::io::Cursor<Vec<u8>>> {
209 json_response(status, &serde_json::to_value(ErrorEnvelope {
210 error: msg.into(), detail: None,
211 }).unwrap())
212}
213
214pub(crate) fn error_with_detail(status: u16, msg: impl Into<String>, detail: serde_json::Value)
215 -> Response<std::io::Cursor<Vec<u8>>>
216{
217 json_response(status, &serde_json::to_value(ErrorEnvelope {
218 error: msg.into(), detail: Some(detail),
219 }).unwrap())
220}
221
222fn write_error_response(prefix: &str, err: lex_store::StoreError)
228 -> Response<std::io::Cursor<Vec<u8>>>
229{
230 if let lex_store::StoreError::Contention { branch, attempts } = &err {
231 let body = serde_json::to_vec(&ErrorEnvelope {
232 error: format!("{prefix}: branch '{branch}' is contended (attempts={attempts})"),
233 detail: Some(serde_json::json!({
234 "kind": "contention",
235 "branch": branch,
236 "attempts": attempts,
237 })),
238 }).unwrap_or_else(|_| b"{}".to_vec());
239 return Response::from_data(body)
240 .with_status_code(503)
241 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
242 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"1"[..]).unwrap());
243 }
244 if let lex_store::StoreError::BudgetExceeded { session_id, cap, spent_after } = &err {
252 let body = serde_json::to_vec(&ErrorEnvelope {
253 error: format!(
254 "{prefix}: session `{session_id}` budget exceeded \
255 (spent_after={spent_after}, cap={cap})"
256 ),
257 detail: Some(serde_json::json!({
258 "kind": "budget_exceeded",
259 "session_id": session_id,
260 "cap": cap,
261 "spent_after": spent_after,
262 })),
263 }).unwrap_or_else(|_| b"{}".to_vec());
264 return Response::from_data(body)
265 .with_status_code(503)
266 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
267 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"0"[..]).unwrap());
268 }
269 error_response(500, format!("{prefix}: {err}"))
270}
271
272pub fn handle(state: Arc<State>, mut req: Request) -> std::io::Result<()> {
273 let method = req.method().clone();
274 let url = req.url().to_string();
275 let path = url.split('?').next().unwrap_or("").to_string();
276 let query = url.split_once('?').map(|(_, q)| q.to_string()).unwrap_or_default();
277
278 let x_lex_user = req.headers().iter()
283 .find(|h| h.field.equiv("x-lex-user"))
284 .map(|h| h.value.as_str().to_string());
285
286 if matches!(method, Method::Post) && path == "/v1/pkg/publish" {
288 let mut body_bytes: Vec<u8> = Vec::new();
289 let _ = req.as_reader().read_to_end(&mut body_bytes);
290 let resp = pkg_publish_handler(&state, &body_bytes);
291 return req.respond(resp);
292 }
293
294 let mut body = String::new();
295 let _ = req.as_reader().read_to_string(&mut body);
296
297 let resp = route(&state, &method, &path, &query, &body, x_lex_user.as_deref());
298 req.respond(resp)
299}
300
301pub fn handle_with_auth<F>(state: Arc<State>, req: Request, auth: F) -> std::io::Result<()>
305where
306 F: FnOnce(&str, &[Header]) -> bool,
307{
308 let path = req.url().split('?').next().unwrap_or("").to_string();
309 if !auth(&path, req.headers()) {
310 return req.respond(
311 Response::from_data(br#"{"error":"unauthorized"}"#.to_vec())
312 .with_status_code(401)
313 .with_header(
314 Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
315 ),
316 );
317 }
318 handle(state, req)
319}
320
321fn route(
322 state: &State,
323 method: &Method,
324 path: &str,
325 query: &str,
326 body: &str,
327 x_lex_user: Option<&str>,
328) -> Response<std::io::Cursor<Vec<u8>>> {
329 match (method, path) {
330 (Method::Get, "/") => crate::web::activity_handler(state),
332 (Method::Get, "/web/branches") => crate::web::branches_handler(state),
333 (Method::Get, "/web/trust") => crate::web::trust_handler(state),
334 (Method::Get, "/web/attention") => crate::web::attention_handler(state),
335 (Method::Get, p) if p.starts_with("/web/branch/") => {
336 let name = &p["/web/branch/".len()..];
337 crate::web::branch_handler(state, name)
338 }
339 (Method::Get, p) if p.starts_with("/web/stage/") => {
340 let id = &p["/web/stage/".len()..];
341 crate::web::stage_html_handler(state, id)
342 }
343 (Method::Post, p) if p.starts_with("/web/stage/") && (
348 p.ends_with("/pin") || p.ends_with("/defer")
349 || p.ends_with("/block") || p.ends_with("/unblock")
350 ) => {
351 let prefix_len = "/web/stage/".len();
352 let last_slash = p.rfind('/').unwrap_or(p.len());
353 let id = &p[prefix_len..last_slash];
354 let verb = &p[last_slash + 1..];
355 let decision = match verb {
356 "pin" => crate::web::WebStageDecision::Pin,
357 "defer" => crate::web::WebStageDecision::Defer,
358 "block" => crate::web::WebStageDecision::Block,
359 "unblock" => crate::web::WebStageDecision::Unblock,
360 _ => unreachable!("matched in outer guard"),
361 };
362 crate::web::stage_decision_handler(state, id, body, decision, x_lex_user)
363 }
364 (Method::Get, "/v1/health") => json_response(200, &serde_json::json!({"ok": true})),
366 (Method::Post, "/v1/parse") => parse_handler(body),
367 (Method::Post, "/v1/check") => check_handler(body),
368 (Method::Post, "/v1/publish") => publish_handler(state, body),
369 (Method::Post, "/v1/patch") => patch_handler(state, body),
370 (Method::Get, p) if p.starts_with("/v1/stage/") => {
371 let suffix = &p["/v1/stage/".len()..];
372 if let Some(id) = suffix.strip_suffix("/attestations") {
375 stage_attestations_handler(state, id)
376 } else {
377 stage_handler(state, suffix)
378 }
379 }
380 (Method::Post, "/v1/run") => run_handler(state, body, false),
381 (Method::Post, "/v1/replay") => run_handler(state, body, true),
382 (Method::Get, p) if p.starts_with("/v1/trace/") => {
383 let id = &p["/v1/trace/".len()..];
384 trace_handler(state, id)
385 }
386 (Method::Get, "/v1/diff") => diff_handler(state, query),
387 (Method::Post, "/v1/merge/start") => merge_start_handler(state, body),
388 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/resolve") => {
389 let id = &p["/v1/merge/".len()..p.len() - "/resolve".len()];
390 merge_resolve_handler(state, id, body)
391 }
392 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/commit") => {
393 let id = &p["/v1/merge/".len()..p.len() - "/commit".len()];
394 merge_commit_handler(state, id)
395 }
396 (Method::Post, "/v1/ops/batch") => ops_batch_handler(state, body),
398 (Method::Post, "/v1/attestations/batch") => attestations_batch_handler(state, body),
399 (Method::Post, "/v1/stages/batch") => crate::sync_http::stages_batch_handler(state, body),
402 (Method::Post, "/v1/stages/fetch") => crate::sync_http::stages_fetch_handler(state, body),
403 (Method::Post, "/v1/intents/batch") => crate::sync_http::intents_batch_handler(state, body),
404 (Method::Post, "/v1/intents/fetch") => crate::sync_http::intents_fetch_handler(state, body),
405 (Method::Get, "/v1/branches") => crate::branches_http::branches_list_handler(state),
409 (Method::Post, "/v1/branches") => crate::branches_http::branch_create_handler(state, body),
410 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/checkout") => {
411 let name = &p["/v1/branches/".len()..p.len() - "/checkout".len()];
412 crate::branches_http::branch_checkout_handler(state, name)
413 }
414 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
417 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
418 crate::branches_http::branch_head_handler(state, name)
419 }
420 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
421 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
422 crate::branches_http::branch_advance_head_handler(state, name, body)
423 }
424 (Method::Get, "/v1/ops/since") => ops_since_handler(state, query),
428 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
429 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
433 (Method::Put, p) if p.starts_with("/v1/pkg/") && p.ends_with("/visibility") => {
437 let name = &p["/v1/pkg/".len()..p.len() - "/visibility".len()];
438 pkg_set_visibility_handler(state, name, body)
439 }
440 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
441 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
442 pkg_head_handler(state, name)
443 }
444 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/versions") => {
445 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
446 pkg_versions_handler(state, name)
447 }
448 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
450 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
451 if let Some((name, version)) = inner.split_once('/') {
453 pkg_archive_handler(state, name, version)
454 } else {
455 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
456 }
457 }
458 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
460 let inner = &p["/v1/pkg/".len()..];
461 if let Some((name, version)) = inner.split_once('/') {
462 pkg_get_version_handler(state, name, version)
463 } else {
464 error_response(400, "expected /v1/pkg/{name}/{version}")
465 }
466 }
467 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
468 let name = &p["/v1/pkg/".len()..];
469 pkg_get_handler(state, name)
470 }
471 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
472 let name = &p["/v1/pkg/".len()..];
473 pkg_delete_handler(state, name)
474 }
475 _ => error_response(404, format!("unknown route: {method:?} {path}")),
476 }
477}
478
479#[derive(Deserialize)]
480struct ParseReq { source: String }
481
482fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
483 let req: ParseReq = match serde_json::from_str(body) {
484 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
485 };
486 match load_program_from_str(&req.source) {
487 Ok(prog) => {
488 let stages = canonicalize_program(&prog);
489 json_response(200, &serde_json::to_value(&stages).unwrap())
490 }
491 Err(e) => error_response(400, format!("syntax error: {e}")),
492 }
493}
494
495pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
496 let req: ParseReq = match serde_json::from_str(body) {
497 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
498 };
499 let prog = match load_program_from_str(&req.source) {
500 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
501 };
502 let stages = canonicalize_program(&prog);
503 match lex_types::check_program(&stages) {
504 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
505 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
506 }
507}
508
509#[derive(Deserialize)]
510struct PublishReq { source: String, #[serde(default)] activate: bool }
511
512pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
513 let req: PublishReq = match serde_json::from_str(body) {
514 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
515 };
516 let prog = match load_program_from_str(&req.source) {
517 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
518 };
519 let mut stages = canonicalize_program(&prog);
523 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
524 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
525 }
526 let example_errors = lex_runtime::evaluate_examples(&stages);
532 if !example_errors.is_empty() {
533 return error_with_detail(422, "example mismatch",
534 serde_json::to_value(&example_errors).unwrap_or_default());
535 }
536
537 let store = state.store.lock().unwrap();
538 let branch = store.current_branch();
539
540 let old_head = match store.branch_head(&branch) {
542 Ok(h) => h,
543 Err(e) => return error_response(500, format!("branch_head: {e}")),
544 };
545 let old_head_stages: Vec<lex_ast::Stage> =
547 old_head.values().filter_map(|stg| store.get_ast(stg).ok()).collect();
548 let old_fns = stage_fns(&old_head_stages);
549 let new_fns = stage_fns(&stages);
550 let old_types = stage_types(&old_head_stages);
551 let new_types = stage_types(&stages);
552 let report =
553 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
554
555 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
557 {
558 let entry = new_imports.entry("<source>".into()).or_default();
559 for s in &stages {
560 if let lex_ast::Stage::Import(im) = s {
561 entry.insert(lex_vcs::ImportRef {
562 reference: im.reference.clone(),
563 alias: im.alias.clone(),
564 });
565 }
566 }
567 }
568
569 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
570 Ok(outcome) => {
571 record_examples_for_publish(&store, &stages, &outcome);
575 json_response(200, &serde_json::json!({
576 "ops": outcome.ops,
577 "head_op": outcome.head_op,
578 }))
579 }
580 Err(lex_store::StoreError::TypeError(errs)) => {
588 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
589 }
590 Err(e) => write_error_response("publish_program", e),
591 }
592}
593
594#[derive(Deserialize)]
595struct PatchReq {
596 stage_id: String,
597 patch: lex_ast::Patch,
598 #[serde(default)] activate: bool,
599}
600
601fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
604 let req: PatchReq = match serde_json::from_str(body) {
605 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
606 };
607 let store = state.store.lock().unwrap();
608
609 let original = match store.get_ast(&req.stage_id) {
611 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
612 };
613
614 let patched = match lex_ast::apply_patch(&original, &req.patch) {
616 Ok(s) => s,
617 Err(e) => return error_with_detail(422, "patch failed",
618 serde_json::to_value(&e).unwrap_or_default()),
619 };
620
621 let branch = store.current_branch();
632
633 let sig = match lex_ast::sig_id(&patched) {
635 Some(s) => s,
636 None => return error_response(500, "patched stage has no sig_id"),
637 };
638
639 let new_id = match store.publish(&patched) {
642 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
643 };
644
645 let original_effects: std::collections::BTreeSet<String> = match &original {
647 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
648 _ => std::collections::BTreeSet::new(),
649 };
650 let patched_effects: std::collections::BTreeSet<String> = match &patched {
651 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
652 _ => std::collections::BTreeSet::new(),
653 };
654 let head_now = match store.get_branch(&branch) {
655 Ok(b) => b.and_then(|b| b.head_op),
656 Err(e) => return error_response(500, format!("get_branch: {e}")),
657 };
658 let kind = if original_effects != patched_effects {
659 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
666 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
667 lex_vcs::OperationKind::ChangeEffectSig {
668 sig_id: sig.clone(),
669 from_stage_id: req.stage_id.clone(),
670 to_stage_id: new_id.clone(),
671 from_effects: original_effects,
672 to_effects: patched_effects,
673 from_budget,
674 to_budget,
675 }
676 } else {
677 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
678 lex_vcs::OperationKind::ModifyBody {
679 sig_id: sig.clone(),
680 from_stage_id: req.stage_id.clone(),
681 to_stage_id: new_id.clone(),
682 from_budget: budget,
683 to_budget: budget,
684 }
685 };
686 let transition = lex_vcs::StageTransition::Replace {
687 sig_id: sig.clone(),
688 from: req.stage_id.clone(),
689 to: new_id.clone(),
690 };
691 let op = lex_vcs::Operation::new(
692 kind,
693 head_now.into_iter().collect::<Vec<_>>(),
694 );
695 let op_id = match store.apply_operation_gated(&branch, op, transition) {
696 Ok(id) => id,
697 Err(lex_store::StoreError::TypeError(errs)) => return error_with_detail(
698 422, "type errors after patch", serde_json::to_value(&errs).unwrap_or_default()),
699 Err(e) => return write_error_response("apply_operation_gated", e),
700 };
701 if req.activate {
702 if let Err(e) = store.activate(&new_id) {
703 return error_response(500, format!("activate: {e}"));
704 }
705 }
706
707 let status = format!("{:?}",
708 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
709 json_response(200, &serde_json::json!({
710 "old_stage_id": req.stage_id,
711 "new_stage_id": new_id,
712 "sig_id": sig,
713 "status": status,
714 "op_id": op_id,
715 }))
716}
717
718pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
719 let store = state.store.lock().unwrap();
720 let meta = match store.get_metadata(id) {
721 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
722 };
723 let ast = match store.get_ast(id) {
724 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
725 };
726 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
727 json_response(200, &serde_json::json!({
728 "metadata": meta,
729 "ast": ast,
730 "status": status,
731 }))
732}
733
734pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
743 let store = state.store.lock().unwrap();
744 if let Err(e) = store.get_metadata(id) {
745 return error_response(404, format!("{e}"));
746 }
747 let log = match store.attestation_log() {
748 Ok(l) => l,
749 Err(e) => return error_response(500, format!("attestation log: {e}")),
750 };
751 let mut listing = match log.list_for_stage(&id.to_string()) {
752 Ok(v) => v,
753 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
754 };
755 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
756 json_response(200, &serde_json::json!({"attestations": listing}))
757}
758
759#[derive(Deserialize, Default)]
760struct PolicyJson {
761 #[serde(default)] allow_effects: Vec<String>,
762 #[serde(default)] allow_fs_read: Vec<String>,
763 #[serde(default)] allow_fs_write: Vec<String>,
764 #[serde(default)] budget: Option<u64>,
765}
766
767impl PolicyJson {
768 fn into_policy(self) -> Policy {
769 Policy {
770 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
771 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
772 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
773 allow_net_host: Vec::new(),
774 allow_proc: Vec::new(),
775 allow_approval: Vec::new(),
776 budget: self.budget,
777 }
778 }
779}
780
781#[derive(Deserialize)]
782struct RunReq {
783 source: String,
784 #[serde(rename = "fn")] func: String,
785 #[serde(default)] args: Vec<serde_json::Value>,
786 #[serde(default)] policy: PolicyJson,
787 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
788}
789
790pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
791 let req: RunReq = match serde_json::from_str(body) {
792 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
793 };
794 let prog = match load_program_from_str(&req.source) {
795 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
796 };
797 let stages = canonicalize_program(&prog);
798 if let Err(errs) = lex_types::check_program(&stages) {
799 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
800 }
801 let bc = compile_program(&stages);
802 let mut policy = req.policy.into_policy();
803 if let Some(ceiling) = &state.policy_ceiling {
809 policy = clamp_policy(policy, ceiling);
810 }
811 if let Err(violations) = check_policy(&bc, &policy) {
812 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
813 }
814
815 let mut recorder = lex_trace::Recorder::new();
816 if with_overrides && !req.overrides.is_empty() {
817 recorder = recorder.with_overrides(req.overrides);
818 }
819 let handle = recorder.handle();
820 let handler = DefaultHandler::new(policy);
821 let mut vm = Vm::with_handler(&bc, Box::new(handler));
822 vm.set_tracer(Box::new(recorder));
823
824 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
825 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
826 let result = vm.call(&req.func, vargs);
827 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
828
829 let store = state.store.lock().unwrap();
830 let (root_out, root_err, status) = match &result {
831 Ok(v) => (Some(value_to_json(v)), None, 200u16),
832 Err(e) => (None, Some(format!("{e}")), 200u16),
833 };
834 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
835 root_out.clone(), root_err.clone(), started, ended);
836 let run_id = match store.save_trace(&tree) {
837 Ok(id) => id,
838 Err(e) => return error_response(500, format!("save_trace: {e}")),
839 };
840
841 let mut body = serde_json::json!({
842 "run_id": run_id,
843 "output": root_out,
844 });
845 if let Some(err) = root_err {
846 body["error"] = serde_json::Value::String(err);
847 }
848 json_response(status, &body)
849}
850
851fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
852 let store = state.store.lock().unwrap();
853 match store.load_trace(id) {
854 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
855 Err(e) => error_response(404, format!("{e}")),
856 }
857}
858
859fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
860 let mut a = None;
861 let mut b = None;
862 for kv in query.split('&') {
863 if let Some((k, v)) = kv.split_once('=') {
864 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
865 }
866 }
867 let (Some(a), Some(b)) = (a, b) else {
868 return error_response(400, "missing a or b query params");
869 };
870 let store = state.store.lock().unwrap();
871 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
872 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
873 match lex_trace::diff_runs(&ta, &tb) {
874 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
875 None => json_response(200, &serde_json::json!({"divergence": null})),
876 }
877}
878
879fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
880
881fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
882
883#[derive(Deserialize)]
884struct MergeStartReq {
885 src_branch: String,
886 dst_branch: String,
887}
888
889fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
900 let req: MergeStartReq = match serde_json::from_str(body) {
901 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
902 };
903 let store = state.store.lock().unwrap();
904 let src_head = match store.get_branch(&req.src_branch) {
905 Ok(Some(b)) => b.head_op,
906 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
907 Err(e) => return error_response(500, format!("src branch read: {e}")),
908 };
909 let dst_head = match store.get_branch(&req.dst_branch) {
910 Ok(Some(b)) => b.head_op,
911 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
912 Err(e) => return error_response(500, format!("dst branch read: {e}")),
913 };
914 let log = match lex_vcs::OpLog::open(store.root()) {
915 Ok(l) => l,
916 Err(e) => return error_response(500, format!("op log: {e}")),
917 };
918 let merge_id = mint_merge_id();
922 let session = match MergeSession::start(
923 merge_id.clone(),
924 &log,
925 src_head.as_ref(),
926 dst_head.as_ref(),
927 ) {
928 Ok(s) => s,
929 Err(e) => return error_response(500, format!("merge start: {e}")),
930 };
931 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
932 let auto_resolved_count = session.auto_resolved.len();
933 let body = serde_json::json!({
934 "merge_id": merge_id,
935 "src_head": session.src_head,
936 "dst_head": session.dst_head,
937 "lca": session.lca,
938 "conflicts": conflicts,
939 "auto_resolved_count": auto_resolved_count,
940 });
941 drop(conflicts);
942 drop(store);
943 let wrapped = ApiMergeSession {
944 inner: session,
945 src_branch: req.src_branch,
946 dst_branch: req.dst_branch,
947 };
948 state.sessions.lock().unwrap().insert(merge_id, wrapped);
949 json_response(200, &body)
950}
951
952#[derive(Deserialize)]
953struct MergeResolveReq {
954 resolutions: Vec<MergeResolveEntry>,
959}
960
961#[derive(Deserialize)]
962struct MergeResolveEntry {
963 conflict_id: String,
964 resolution: lex_vcs::Resolution,
965}
966
967fn merge_resolve_handler(
978 state: &State,
979 merge_id: &str,
980 body: &str,
981) -> Response<std::io::Cursor<Vec<u8>>> {
982 let req: MergeResolveReq = match serde_json::from_str(body) {
983 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
984 };
985 let mut sessions = state.sessions.lock().unwrap();
986 let Some(wrapped) = sessions.get_mut(merge_id) else {
987 return error_response(404, format!("unknown merge_id `{merge_id}`"));
988 };
989 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
990 .map(|e| (e.conflict_id, e.resolution))
991 .collect();
992 let store = state.store.lock().unwrap();
997 let checker = lex_store::MergeResolutionChecker::new(&store, wrapped.dst_branch.clone());
998 let verdicts = wrapped.inner.resolve_checked(pairs, &checker);
999 drop(store);
1000 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
1001 let body = serde_json::json!({
1002 "verdicts": verdicts,
1003 "remaining_conflicts": remaining,
1004 });
1005 json_response(200, &body)
1006}
1007
1008fn merge_commit_handler(
1027 state: &State,
1028 merge_id: &str,
1029) -> Response<std::io::Cursor<Vec<u8>>> {
1030 use std::collections::BTreeMap;
1031 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
1032 Some(w) => w,
1033 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
1034 };
1035 let dst_branch = wrapped.dst_branch.clone();
1036 let src_head = wrapped.inner.src_head.clone();
1037 let dst_head = wrapped.inner.dst_head.clone();
1038 let auto_resolved = wrapped.inner.auto_resolved.clone();
1039
1040 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
1043
1044 for outcome in &auto_resolved {
1046 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
1047 entries.insert(sig_id.clone(), stage_id.clone());
1048 }
1049 }
1050
1051 let resolved = match wrapped.inner.commit() {
1053 Ok(r) => r,
1054 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
1055 return error_with_detail(
1059 422,
1060 "conflicts remaining",
1061 serde_json::json!({"unresolved": ids}),
1062 );
1063 }
1064 };
1065
1066 for (conflict_id, resolution) in resolved {
1067 match resolution {
1068 lex_vcs::Resolution::TakeOurs => {
1069 }
1071 lex_vcs::Resolution::TakeTheirs => {
1072 match resolve_take_theirs(state, &src_head, &conflict_id) {
1082 Ok(stage_id) => {
1083 entries.insert(conflict_id.clone(), stage_id);
1084 }
1085 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1086 }
1087 }
1088 lex_vcs::Resolution::Custom { op } => {
1089 match op.kind.merge_target() {
1098 Some((sig, stage)) => {
1099 if sig != conflict_id {
1100 return error_with_detail(
1101 422,
1102 "custom op targets a different sig than the conflict",
1103 serde_json::json!({
1104 "conflict_id": conflict_id,
1105 "op_targets": sig,
1106 }),
1107 );
1108 }
1109 entries.insert(conflict_id, stage);
1110 }
1111 None => {
1112 return error_with_detail(
1113 422,
1114 "custom op kind doesn't yield a single sig→stage delta",
1115 serde_json::json!({
1116 "conflict_id": conflict_id,
1117 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1118 }),
1119 );
1120 }
1121 }
1122 }
1123 lex_vcs::Resolution::Defer => {
1124 return error_response(500, "internal: Defer slipped past commit gate");
1126 }
1127 }
1128 }
1129
1130 let resolved_count = entries.len();
1131 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1132 if let Some(d) = dst_head { parents.push(d); }
1133 if let Some(s) = src_head { parents.push(s); }
1134 let op = lex_vcs::Operation::new(
1135 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1136 parents,
1137 );
1138 let transition = lex_vcs::StageTransition::Merge { entries };
1139 let store = state.store.lock().unwrap();
1140 match store.apply_merge_op_gated(&dst_branch, op, transition) {
1143 Ok(new_head_op) => json_response(200, &serde_json::json!({
1144 "new_head_op": new_head_op,
1145 "dst_branch": dst_branch,
1146 })),
1147 Err(lex_store::StoreError::TypeError(errs)) => error_with_detail(
1148 422, "merged program has type errors", serde_json::to_value(&errs).unwrap_or_default()),
1149 Err(e) => write_error_response("apply merge op", e),
1150 }
1151}
1152
1153fn resolve_take_theirs(
1158 state: &State,
1159 src_head: &Option<lex_vcs::OpId>,
1160 sig: &lex_vcs::SigId,
1161) -> std::io::Result<Option<lex_vcs::StageId>> {
1162 let store = state.store.lock().unwrap();
1163 let log = lex_vcs::OpLog::open(store.root())?;
1164 let Some(head) = src_head.as_ref() else { return Ok(None); };
1165 let mut current: Option<lex_vcs::StageId> = None;
1168 for record in log.walk_forward(head, None)? {
1169 match &record.produces {
1170 lex_vcs::StageTransition::Create { sig_id, stage_id }
1171 if sig_id == sig => { current = Some(stage_id.clone()); }
1172 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1173 if sig_id == sig => { current = Some(to.clone()); }
1174 lex_vcs::StageTransition::Remove { sig_id, .. }
1175 if sig_id == sig => { current = None; }
1176 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1177 if from == sig || to == sig => {
1178 if from == sig { current = None; }
1179 if to == sig { current = Some(body_stage_id.clone()); }
1180 }
1181 lex_vcs::StageTransition::Merge { entries } => {
1182 if let Some(opt) = entries.get(sig) {
1183 current = opt.clone();
1184 }
1185 }
1186 _ => {}
1187 }
1188 }
1189 Ok(current)
1190}
1191
1192fn mint_merge_id() -> MergeSessionId {
1193 use std::sync::atomic::{AtomicU64, Ordering};
1194 static COUNTER: AtomicU64 = AtomicU64::new(0);
1195 let nanos = SystemTime::now()
1196 .duration_since(UNIX_EPOCH)
1197 .map(|d| d.as_nanos())
1198 .unwrap_or(0);
1199 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1200 format!("merge_{nanos:x}_{n:x}")
1201}
1202
1203pub(crate) fn ops_batch_handler(state: &State, body: &str)
1234 -> Response<std::io::Cursor<Vec<u8>>>
1235{
1236 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1237 Ok(r) => r,
1238 Err(e) => return error_response(400,
1239 format!("body must be a JSON array of OperationRecord: {e}")),
1240 };
1241 let store = state.store.lock().unwrap();
1242 let log = match lex_vcs::OpLog::open(store.root()) {
1243 Ok(l) => l,
1244 Err(e) => return error_response(500, format!("opening op log: {e}")),
1245 };
1246
1247 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1255 std::collections::BTreeSet::new();
1256 for rec in &records {
1257 let expected = rec.op.op_id();
1258 if expected != rec.op_id {
1259 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1260 "supplied": rec.op_id,
1261 "expected": expected,
1262 }));
1263 }
1264 for parent in &rec.op.parents {
1265 let known = match log.get(parent) {
1266 Ok(Some(_)) => true,
1267 Ok(None) => false,
1268 Err(e) => return error_response(500, format!("op log read: {e}")),
1269 };
1270 if !known && !batch_ids.contains(parent) {
1271 return error_with_detail(422, "MissingParent", serde_json::json!({
1272 "op_id": rec.op_id,
1273 "missing_parent": parent,
1274 }));
1275 }
1276 }
1277 batch_ids.insert(rec.op_id.clone());
1278 }
1279
1280 let mut added = 0usize;
1283 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1284 for rec in &records {
1285 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1286 match log.put(rec) {
1287 Ok(()) => {
1288 if !already_present {
1289 added += 1;
1290 added_ids.push(&rec.op_id);
1291 }
1292 }
1293 Err(e) => return error_response(500, format!("op log write: {e}")),
1294 }
1295 }
1296
1297 json_response(200, &serde_json::json!({
1298 "received": records.len(),
1299 "added": added,
1300 "skipped": records.len() - added,
1301 "added_ids": added_ids,
1302 }))
1303}
1304
1305pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1327 -> Response<std::io::Cursor<Vec<u8>>>
1328{
1329 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1330 Ok(a) => a,
1331 Err(e) => return error_response(400,
1332 format!("body must be a JSON array of Attestation: {e}")),
1333 };
1334 let store = state.store.lock().unwrap();
1335 let log = match store.attestation_log() {
1336 Ok(l) => l,
1337 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1338 };
1339 let op_log = match lex_vcs::OpLog::open(store.root()) {
1340 Ok(l) => l,
1341 Err(e) => return error_response(500, format!("opening op log: {e}")),
1342 };
1343
1344 for att in &attestations {
1346 let expected = lex_vcs::Attestation::with_timestamp(
1349 att.stage_id.clone(),
1350 att.op_id.clone(),
1351 att.intent_id.clone(),
1352 att.kind.clone(),
1353 att.result.clone(),
1354 att.produced_by.clone(),
1355 att.cost.clone(),
1356 att.timestamp,
1357 ).attestation_id;
1358 if expected != att.attestation_id {
1359 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1360 "supplied": att.attestation_id,
1361 "expected": expected,
1362 }));
1363 }
1364 if let Some(op_id) = &att.op_id {
1368 match op_log.get(op_id) {
1369 Ok(Some(_)) => {}
1370 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1371 "attestation_id": att.attestation_id,
1372 "op_id": op_id,
1373 })),
1374 Err(e) => return error_response(500, format!("op log read: {e}")),
1375 }
1376 }
1377 }
1378
1379 let mut added = 0usize;
1383 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1384 for att in &attestations {
1385 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1386 match log.put(att) {
1387 Ok(()) => {
1388 if !already_present {
1389 added += 1;
1390 added_ids.push(&att.attestation_id);
1391 }
1392 }
1393 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1394 }
1395 }
1396
1397 json_response(200, &serde_json::json!({
1398 "received": attestations.len(),
1399 "added": added,
1400 "skipped": attestations.len() - added,
1401 "added_ids": added_ids,
1402 }))
1403}
1404
1405pub(crate) fn ops_since_handler(state: &State, query: &str)
1437 -> Response<std::io::Cursor<Vec<u8>>>
1438{
1439 let mut after: Option<String> = None;
1440 let mut branch = String::from("main");
1441 let mut limit: Option<usize> = None;
1442 for kv in query.split('&') {
1443 let Some((k, v)) = kv.split_once('=') else { continue };
1444 match k {
1445 "after" => after = Some(v.to_string()),
1446 "branch" => branch = v.to_string(),
1447 "limit" => {
1448 limit = Some(match v.parse::<usize>() {
1449 Ok(n) => n,
1450 Err(_) => return error_response(400,
1451 format!("limit must be a positive integer, got `{v}`")),
1452 });
1453 }
1454 _ => {}
1455 }
1456 }
1457
1458 let store = state.store.lock().unwrap();
1459 let log = match lex_vcs::OpLog::open(store.root()) {
1460 Ok(l) => l,
1461 Err(e) => return error_response(500, format!("opening op log: {e}")),
1462 };
1463 let head = match store.get_branch(&branch) {
1464 Ok(Some(b)) => b.head_op,
1465 Ok(None) => None,
1466 Err(e) => return error_response(500, format!("get_branch: {e}")),
1467 };
1468 let Some(head) = head else {
1469 return json_response(200, &serde_json::json!([]));
1470 };
1471
1472 let ops_since = match log.ops_since(&head, after.as_ref()) {
1473 Ok(o) => o,
1474 Err(e) => return error_response(500, format!("ops_since: {e}")),
1475 };
1476 let mut ops = ops_since;
1480 ops.reverse();
1481 if let Some(n) = limit {
1482 ops.truncate(n);
1483 }
1484
1485 json_response(200, &serde_json::to_value(&ops).unwrap_or_default())
1486}
1487
1488pub(crate) fn attestations_since_handler(state: &State, query: &str)
1501 -> Response<std::io::Cursor<Vec<u8>>>
1502{
1503 let mut after_op: Option<String> = None;
1504 let mut limit: Option<usize> = None;
1505 for kv in query.split('&') {
1506 let Some((k, v)) = kv.split_once('=') else { continue };
1507 match k {
1508 "after-op" => after_op = Some(v.to_string()),
1509 "limit" => {
1510 limit = Some(match v.parse::<usize>() {
1511 Ok(n) => n,
1512 Err(_) => return error_response(400,
1513 format!("limit must be a positive integer, got `{v}`")),
1514 });
1515 }
1516 _ => {}
1517 }
1518 }
1519
1520 let store = state.store.lock().unwrap();
1521 let log = match store.attestation_log() {
1522 Ok(l) => l,
1523 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1524 };
1525
1526 let exclude: std::collections::BTreeSet<String> = match &after_op {
1530 None => std::collections::BTreeSet::new(),
1531 Some(cutoff) => {
1532 let op_log = match lex_vcs::OpLog::open(store.root()) {
1533 Ok(l) => l,
1534 Err(e) => return error_response(500, format!("opening op log: {e}")),
1535 };
1536 match op_log.walk_back(cutoff, None) {
1537 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1538 Err(_) => {
1539 std::collections::BTreeSet::new()
1543 }
1544 }
1545 }
1546 };
1547
1548 let all = match log.list_all() {
1549 Ok(v) => v,
1550 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1551 };
1552 let mut filtered: Vec<lex_vcs::Attestation> = all
1553 .into_iter()
1554 .filter(|a| match &a.op_id {
1555 Some(op_id) => !exclude.contains(op_id),
1556 None => true,
1560 })
1561 .collect();
1562 filtered.sort_by(|a, b| {
1566 a.timestamp.cmp(&b.timestamp)
1567 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1568 });
1569 if let Some(n) = limit {
1570 filtered.truncate(n);
1571 }
1572
1573 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1574}
1575
1576#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1580struct PkgRecord {
1581 name: String,
1582 version: String,
1583 head_op: Option<String>,
1584 published_at: u64,
1585 function_names: Vec<String>,
1587 ops: Vec<serde_json::Value>,
1589}
1590
1591#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
1598#[serde(rename_all = "lowercase")]
1599pub enum Visibility {
1600 #[default]
1601 Private,
1602 Public,
1603}
1604
1605#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1610struct PkgIndex {
1611 latest: Option<String>,
1613 versions: Vec<PkgVersionSummary>,
1615 #[serde(default)]
1619 visibility: Visibility,
1620}
1621
1622#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1623struct PkgVersionSummary {
1624 version: String,
1625 head_op: Option<String>,
1626 published_at: u64,
1627}
1628
1629fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1630 root.join("packages").join(name)
1631}
1632
1633fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
1634 pkg_name_dir(root, name).join("index.json")
1635}
1636
1637fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1638 pkg_name_dir(root, name).join(format!("{version}.json"))
1639}
1640
1641fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1642 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
1643}
1644
1645fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
1646 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
1647 serde_json::from_slice(&bytes).ok()
1648}
1649
1650fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
1651 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
1652 serde_json::from_slice(&bytes).ok()
1653}
1654
1655fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1656 let index = load_pkg_index(root, name)?;
1657 let latest = index.latest.clone()?;
1658 load_pkg_record(root, name, &latest)
1659}
1660
1661fn pkg_is_public(root: &std::path::Path, name: &str) -> bool {
1665 load_pkg_index(root, name).map(|i| i.visibility) == Some(Visibility::Public)
1666}
1667
1668fn valid_pkg_segment(s: &str) -> bool {
1673 !s.is_empty()
1674 && s.len() <= 128
1675 && s != "."
1676 && s != ".."
1677 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
1678}
1679
1680#[derive(Deserialize)]
1681struct VisibilityReq {
1682 visibility: Visibility,
1683}
1684
1685fn pkg_set_visibility_handler(
1692 state: &State,
1693 name: &str,
1694 body: &str,
1695) -> Response<std::io::Cursor<Vec<u8>>> {
1696 if !valid_pkg_segment(name) {
1697 return error_response(400, format!("invalid package name {name:?}"));
1698 }
1699 let req: VisibilityReq = match serde_json::from_str(body) {
1700 Ok(r) => r,
1701 Err(e) => return error_response(400, format!("bad request: {e}")),
1702 };
1703 let mut index = match load_pkg_index(&state.root, name) {
1704 Some(i) => i,
1705 None => return error_response(404, format!("package {name:?} not found")),
1706 };
1707 index.visibility = req.visibility;
1708 let bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1709 match std::fs::write(pkg_index_path(&state.root, name), bytes) {
1710 Ok(()) => json_response(
1711 200,
1712 &serde_json::json!({ "name": name, "visibility": index.visibility }),
1713 ),
1714 Err(e) => error_response(500, format!("write index: {e}")),
1715 }
1716}
1717
1718fn public_pkg_names(root: &std::path::Path) -> Vec<String> {
1721 list_pkg_names(root)
1722 .into_iter()
1723 .filter(|name| pkg_is_public(root, name))
1724 .collect()
1725}
1726
1727fn public_pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
1731 let packages: Vec<serde_json::Value> = public_pkg_names(&state.root)
1732 .iter()
1733 .filter_map(|name| {
1734 let r = load_latest_pkg_record(&state.root, name)?;
1735 Some(serde_json::json!({
1736 "name": r.name,
1737 "version": r.version,
1738 "head_op": r.head_op,
1739 "published_at": r.published_at,
1740 }))
1741 })
1742 .collect();
1743 json_response(200, &serde_json::json!({ "packages": packages }))
1744}
1745
1746#[derive(Debug, PartialEq, Eq)]
1751enum PublicTarget {
1752 List,
1753 Latest(String),
1754 Versions(String),
1755 Head(String),
1756 Version(String, String),
1757 Archive(String, String),
1758}
1759
1760impl PublicTarget {
1761 fn pkg_name(&self) -> Option<&str> {
1763 match self {
1764 PublicTarget::List => None,
1765 PublicTarget::Latest(n)
1766 | PublicTarget::Versions(n)
1767 | PublicTarget::Head(n)
1768 | PublicTarget::Version(n, _)
1769 | PublicTarget::Archive(n, _) => Some(n),
1770 }
1771 }
1772}
1773
1774fn resolve_public(method: &Method, path: &str) -> Result<PublicTarget, u16> {
1779 if !matches!(method, Method::Get) {
1780 return Err(405);
1781 }
1782 let rest = path.trim_matches('/');
1783 if rest.is_empty() {
1784 return Ok(PublicTarget::List);
1785 }
1786 let segs: Vec<&str> = rest.split('/').collect();
1787 if !segs.iter().all(|s| valid_pkg_segment(s)) {
1788 return Err(404);
1789 }
1790 match segs.as_slice() {
1791 [n] => Ok(PublicTarget::Latest(n.to_string())),
1792 [n, "versions"] => Ok(PublicTarget::Versions(n.to_string())),
1793 [n, "head"] => Ok(PublicTarget::Head(n.to_string())),
1794 [n, v, "archive"] => Ok(PublicTarget::Archive(n.to_string(), v.to_string())),
1795 [n, v] => Ok(PublicTarget::Version(n.to_string(), v.to_string())),
1796 _ => Err(404),
1797 }
1798}
1799
1800pub fn route_public(
1811 state: &State,
1812 method: &Method,
1813 path: &str,
1814 _query: &str,
1815) -> Response<std::io::Cursor<Vec<u8>>> {
1816 let target = match resolve_public(method, path) {
1817 Ok(t) => t,
1818 Err(405) => return error_response(405, "public read is GET-only"),
1819 Err(_) => return error_response(404, "not found"),
1820 };
1821 if let PublicTarget::List = target {
1823 return public_pkg_list_handler(state);
1824 }
1825 if let Some(name) = target.pkg_name() {
1827 if !pkg_is_public(&state.root, name) {
1828 return error_response(404, format!("package {name:?} not found"));
1829 }
1830 }
1831 match target {
1832 PublicTarget::List => unreachable!("handled above"),
1833 PublicTarget::Latest(n) => pkg_get_handler(state, &n),
1834 PublicTarget::Versions(n) => pkg_versions_handler(state, &n),
1835 PublicTarget::Head(n) => pkg_head_handler(state, &n),
1836 PublicTarget::Version(n, v) => pkg_get_version_handler(state, &n, &v),
1837 PublicTarget::Archive(n, v) => pkg_archive_handler(state, &n, &v),
1838 }
1839}
1840
1841fn save_pkg_record(
1842 root: &std::path::Path,
1843 record: &PkgRecord,
1844 archive: &[u8],
1845) -> std::io::Result<()> {
1846 let dir = pkg_name_dir(root, &record.name);
1847 std::fs::create_dir_all(&dir)?;
1848
1849 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
1851 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
1852
1853 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
1855
1856 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
1858 index.latest = Some(record.version.clone());
1859 if !index.versions.iter().any(|v| v.version == record.version) {
1860 index.versions.push(PkgVersionSummary {
1861 version: record.version.clone(),
1862 head_op: record.head_op.clone(),
1863 published_at: record.published_at,
1864 });
1865 }
1866 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1867 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
1868}
1869
1870fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
1871 let dir = root.join("packages");
1872 let Ok(entries) = std::fs::read_dir(&dir) else {
1873 return Vec::new();
1874 };
1875 let mut names: Vec<String> = entries
1876 .filter_map(|e| e.ok())
1877 .filter(|e| e.path().is_dir())
1878 .filter_map(|e| e.file_name().into_string().ok())
1879 .collect();
1880 names.sort();
1881 names
1882}
1883
1884fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
1885 let Ok(entries) = std::fs::read_dir(dir) else { return };
1886 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
1887 entries.sort_by_key(|e| e.path());
1888 for entry in entries {
1889 let path = entry.path();
1890 if path.is_dir() {
1891 collect_lex_files(&path, out);
1892 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
1893 out.push(path);
1894 }
1895 }
1896}
1897
1898fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
1901 let tmp = match tempfile::TempDir::new() {
1902 Ok(t) => t,
1903 Err(e) => return error_response(500, format!("create temp dir: {e}")),
1904 };
1905 {
1906 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
1907 let mut ar = tar::Archive::new(gz);
1908 if let Err(e) = ar.unpack(tmp.path()) {
1909 return error_response(400, format!("unpack archive: {e}"));
1910 }
1911 }
1912
1913 let toml_path = tmp.path().join("lex.toml");
1914 if !toml_path.exists() {
1915 return error_response(400, "archive must contain lex.toml at root");
1916 }
1917 let manifest = match Manifest::load(&toml_path) {
1918 Ok(m) => m,
1919 Err(e) => return error_response(400, format!("lex.toml: {e}")),
1920 };
1921 let (pkg_name, pkg_version) = match &manifest.package {
1922 Some(m) => (m.name.clone(), m.version.clone()),
1923 None => return error_response(400, "lex.toml must have a [package] section"),
1924 };
1925
1926 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
1930 return error_response(
1931 409,
1932 format!(
1933 "package {pkg_name}@{pkg_version} already published; \
1934 bump the version in lex.toml to publish a new release"
1935 ),
1936 );
1937 }
1938
1939 let src_dir = tmp.path().join("src");
1940 if !src_dir.exists() {
1941 return error_response(400, "archive must contain a src/ directory");
1942 }
1943 let mut lex_files: Vec<PathBuf> = Vec::new();
1944 collect_lex_files(&src_dir, &mut lex_files);
1945 if lex_files.is_empty() {
1946 return error_response(400, "no .lex files found in src/");
1947 }
1948
1949 let store = state.store.lock().unwrap();
1950 let branch = store.current_branch();
1951
1952 let old_head = match store.branch_head(&branch) {
1972 Ok(h) => h,
1973 Err(e) => return error_response(500, format!("branch_head: {e}")),
1974 };
1975 let old_pairs: Vec<(String, String)> =
1982 old_head.iter().map(|(sig, stage)| (sig.clone(), stage.clone())).collect();
1983 let mut old_fns_by_name: BTreeMap<String, Vec<lex_ast::FnDecl>> = BTreeMap::new();
1984 for fd in store.get_asts_for_sigs_bulk(&old_pairs)
1985 .into_iter()
1986 .filter_map(|r| r.ok())
1987 .filter_map(|s| match s { lex_ast::Stage::FnDecl(fd) => Some(fd), _ => None })
1988 {
1989 old_fns_by_name.entry(fd.name.clone()).or_default().push(fd);
1990 }
1991 let mut old_types_by_name: BTreeMap<String, lex_ast::TypeDecl> = BTreeMap::new();
1994 for td in store.get_asts_for_sigs_bulk(&old_pairs)
1995 .into_iter()
1996 .filter_map(|r| r.ok())
1997 .filter_map(|s| match s { lex_ast::Stage::TypeDecl(td) => Some(td), _ => None })
1998 {
1999 old_types_by_name.insert(td.name.clone(), td);
2000 }
2001 fn structural_key(fd: &lex_ast::FnDecl) -> Option<String> {
2009 let mut anon = fd.clone();
2010 anon.name = String::new();
2011 lex_ast::sig_id(&lex_ast::Stage::FnDecl(anon))
2012 }
2013
2014 fn take_matching(
2024 map: &mut BTreeMap<String, Vec<lex_ast::FnDecl>>,
2025 name: &str,
2026 new_fd: &lex_ast::FnDecl,
2027 ) -> Option<lex_ast::FnDecl> {
2028 let candidates = map.get_mut(name)?;
2029 let idx = match candidates.len() {
2030 0 => return None,
2031 1 => 0,
2032 _ => {
2033 let want = structural_key(new_fd);
2034 candidates.iter().position(|c| structural_key(c) == want)?
2035 }
2036 };
2037 let matched = candidates.remove(idx);
2038 if candidates.is_empty() {
2039 map.remove(name);
2040 }
2041 Some(matched)
2042 }
2043
2044 let loaded = match load_package(&lex_files, tmp.path(), &pkg_name) {
2064 Ok(p) => p,
2065 Err(e) => return error_response(400, format!("load package: {e}")),
2066 };
2067 let mut stages = canonicalize_program(&loaded.program);
2068 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
2074 return error_with_detail(
2075 422,
2076 format!("type errors in package {pkg_name}"),
2077 serde_json::to_value(&errs).unwrap(),
2078 );
2079 }
2080 let new_fns = stage_fns(&stages);
2081 let all_function_names: Vec<String> = new_fns.keys().cloned().collect();
2082
2083 let mut old_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2088 for (name, new_fd) in &new_fns {
2089 if let Some(fd) = take_matching(&mut old_fns_by_name, name, new_fd) {
2090 old_fns.insert(name.clone(), fd);
2091 }
2092 }
2093 let new_types = stage_types(&stages);
2094 let old_types: BTreeMap<String, lex_ast::TypeDecl> = new_types
2098 .keys()
2099 .filter_map(|n| old_types_by_name.get(n).map(|td| (n.clone(), td.clone())))
2100 .collect();
2101 let report =
2102 lex_vcs::compute_diff_with_types(&old_fns, &new_fns, &old_types, &new_types, false);
2103
2104 let mut new_imports = lex_vcs::ImportMap::new();
2114 for (file, modules) in &loaded.imports_by_file {
2115 let entry = new_imports.entry(file.clone()).or_default();
2116 for m in modules {
2117 entry.insert(lex_vcs::ImportRef {
2118 reference: m.clone(),
2119 alias: lex_vcs::default_import_alias(m),
2120 });
2121 }
2122 }
2123
2124 let outcome = match store.publish_program(&branch, &stages, &report, &new_imports, false) {
2125 Ok(outcome) => outcome,
2126 Err(lex_store::StoreError::TypeError(errs)) => {
2127 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
2128 }
2129 Err(e) => return write_error_response("publish_program", e),
2130 };
2131 let all_ops: Vec<serde_json::Value> = match serde_json::to_value(&outcome.ops) {
2132 Ok(serde_json::Value::Array(arr)) => arr,
2133 _ => Vec::new(),
2134 };
2135 let final_head_op = outcome.head_op;
2136
2137 let now = SystemTime::now()
2162 .duration_since(UNIX_EPOCH)
2163 .map(|d| d.as_secs())
2164 .unwrap_or(0);
2165 let record = PkgRecord {
2166 name: pkg_name.clone(),
2167 version: pkg_version,
2168 head_op: final_head_op.clone(),
2169 published_at: now,
2170 function_names: all_function_names,
2171 ops: all_ops.clone(),
2172 };
2173 if let Err(e) = save_pkg_record(&state.root, &record, body) {
2174 return error_response(500, format!("save package index: {e}"));
2175 }
2176
2177 json_response(200, &serde_json::json!({
2178 "package": pkg_name,
2179 "ops": all_ops,
2180 "head_op": final_head_op,
2181 }))
2182}
2183
2184fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2186 let names = list_pkg_names(&state.root);
2187 let packages: Vec<serde_json::Value> = names.iter()
2188 .filter_map(|name| {
2189 let idx = load_pkg_index(&state.root, name)?;
2190 let latest = idx.latest.as_deref()?;
2191 let r = load_pkg_record(&state.root, name, latest)?;
2192 Some(serde_json::json!({
2193 "name": r.name,
2194 "version": r.version,
2195 "head_op": r.head_op,
2196 "published_at": r.published_at,
2197 }))
2198 })
2199 .collect();
2200 json_response(200, &serde_json::json!({ "packages": packages }))
2201}
2202
2203fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2205 match load_latest_pkg_record(&state.root, name) {
2206 Some(r) => json_response(200, &serde_json::json!({
2207 "name": r.name,
2208 "version": r.version,
2209 "head_op": r.head_op,
2210 "published_at": r.published_at,
2211 "function_names": r.function_names,
2212 "ops": r.ops,
2213 })),
2214 None => error_response(404, format!("package {name:?} not found")),
2215 }
2216}
2217
2218fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2220 match load_pkg_index(&state.root, name) {
2221 Some(idx) => json_response(200, &serde_json::json!({
2222 "name": name,
2223 "latest": idx.latest,
2224 "versions": idx.versions,
2225 })),
2226 None => error_response(404, format!("package {name:?} not found")),
2227 }
2228}
2229
2230fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2232 match load_pkg_record(&state.root, name, version) {
2233 Some(r) => json_response(200, &serde_json::json!({
2234 "name": r.name,
2235 "version": r.version,
2236 "head_op": r.head_op,
2237 "published_at": r.published_at,
2238 "function_names": r.function_names,
2239 "ops": r.ops,
2240 })),
2241 None => error_response(404, format!("package {name:?}@{version:?} not found")),
2242 }
2243}
2244
2245fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2247 let path = pkg_archive_path(&state.root, name, version);
2248 match std::fs::read(&path) {
2249 Ok(bytes) => Response::from_data(bytes)
2250 .with_status_code(200)
2251 .with_header(
2252 tiny_http::Header::from_bytes(
2253 &b"Content-Type"[..],
2254 &b"application/gzip"[..],
2255 )
2256 .unwrap(),
2257 ),
2258 Err(_) => error_response(404, format!("archive for {name:?}@{version:?} not found")),
2259 }
2260}
2261
2262fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2264 match load_latest_pkg_record(&state.root, name) {
2265 Some(r) => json_response(200, &serde_json::json!({
2266 "name": r.name,
2267 "version": r.version,
2268 "head_op": r.head_op,
2269 })),
2270 None => error_response(404, format!("package {name:?} not found")),
2271 }
2272}
2273
2274fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2276 let record = match load_latest_pkg_record(&state.root, name) {
2277 Some(r) => r,
2278 None => return error_response(404, format!("package {name:?} not found")),
2279 };
2280
2281 let store = state.store.lock().unwrap();
2282 let branch = store.current_branch();
2283
2284 let head = match store.branch_head(&branch) {
2285 Ok(h) => h,
2286 Err(e) => return error_response(500, format!("branch_head: {e}")),
2287 };
2288
2289 let head_pairs: Vec<(String, String)> = head
2296 .iter()
2297 .map(|(sig, stage)| (sig.clone(), stage.clone()))
2298 .collect();
2299 let old_fns: BTreeMap<String, lex_ast::FnDecl> = store
2300 .get_asts_for_sigs_bulk(&head_pairs)
2301 .into_iter()
2302 .filter_map(|r| r.ok())
2303 .filter_map(|s| match s {
2304 lex_ast::Stage::FnDecl(fd)
2305 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
2306 _ => None,
2307 })
2308 .collect();
2309
2310 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2311 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2312 let empty_imports = lex_vcs::ImportMap::new();
2313
2314 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
2315 Ok(outcome) => {
2316 let ver = record.version.clone();
2318 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
2319 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
2320 if let Some(mut idx) = load_pkg_index(&state.root, name) {
2322 idx.versions.retain(|v| v.version != ver);
2323 idx.latest = idx.versions.last().map(|v| v.version.clone());
2324 if idx.versions.is_empty() {
2325 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
2326 } else {
2327 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
2328 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
2329 }
2330 }
2331 json_response(200, &serde_json::json!({
2332 "deleted": name,
2333 "version": ver,
2334 "ops": outcome.ops,
2335 "head_op": outcome.head_op,
2336 }))
2337 }
2338 Err(lex_store::StoreError::TypeError(errs)) => {
2339 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
2340 }
2341 Err(e) => write_error_response("retract package", e),
2342 }
2343}
2344
2345#[cfg(test)]
2346mod policy_ceiling_tests {
2347 use super::*;
2348 use lex_runtime::Policy;
2349 use std::path::PathBuf;
2350
2351 fn permissive_request() -> Policy {
2355 Policy {
2356 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
2357 .iter()
2358 .map(|s| s.to_string())
2359 .collect(),
2360 allow_fs_read: vec![PathBuf::from("/")],
2361 allow_fs_write: vec![PathBuf::from("/")],
2362 allow_net_host: Vec::new(),
2363 allow_proc: Vec::new(),
2364 allow_approval: Vec::new(),
2365 budget: None,
2366 }
2367 }
2368
2369 #[test]
2370 fn ceiling_drops_effects_the_caller_was_not_granted() {
2371 let ceiling = Policy {
2372 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
2373 ..Policy::default()
2374 };
2375 let got = clamp_policy(permissive_request(), &ceiling);
2376 assert!(got.allow_effects.contains("io"));
2377 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
2378 assert!(!got.allow_effects.contains("fs_write"));
2379 assert!(!got.allow_effects.contains("net"));
2380 assert!(!got.allow_effects.contains("time"));
2382 }
2383
2384 #[test]
2385 fn ceiling_scopes_override_caller_scopes() {
2386 let ceiling = Policy {
2387 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
2388 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
2389 ..Policy::default()
2390 };
2391 let got = clamp_policy(permissive_request(), &ceiling);
2392 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
2395 assert!(got.allow_fs_write.is_empty());
2396 assert!(got.allow_proc.is_empty());
2397 assert!(got.allow_net_host.is_empty());
2398 }
2399
2400 #[test]
2401 fn ceiling_caps_budget_and_prefers_the_smaller() {
2402 let mut req = permissive_request();
2404 req.budget = None;
2405 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
2406 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
2407
2408 let mut req2 = permissive_request();
2410 req2.budget = Some(50);
2411 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
2412 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
2413 }
2414
2415 #[test]
2416 fn empty_ceiling_is_pure_only() {
2417 let got = clamp_policy(permissive_request(), &Policy::default());
2418 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
2419 assert!(got.allow_proc.is_empty());
2420 assert!(got.allow_fs_write.is_empty());
2421 }
2422}
2423
2424#[cfg(test)]
2425mod public_read_tests {
2426 use super::*;
2427
2428 fn seed_pkg(root: &std::path::Path, name: &str, version: &str) {
2431 let record = PkgRecord {
2432 name: name.to_string(),
2433 version: version.to_string(),
2434 head_op: Some(format!("op-{name}")),
2435 published_at: 1,
2436 function_names: vec![format!("{name}.f")],
2437 ops: vec![],
2438 };
2439 save_pkg_record(root, &record, format!("ARCHIVE:{name}@{version}").as_bytes())
2440 .expect("seed package");
2441 }
2442
2443 #[test]
2444 fn new_package_defaults_to_private() {
2445 let tmp = tempfile::TempDir::new().unwrap();
2446 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2447 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2448 assert!(!pkg_is_public(tmp.path(), "does-not-exist"));
2450 }
2451
2452 #[test]
2453 fn set_visibility_round_trips_and_index_persists() {
2454 let tmp = tempfile::TempDir::new().unwrap();
2455 let state = State::open(tmp.path().to_path_buf()).unwrap();
2456 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2457
2458 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"public"}"#);
2459 assert!(pkg_is_public(tmp.path(), "lex-schema"));
2460 let idx = load_pkg_index(tmp.path(), "lex-schema").unwrap();
2462 assert_eq!(idx.latest.as_deref(), Some("0.9.2"));
2463 assert_eq!(idx.versions.len(), 1);
2464
2465 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"private"}"#);
2466 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2467 }
2468
2469 #[test]
2470 fn set_visibility_on_unknown_package_is_a_noop() {
2471 let tmp = tempfile::TempDir::new().unwrap();
2472 let state = State::open(tmp.path().to_path_buf()).unwrap();
2473 let _ = pkg_set_visibility_handler(&state, "ghost", r#"{"visibility":"public"}"#);
2475 assert!(load_pkg_index(tmp.path(), "ghost").is_none());
2476 }
2477
2478 #[test]
2479 fn public_listing_omits_private_packages() {
2480 let tmp = tempfile::TempDir::new().unwrap();
2481 let state = State::open(tmp.path().to_path_buf()).unwrap();
2482 seed_pkg(tmp.path(), "pub-pkg", "1.0.0");
2483 seed_pkg(tmp.path(), "priv-pkg", "1.0.0");
2484 let _ = pkg_set_visibility_handler(&state, "pub-pkg", r#"{"visibility":"public"}"#);
2485
2486 let names = public_pkg_names(tmp.path());
2487 assert_eq!(names, vec!["pub-pkg".to_string()]);
2488 }
2489
2490 #[test]
2491 fn resolve_public_maps_routes() {
2492 let get = Method::Get;
2493 assert_eq!(resolve_public(&get, "").unwrap(), PublicTarget::List);
2494 assert_eq!(resolve_public(&get, "/").unwrap(), PublicTarget::List);
2495 assert_eq!(
2496 resolve_public(&get, "/lex-schema").unwrap(),
2497 PublicTarget::Latest("lex-schema".into())
2498 );
2499 assert_eq!(
2500 resolve_public(&get, "/lex-schema/versions").unwrap(),
2501 PublicTarget::Versions("lex-schema".into())
2502 );
2503 assert_eq!(
2504 resolve_public(&get, "/lex-schema/head").unwrap(),
2505 PublicTarget::Head("lex-schema".into())
2506 );
2507 assert_eq!(
2508 resolve_public(&get, "/lex-schema/0.9.2").unwrap(),
2509 PublicTarget::Version("lex-schema".into(), "0.9.2".into())
2510 );
2511 assert_eq!(
2512 resolve_public(&get, "/lex-schema/0.9.2/archive").unwrap(),
2513 PublicTarget::Archive("lex-schema".into(), "0.9.2".into())
2514 );
2515 }
2516
2517 #[test]
2518 fn resolve_public_rejects_bad_method_and_traversal() {
2519 assert_eq!(resolve_public(&Method::Put, "/lex-schema"), Err(405));
2521 assert_eq!(resolve_public(&Method::Post, "").err(), Some(405));
2522 assert_eq!(resolve_public(&Method::Get, "/.."), Err(404));
2524 assert_eq!(resolve_public(&Method::Get, "/lex-schema/../etc"), Err(404));
2525 assert_eq!(resolve_public(&Method::Get, "/a/b/c/d"), Err(404));
2526 assert!(resolve_public(&Method::Get, "/lex schema").is_err());
2528 }
2529}