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
23pub struct State {
24 pub store: Mutex<Store>,
25 pub root: PathBuf,
30 pub sessions: Mutex<HashMap<MergeSessionId, ApiMergeSession>>,
37 pub policy_ceiling: Option<Policy>,
58}
59
60pub struct ApiMergeSession {
67 pub inner: MergeSession,
68 pub src_branch: String,
69 pub dst_branch: String,
70}
71
72impl State {
73 pub fn open(root: PathBuf) -> anyhow::Result<Self> {
74 Self::open_with_ceiling(root, None)
75 }
76
77 pub fn open_with_ceiling(
82 root: PathBuf,
83 policy_ceiling: Option<Policy>,
84 ) -> anyhow::Result<Self> {
85 Ok(Self {
86 store: Mutex::new(Store::open(&root)?),
87 root,
88 sessions: Mutex::new(HashMap::new()),
89 policy_ceiling,
90 })
91 }
92
93 pub fn new_with_tenant(tenant_id: &str, store_root: PathBuf) -> anyhow::Result<Self> {
103 validate_tenant_id(tenant_id)?;
104 Self::open(store_root.join(tenant_id))
105 }
106
107 pub fn new_with_tenant_and_ceiling(
112 tenant_id: &str,
113 store_root: PathBuf,
114 policy_ceiling: Option<Policy>,
115 ) -> anyhow::Result<Self> {
116 validate_tenant_id(tenant_id)?;
117 Self::open_with_ceiling(store_root.join(tenant_id), policy_ceiling)
118 }
119}
120
121fn clamp_policy(requested: Policy, ceiling: &Policy) -> Policy {
137 let allow_effects: BTreeSet<String> = requested
138 .allow_effects
139 .intersection(&ceiling.allow_effects)
140 .cloned()
141 .collect();
142 let budget = match (requested.budget, ceiling.budget) {
143 (Some(r), Some(c)) => Some(r.min(c)),
144 (None, Some(c)) => Some(c),
145 (Some(r), None) => Some(r),
146 (None, None) => None,
147 };
148 Policy {
149 allow_effects,
150 allow_fs_read: ceiling.allow_fs_read.clone(),
151 allow_fs_write: ceiling.allow_fs_write.clone(),
152 allow_net_host: ceiling.allow_net_host.clone(),
153 allow_proc: ceiling.allow_proc.clone(),
154 allow_approval: ceiling.allow_approval.clone(),
155 budget,
156 }
157}
158
159fn validate_tenant_id(tenant_id: &str) -> anyhow::Result<()> {
160 if tenant_id.is_empty() {
161 anyhow::bail!("tenant_id must not be empty");
162 }
163 if tenant_id.len() > 64 {
164 anyhow::bail!("tenant_id must be at most 64 bytes");
165 }
166 if !tenant_id
167 .bytes()
168 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
169 {
170 anyhow::bail!(
171 "tenant_id {tenant_id:?} contains characters outside [A-Za-z0-9_-]"
172 );
173 }
174 Ok(())
175}
176
177#[derive(Debug, Serialize, Deserialize)]
178struct ErrorEnvelope {
179 error: String,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 detail: Option<serde_json::Value>,
182}
183
184pub(crate) fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
185 let bytes = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
186 Response::from_data(bytes)
187 .with_status_code(status)
188 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
189}
190
191pub(crate) fn error_response(status: u16, msg: impl Into<String>) -> Response<std::io::Cursor<Vec<u8>>> {
192 json_response(status, &serde_json::to_value(ErrorEnvelope {
193 error: msg.into(), detail: None,
194 }).unwrap())
195}
196
197pub(crate) fn error_with_detail(status: u16, msg: impl Into<String>, detail: serde_json::Value)
198 -> Response<std::io::Cursor<Vec<u8>>>
199{
200 json_response(status, &serde_json::to_value(ErrorEnvelope {
201 error: msg.into(), detail: Some(detail),
202 }).unwrap())
203}
204
205fn write_error_response(prefix: &str, err: lex_store::StoreError)
211 -> Response<std::io::Cursor<Vec<u8>>>
212{
213 if let lex_store::StoreError::Contention { branch, attempts } = &err {
214 let body = serde_json::to_vec(&ErrorEnvelope {
215 error: format!("{prefix}: branch '{branch}' is contended (attempts={attempts})"),
216 detail: Some(serde_json::json!({
217 "kind": "contention",
218 "branch": branch,
219 "attempts": attempts,
220 })),
221 }).unwrap_or_else(|_| b"{}".to_vec());
222 return Response::from_data(body)
223 .with_status_code(503)
224 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
225 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"1"[..]).unwrap());
226 }
227 if let lex_store::StoreError::BudgetExceeded { session_id, cap, spent_after } = &err {
235 let body = serde_json::to_vec(&ErrorEnvelope {
236 error: format!(
237 "{prefix}: session `{session_id}` budget exceeded \
238 (spent_after={spent_after}, cap={cap})"
239 ),
240 detail: Some(serde_json::json!({
241 "kind": "budget_exceeded",
242 "session_id": session_id,
243 "cap": cap,
244 "spent_after": spent_after,
245 })),
246 }).unwrap_or_else(|_| b"{}".to_vec());
247 return Response::from_data(body)
248 .with_status_code(503)
249 .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
250 .with_header(Header::from_bytes(&b"Retry-After"[..], &b"0"[..]).unwrap());
251 }
252 error_response(500, format!("{prefix}: {err}"))
253}
254
255pub fn handle(state: Arc<State>, mut req: Request) -> std::io::Result<()> {
256 let method = req.method().clone();
257 let url = req.url().to_string();
258 let path = url.split('?').next().unwrap_or("").to_string();
259 let query = url.split_once('?').map(|(_, q)| q.to_string()).unwrap_or_default();
260
261 let x_lex_user = req.headers().iter()
266 .find(|h| h.field.equiv("x-lex-user"))
267 .map(|h| h.value.as_str().to_string());
268
269 if matches!(method, Method::Post) && path == "/v1/pkg/publish" {
271 let mut body_bytes: Vec<u8> = Vec::new();
272 let _ = req.as_reader().read_to_end(&mut body_bytes);
273 let resp = pkg_publish_handler(&state, &body_bytes);
274 return req.respond(resp);
275 }
276
277 let mut body = String::new();
278 let _ = req.as_reader().read_to_string(&mut body);
279
280 let resp = route(&state, &method, &path, &query, &body, x_lex_user.as_deref());
281 req.respond(resp)
282}
283
284pub fn handle_with_auth<F>(state: Arc<State>, req: Request, auth: F) -> std::io::Result<()>
288where
289 F: FnOnce(&str, &[Header]) -> bool,
290{
291 let path = req.url().split('?').next().unwrap_or("").to_string();
292 if !auth(&path, req.headers()) {
293 return req.respond(
294 Response::from_data(br#"{"error":"unauthorized"}"#.to_vec())
295 .with_status_code(401)
296 .with_header(
297 Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
298 ),
299 );
300 }
301 handle(state, req)
302}
303
304fn route(
305 state: &State,
306 method: &Method,
307 path: &str,
308 query: &str,
309 body: &str,
310 x_lex_user: Option<&str>,
311) -> Response<std::io::Cursor<Vec<u8>>> {
312 match (method, path) {
313 (Method::Get, "/") => crate::web::activity_handler(state),
315 (Method::Get, "/web/branches") => crate::web::branches_handler(state),
316 (Method::Get, "/web/trust") => crate::web::trust_handler(state),
317 (Method::Get, "/web/attention") => crate::web::attention_handler(state),
318 (Method::Get, p) if p.starts_with("/web/branch/") => {
319 let name = &p["/web/branch/".len()..];
320 crate::web::branch_handler(state, name)
321 }
322 (Method::Get, p) if p.starts_with("/web/stage/") => {
323 let id = &p["/web/stage/".len()..];
324 crate::web::stage_html_handler(state, id)
325 }
326 (Method::Post, p) if p.starts_with("/web/stage/") && (
331 p.ends_with("/pin") || p.ends_with("/defer")
332 || p.ends_with("/block") || p.ends_with("/unblock")
333 ) => {
334 let prefix_len = "/web/stage/".len();
335 let last_slash = p.rfind('/').unwrap_or(p.len());
336 let id = &p[prefix_len..last_slash];
337 let verb = &p[last_slash + 1..];
338 let decision = match verb {
339 "pin" => crate::web::WebStageDecision::Pin,
340 "defer" => crate::web::WebStageDecision::Defer,
341 "block" => crate::web::WebStageDecision::Block,
342 "unblock" => crate::web::WebStageDecision::Unblock,
343 _ => unreachable!("matched in outer guard"),
344 };
345 crate::web::stage_decision_handler(state, id, body, decision, x_lex_user)
346 }
347 (Method::Get, "/v1/health") => json_response(200, &serde_json::json!({"ok": true})),
349 (Method::Post, "/v1/parse") => parse_handler(body),
350 (Method::Post, "/v1/check") => check_handler(body),
351 (Method::Post, "/v1/publish") => publish_handler(state, body),
352 (Method::Post, "/v1/patch") => patch_handler(state, body),
353 (Method::Get, p) if p.starts_with("/v1/stage/") => {
354 let suffix = &p["/v1/stage/".len()..];
355 if let Some(id) = suffix.strip_suffix("/attestations") {
358 stage_attestations_handler(state, id)
359 } else {
360 stage_handler(state, suffix)
361 }
362 }
363 (Method::Post, "/v1/run") => run_handler(state, body, false),
364 (Method::Post, "/v1/replay") => run_handler(state, body, true),
365 (Method::Get, p) if p.starts_with("/v1/trace/") => {
366 let id = &p["/v1/trace/".len()..];
367 trace_handler(state, id)
368 }
369 (Method::Get, "/v1/diff") => diff_handler(state, query),
370 (Method::Post, "/v1/merge/start") => merge_start_handler(state, body),
371 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/resolve") => {
372 let id = &p["/v1/merge/".len()..p.len() - "/resolve".len()];
373 merge_resolve_handler(state, id, body)
374 }
375 (Method::Post, p) if p.starts_with("/v1/merge/") && p.ends_with("/commit") => {
376 let id = &p["/v1/merge/".len()..p.len() - "/commit".len()];
377 merge_commit_handler(state, id)
378 }
379 (Method::Post, "/v1/ops/batch") => ops_batch_handler(state, body),
381 (Method::Post, "/v1/attestations/batch") => attestations_batch_handler(state, body),
382 (Method::Post, "/v1/stages/batch") => crate::sync_http::stages_batch_handler(state, body),
385 (Method::Post, "/v1/stages/fetch") => crate::sync_http::stages_fetch_handler(state, body),
386 (Method::Post, "/v1/intents/batch") => crate::sync_http::intents_batch_handler(state, body),
387 (Method::Post, "/v1/intents/fetch") => crate::sync_http::intents_fetch_handler(state, body),
388 (Method::Get, "/v1/branches") => crate::branches_http::branches_list_handler(state),
392 (Method::Post, "/v1/branches") => crate::branches_http::branch_create_handler(state, body),
393 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/checkout") => {
394 let name = &p["/v1/branches/".len()..p.len() - "/checkout".len()];
395 crate::branches_http::branch_checkout_handler(state, name)
396 }
397 (Method::Get, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
400 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
401 crate::branches_http::branch_head_handler(state, name)
402 }
403 (Method::Post, p) if p.starts_with("/v1/branches/") && p.ends_with("/head") => {
404 let name = &p["/v1/branches/".len()..p.len() - "/head".len()];
405 crate::branches_http::branch_advance_head_handler(state, name, body)
406 }
407 (Method::Get, "/v1/ops/since") => ops_since_handler(state, query),
411 (Method::Get, "/v1/attestations/since") => attestations_since_handler(state, query),
412 (Method::Get, "/v1/pkg") => pkg_list_handler(state),
416 (Method::Put, p) if p.starts_with("/v1/pkg/") && p.ends_with("/visibility") => {
420 let name = &p["/v1/pkg/".len()..p.len() - "/visibility".len()];
421 pkg_set_visibility_handler(state, name, body)
422 }
423 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/head") => {
424 let name = &p["/v1/pkg/".len()..p.len() - "/head".len()];
425 pkg_head_handler(state, name)
426 }
427 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/versions") => {
428 let name = &p["/v1/pkg/".len()..p.len() - "/versions".len()];
429 pkg_versions_handler(state, name)
430 }
431 (Method::Get, p) if p.starts_with("/v1/pkg/") && p.ends_with("/archive") => {
433 let inner = &p["/v1/pkg/".len()..p.len() - "/archive".len()];
434 if let Some((name, version)) = inner.split_once('/') {
436 pkg_archive_handler(state, name, version)
437 } else {
438 error_response(400, "expected /v1/pkg/{name}/{version}/archive")
439 }
440 }
441 (Method::Get, p) if p.starts_with("/v1/pkg/") && p["/v1/pkg/".len()..].contains('/') => {
443 let inner = &p["/v1/pkg/".len()..];
444 if let Some((name, version)) = inner.split_once('/') {
445 pkg_get_version_handler(state, name, version)
446 } else {
447 error_response(400, "expected /v1/pkg/{name}/{version}")
448 }
449 }
450 (Method::Get, p) if p.starts_with("/v1/pkg/") => {
451 let name = &p["/v1/pkg/".len()..];
452 pkg_get_handler(state, name)
453 }
454 (Method::Delete, p) if p.starts_with("/v1/pkg/") => {
455 let name = &p["/v1/pkg/".len()..];
456 pkg_delete_handler(state, name)
457 }
458 _ => error_response(404, format!("unknown route: {method:?} {path}")),
459 }
460}
461
462#[derive(Deserialize)]
463struct ParseReq { source: String }
464
465fn parse_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
466 let req: ParseReq = match serde_json::from_str(body) {
467 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
468 };
469 match load_program_from_str(&req.source) {
470 Ok(prog) => {
471 let stages = canonicalize_program(&prog);
472 json_response(200, &serde_json::to_value(&stages).unwrap())
473 }
474 Err(e) => error_response(400, format!("syntax error: {e}")),
475 }
476}
477
478pub(crate) fn check_handler(body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
479 let req: ParseReq = match serde_json::from_str(body) {
480 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
481 };
482 let prog = match load_program_from_str(&req.source) {
483 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
484 };
485 let stages = canonicalize_program(&prog);
486 match lex_types::check_program(&stages) {
487 Ok(_) => json_response(200, &serde_json::json!({"ok": true})),
488 Err(errs) => json_response(422, &serde_json::to_value(&errs).unwrap()),
489 }
490}
491
492#[derive(Deserialize)]
493struct PublishReq { source: String, #[serde(default)] activate: bool }
494
495pub(crate) fn publish_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
496 let req: PublishReq = 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 mut stages = canonicalize_program(&prog);
506 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
507 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
508 }
509 let example_errors = lex_runtime::evaluate_examples(&stages);
515 if !example_errors.is_empty() {
516 return error_with_detail(422, "example mismatch",
517 serde_json::to_value(&example_errors).unwrap_or_default());
518 }
519
520 let store = state.store.lock().unwrap();
521 let branch = store.current_branch();
522
523 let old_head = match store.branch_head(&branch) {
525 Ok(h) => h,
526 Err(e) => return error_response(500, format!("branch_head: {e}")),
527 };
528 let old_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = old_head.values()
529 .filter_map(|stg| store.get_ast(stg).ok())
530 .filter_map(|s| match s {
531 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd)),
532 _ => None,
533 })
534 .collect();
535 let new_fns: std::collections::BTreeMap<String, lex_ast::FnDecl> = stages.iter()
536 .filter_map(|s| match s {
537 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
538 _ => None,
539 })
540 .collect();
541 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
542
543 let mut new_imports: lex_vcs::ImportMap = lex_vcs::ImportMap::new();
545 {
546 let entry = new_imports.entry("<source>".into()).or_default();
547 for s in &stages {
548 if let lex_ast::Stage::Import(im) = s {
549 entry.insert(im.reference.clone());
550 }
551 }
552 }
553
554 match store.publish_program(&branch, &stages, &report, &new_imports, req.activate) {
555 Ok(outcome) => {
556 record_examples_for_publish(&store, &stages, &outcome);
560 json_response(200, &serde_json::json!({
561 "ops": outcome.ops,
562 "head_op": outcome.head_op,
563 }))
564 }
565 Err(lex_store::StoreError::TypeError(errs)) => {
573 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
574 }
575 Err(e) => write_error_response("publish_program", e),
576 }
577}
578
579#[derive(Deserialize)]
580struct PatchReq {
581 stage_id: String,
582 patch: lex_ast::Patch,
583 #[serde(default)] activate: bool,
584}
585
586fn patch_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
589 let req: PatchReq = match serde_json::from_str(body) {
590 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
591 };
592 let store = state.store.lock().unwrap();
593
594 let original = match store.get_ast(&req.stage_id) {
596 Ok(s) => s, Err(e) => return error_response(404, format!("stage: {e}")),
597 };
598
599 let patched = match lex_ast::apply_patch(&original, &req.patch) {
601 Ok(s) => s,
602 Err(e) => return error_with_detail(422, "patch failed",
603 serde_json::to_value(&e).unwrap_or_default()),
604 };
605
606 let branch = store.current_branch();
617
618 let sig = match lex_ast::sig_id(&patched) {
620 Some(s) => s,
621 None => return error_response(500, "patched stage has no sig_id"),
622 };
623
624 let new_id = match store.publish(&patched) {
627 Ok(id) => id, Err(e) => return error_response(500, format!("publish: {e}")),
628 };
629
630 let original_effects: std::collections::BTreeSet<String> = match &original {
632 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
633 _ => std::collections::BTreeSet::new(),
634 };
635 let patched_effects: std::collections::BTreeSet<String> = match &patched {
636 lex_ast::Stage::FnDecl(fd) => fd.effects.iter().map(|e| e.name.clone()).collect(),
637 _ => std::collections::BTreeSet::new(),
638 };
639 let head_now = match store.get_branch(&branch) {
640 Ok(b) => b.and_then(|b| b.head_op),
641 Err(e) => return error_response(500, format!("get_branch: {e}")),
642 };
643 let kind = if original_effects != patched_effects {
644 let from_budget = lex_vcs::operation_budget_from_effects(&original_effects);
651 let to_budget = lex_vcs::operation_budget_from_effects(&patched_effects);
652 lex_vcs::OperationKind::ChangeEffectSig {
653 sig_id: sig.clone(),
654 from_stage_id: req.stage_id.clone(),
655 to_stage_id: new_id.clone(),
656 from_effects: original_effects,
657 to_effects: patched_effects,
658 from_budget,
659 to_budget,
660 }
661 } else {
662 let budget = lex_vcs::operation_budget_from_effects(&original_effects);
663 lex_vcs::OperationKind::ModifyBody {
664 sig_id: sig.clone(),
665 from_stage_id: req.stage_id.clone(),
666 to_stage_id: new_id.clone(),
667 from_budget: budget,
668 to_budget: budget,
669 }
670 };
671 let transition = lex_vcs::StageTransition::Replace {
672 sig_id: sig.clone(),
673 from: req.stage_id.clone(),
674 to: new_id.clone(),
675 };
676 let op = lex_vcs::Operation::new(
677 kind,
678 head_now.into_iter().collect::<Vec<_>>(),
679 );
680 let op_id = match store.apply_operation_gated(&branch, op, transition) {
681 Ok(id) => id,
682 Err(lex_store::StoreError::TypeError(errs)) => return error_with_detail(
683 422, "type errors after patch", serde_json::to_value(&errs).unwrap_or_default()),
684 Err(e) => return write_error_response("apply_operation_gated", e),
685 };
686 if req.activate {
687 if let Err(e) = store.activate(&new_id) {
688 return error_response(500, format!("activate: {e}"));
689 }
690 }
691
692 let status = format!("{:?}",
693 store.get_status(&new_id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
694 json_response(200, &serde_json::json!({
695 "old_stage_id": req.stage_id,
696 "new_stage_id": new_id,
697 "sig_id": sig,
698 "status": status,
699 "op_id": op_id,
700 }))
701}
702
703pub(crate) fn stage_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
704 let store = state.store.lock().unwrap();
705 let meta = match store.get_metadata(id) {
706 Ok(m) => m, Err(e) => return error_response(404, format!("{e}")),
707 };
708 let ast = match store.get_ast(id) {
709 Ok(a) => a, Err(e) => return error_response(404, format!("{e}")),
710 };
711 let status = format!("{:?}", store.get_status(id).unwrap_or(lex_store::StageStatus::Draft)).to_lowercase();
712 json_response(200, &serde_json::json!({
713 "metadata": meta,
714 "ast": ast,
715 "status": status,
716 }))
717}
718
719pub(crate) fn stage_attestations_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
728 let store = state.store.lock().unwrap();
729 if let Err(e) = store.get_metadata(id) {
730 return error_response(404, format!("{e}"));
731 }
732 let log = match store.attestation_log() {
733 Ok(l) => l,
734 Err(e) => return error_response(500, format!("attestation log: {e}")),
735 };
736 let mut listing = match log.list_for_stage(&id.to_string()) {
737 Ok(v) => v,
738 Err(e) => return error_response(500, format!("list_for_stage: {e}")),
739 };
740 listing.sort_by_key(|a| std::cmp::Reverse(a.timestamp));
741 json_response(200, &serde_json::json!({"attestations": listing}))
742}
743
744#[derive(Deserialize, Default)]
745struct PolicyJson {
746 #[serde(default)] allow_effects: Vec<String>,
747 #[serde(default)] allow_fs_read: Vec<String>,
748 #[serde(default)] allow_fs_write: Vec<String>,
749 #[serde(default)] budget: Option<u64>,
750}
751
752impl PolicyJson {
753 fn into_policy(self) -> Policy {
754 Policy {
755 allow_effects: self.allow_effects.into_iter().collect::<BTreeSet<_>>(),
756 allow_fs_read: self.allow_fs_read.into_iter().map(PathBuf::from).collect(),
757 allow_fs_write: self.allow_fs_write.into_iter().map(PathBuf::from).collect(),
758 allow_net_host: Vec::new(),
759 allow_proc: Vec::new(),
760 allow_approval: Vec::new(),
761 budget: self.budget,
762 }
763 }
764}
765
766#[derive(Deserialize)]
767struct RunReq {
768 source: String,
769 #[serde(rename = "fn")] func: String,
770 #[serde(default)] args: Vec<serde_json::Value>,
771 #[serde(default)] policy: PolicyJson,
772 #[serde(default)] overrides: IndexMap<String, serde_json::Value>,
773}
774
775pub(crate) fn run_handler(state: &State, body: &str, with_overrides: bool) -> Response<std::io::Cursor<Vec<u8>>> {
776 let req: RunReq = match serde_json::from_str(body) {
777 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
778 };
779 let prog = match load_program_from_str(&req.source) {
780 Ok(p) => p, Err(e) => return error_response(400, format!("syntax error: {e}")),
781 };
782 let stages = canonicalize_program(&prog);
783 if let Err(errs) = lex_types::check_program(&stages) {
784 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
785 }
786 let bc = compile_program(&stages);
787 let mut policy = req.policy.into_policy();
788 if let Some(ceiling) = &state.policy_ceiling {
794 policy = clamp_policy(policy, ceiling);
795 }
796 if let Err(violations) = check_policy(&bc, &policy) {
797 return error_with_detail(403, "policy violation", serde_json::to_value(&violations).unwrap());
798 }
799
800 let mut recorder = lex_trace::Recorder::new();
801 if with_overrides && !req.overrides.is_empty() {
802 recorder = recorder.with_overrides(req.overrides);
803 }
804 let handle = recorder.handle();
805 let handler = DefaultHandler::new(policy);
806 let mut vm = Vm::with_handler(&bc, Box::new(handler));
807 vm.set_tracer(Box::new(recorder));
808
809 let vargs: Vec<Value> = req.args.iter().map(json_to_value).collect();
810 let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
811 let result = vm.call(&req.func, vargs);
812 let ended = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
813
814 let store = state.store.lock().unwrap();
815 let (root_out, root_err, status) = match &result {
816 Ok(v) => (Some(value_to_json(v)), None, 200u16),
817 Err(e) => (None, Some(format!("{e}")), 200u16),
818 };
819 let tree = handle.finalize(req.func.clone(), serde_json::Value::Null,
820 root_out.clone(), root_err.clone(), started, ended);
821 let run_id = match store.save_trace(&tree) {
822 Ok(id) => id,
823 Err(e) => return error_response(500, format!("save_trace: {e}")),
824 };
825
826 let mut body = serde_json::json!({
827 "run_id": run_id,
828 "output": root_out,
829 });
830 if let Some(err) = root_err {
831 body["error"] = serde_json::Value::String(err);
832 }
833 json_response(status, &body)
834}
835
836fn trace_handler(state: &State, id: &str) -> Response<std::io::Cursor<Vec<u8>>> {
837 let store = state.store.lock().unwrap();
838 match store.load_trace(id) {
839 Ok(t) => json_response(200, &serde_json::to_value(&t).unwrap()),
840 Err(e) => error_response(404, format!("{e}")),
841 }
842}
843
844fn diff_handler(state: &State, query: &str) -> Response<std::io::Cursor<Vec<u8>>> {
845 let mut a = None;
846 let mut b = None;
847 for kv in query.split('&') {
848 if let Some((k, v)) = kv.split_once('=') {
849 match k { "a" => a = Some(v.to_string()), "b" => b = Some(v.to_string()), _ => {} }
850 }
851 }
852 let (Some(a), Some(b)) = (a, b) else {
853 return error_response(400, "missing a or b query params");
854 };
855 let store = state.store.lock().unwrap();
856 let ta = match store.load_trace(&a) { Ok(t) => t, Err(e) => return error_response(404, format!("a: {e}")) };
857 let tb = match store.load_trace(&b) { Ok(t) => t, Err(e) => return error_response(404, format!("b: {e}")) };
858 match lex_trace::diff_runs(&ta, &tb) {
859 Some(d) => json_response(200, &serde_json::to_value(&d).unwrap()),
860 None => json_response(200, &serde_json::json!({"divergence": null})),
861 }
862}
863
864fn json_to_value(v: &serde_json::Value) -> Value { Value::from_json(v) }
865
866fn value_to_json(v: &Value) -> serde_json::Value { v.to_json() }
867
868#[derive(Deserialize)]
869struct MergeStartReq {
870 src_branch: String,
871 dst_branch: String,
872}
873
874fn merge_start_handler(state: &State, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
885 let req: MergeStartReq = match serde_json::from_str(body) {
886 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
887 };
888 let store = state.store.lock().unwrap();
889 let src_head = match store.get_branch(&req.src_branch) {
890 Ok(Some(b)) => b.head_op,
891 Ok(None) => return error_response(404, format!("unknown src branch `{}`", req.src_branch)),
892 Err(e) => return error_response(500, format!("src branch read: {e}")),
893 };
894 let dst_head = match store.get_branch(&req.dst_branch) {
895 Ok(Some(b)) => b.head_op,
896 Ok(None) => return error_response(404, format!("unknown dst branch `{}`", req.dst_branch)),
897 Err(e) => return error_response(500, format!("dst branch read: {e}")),
898 };
899 let log = match lex_vcs::OpLog::open(store.root()) {
900 Ok(l) => l,
901 Err(e) => return error_response(500, format!("op log: {e}")),
902 };
903 let merge_id = mint_merge_id();
907 let session = match MergeSession::start(
908 merge_id.clone(),
909 &log,
910 src_head.as_ref(),
911 dst_head.as_ref(),
912 ) {
913 Ok(s) => s,
914 Err(e) => return error_response(500, format!("merge start: {e}")),
915 };
916 let conflicts: Vec<&lex_vcs::ConflictRecord> = session.remaining_conflicts();
917 let auto_resolved_count = session.auto_resolved.len();
918 let body = serde_json::json!({
919 "merge_id": merge_id,
920 "src_head": session.src_head,
921 "dst_head": session.dst_head,
922 "lca": session.lca,
923 "conflicts": conflicts,
924 "auto_resolved_count": auto_resolved_count,
925 });
926 drop(conflicts);
927 drop(store);
928 let wrapped = ApiMergeSession {
929 inner: session,
930 src_branch: req.src_branch,
931 dst_branch: req.dst_branch,
932 };
933 state.sessions.lock().unwrap().insert(merge_id, wrapped);
934 json_response(200, &body)
935}
936
937#[derive(Deserialize)]
938struct MergeResolveReq {
939 resolutions: Vec<MergeResolveEntry>,
944}
945
946#[derive(Deserialize)]
947struct MergeResolveEntry {
948 conflict_id: String,
949 resolution: lex_vcs::Resolution,
950}
951
952fn merge_resolve_handler(
963 state: &State,
964 merge_id: &str,
965 body: &str,
966) -> Response<std::io::Cursor<Vec<u8>>> {
967 let req: MergeResolveReq = match serde_json::from_str(body) {
968 Ok(r) => r, Err(e) => return error_response(400, format!("bad request: {e}")),
969 };
970 let mut sessions = state.sessions.lock().unwrap();
971 let Some(wrapped) = sessions.get_mut(merge_id) else {
972 return error_response(404, format!("unknown merge_id `{merge_id}`"));
973 };
974 let pairs: Vec<(String, lex_vcs::Resolution)> = req.resolutions.into_iter()
975 .map(|e| (e.conflict_id, e.resolution))
976 .collect();
977 let store = state.store.lock().unwrap();
982 let checker = lex_store::MergeResolutionChecker::new(&store, wrapped.dst_branch.clone());
983 let verdicts = wrapped.inner.resolve_checked(pairs, &checker);
984 drop(store);
985 let remaining: Vec<&lex_vcs::ConflictRecord> = wrapped.inner.remaining_conflicts();
986 let body = serde_json::json!({
987 "verdicts": verdicts,
988 "remaining_conflicts": remaining,
989 });
990 json_response(200, &body)
991}
992
993fn merge_commit_handler(
1012 state: &State,
1013 merge_id: &str,
1014) -> Response<std::io::Cursor<Vec<u8>>> {
1015 use std::collections::BTreeMap;
1016 let wrapped = match state.sessions.lock().unwrap().remove(merge_id) {
1017 Some(w) => w,
1018 None => return error_response(404, format!("unknown merge_id `{merge_id}`")),
1019 };
1020 let dst_branch = wrapped.dst_branch.clone();
1021 let src_head = wrapped.inner.src_head.clone();
1022 let dst_head = wrapped.inner.dst_head.clone();
1023 let auto_resolved = wrapped.inner.auto_resolved.clone();
1024
1025 let mut entries: BTreeMap<lex_vcs::SigId, Option<lex_vcs::StageId>> = BTreeMap::new();
1028
1029 for outcome in &auto_resolved {
1031 if let lex_vcs::MergeOutcome::Src { sig_id, stage_id } = outcome {
1032 entries.insert(sig_id.clone(), stage_id.clone());
1033 }
1034 }
1035
1036 let resolved = match wrapped.inner.commit() {
1038 Ok(r) => r,
1039 Err(lex_vcs::CommitError::ConflictsRemaining(ids)) => {
1040 return error_with_detail(
1044 422,
1045 "conflicts remaining",
1046 serde_json::json!({"unresolved": ids}),
1047 );
1048 }
1049 };
1050
1051 for (conflict_id, resolution) in resolved {
1052 match resolution {
1053 lex_vcs::Resolution::TakeOurs => {
1054 }
1056 lex_vcs::Resolution::TakeTheirs => {
1057 match resolve_take_theirs(state, &src_head, &conflict_id) {
1067 Ok(stage_id) => {
1068 entries.insert(conflict_id.clone(), stage_id);
1069 }
1070 Err(e) => return error_response(500, format!("resolve take_theirs: {e}")),
1071 }
1072 }
1073 lex_vcs::Resolution::Custom { op } => {
1074 match op.kind.merge_target() {
1083 Some((sig, stage)) => {
1084 if sig != conflict_id {
1085 return error_with_detail(
1086 422,
1087 "custom op targets a different sig than the conflict",
1088 serde_json::json!({
1089 "conflict_id": conflict_id,
1090 "op_targets": sig,
1091 }),
1092 );
1093 }
1094 entries.insert(conflict_id, stage);
1095 }
1096 None => {
1097 return error_with_detail(
1098 422,
1099 "custom op kind doesn't yield a single sig→stage delta",
1100 serde_json::json!({
1101 "conflict_id": conflict_id,
1102 "kind": serde_json::to_value(&op.kind).unwrap_or(serde_json::Value::Null),
1103 }),
1104 );
1105 }
1106 }
1107 }
1108 lex_vcs::Resolution::Defer => {
1109 return error_response(500, "internal: Defer slipped past commit gate");
1111 }
1112 }
1113 }
1114
1115 let resolved_count = entries.len();
1116 let mut parents: Vec<lex_vcs::OpId> = Vec::new();
1117 if let Some(d) = dst_head { parents.push(d); }
1118 if let Some(s) = src_head { parents.push(s); }
1119 let op = lex_vcs::Operation::new(
1120 lex_vcs::OperationKind::Merge { resolved: resolved_count },
1121 parents,
1122 );
1123 let transition = lex_vcs::StageTransition::Merge { entries };
1124 let store = state.store.lock().unwrap();
1125 match store.apply_merge_op_gated(&dst_branch, op, transition) {
1128 Ok(new_head_op) => json_response(200, &serde_json::json!({
1129 "new_head_op": new_head_op,
1130 "dst_branch": dst_branch,
1131 })),
1132 Err(lex_store::StoreError::TypeError(errs)) => error_with_detail(
1133 422, "merged program has type errors", serde_json::to_value(&errs).unwrap_or_default()),
1134 Err(e) => write_error_response("apply merge op", e),
1135 }
1136}
1137
1138fn resolve_take_theirs(
1143 state: &State,
1144 src_head: &Option<lex_vcs::OpId>,
1145 sig: &lex_vcs::SigId,
1146) -> std::io::Result<Option<lex_vcs::StageId>> {
1147 let store = state.store.lock().unwrap();
1148 let log = lex_vcs::OpLog::open(store.root())?;
1149 let Some(head) = src_head.as_ref() else { return Ok(None); };
1150 let mut current: Option<lex_vcs::StageId> = None;
1153 for record in log.walk_forward(head, None)? {
1154 match &record.produces {
1155 lex_vcs::StageTransition::Create { sig_id, stage_id }
1156 if sig_id == sig => { current = Some(stage_id.clone()); }
1157 lex_vcs::StageTransition::Replace { sig_id, to, .. }
1158 if sig_id == sig => { current = Some(to.clone()); }
1159 lex_vcs::StageTransition::Remove { sig_id, .. }
1160 if sig_id == sig => { current = None; }
1161 lex_vcs::StageTransition::Rename { from, to, body_stage_id }
1162 if from == sig || to == sig => {
1163 if from == sig { current = None; }
1164 if to == sig { current = Some(body_stage_id.clone()); }
1165 }
1166 lex_vcs::StageTransition::Merge { entries } => {
1167 if let Some(opt) = entries.get(sig) {
1168 current = opt.clone();
1169 }
1170 }
1171 _ => {}
1172 }
1173 }
1174 Ok(current)
1175}
1176
1177fn mint_merge_id() -> MergeSessionId {
1178 use std::sync::atomic::{AtomicU64, Ordering};
1179 static COUNTER: AtomicU64 = AtomicU64::new(0);
1180 let nanos = SystemTime::now()
1181 .duration_since(UNIX_EPOCH)
1182 .map(|d| d.as_nanos())
1183 .unwrap_or(0);
1184 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1185 format!("merge_{nanos:x}_{n:x}")
1186}
1187
1188pub(crate) fn ops_batch_handler(state: &State, body: &str)
1219 -> Response<std::io::Cursor<Vec<u8>>>
1220{
1221 let records: Vec<lex_vcs::OperationRecord> = match serde_json::from_str(body) {
1222 Ok(r) => r,
1223 Err(e) => return error_response(400,
1224 format!("body must be a JSON array of OperationRecord: {e}")),
1225 };
1226 let store = state.store.lock().unwrap();
1227 let log = match lex_vcs::OpLog::open(store.root()) {
1228 Ok(l) => l,
1229 Err(e) => return error_response(500, format!("opening op log: {e}")),
1230 };
1231
1232 let mut batch_ids: std::collections::BTreeSet<lex_vcs::OpId> =
1240 std::collections::BTreeSet::new();
1241 for rec in &records {
1242 let expected = rec.op.op_id();
1243 if expected != rec.op_id {
1244 return error_with_detail(409, "OpIdMismatch", serde_json::json!({
1245 "supplied": rec.op_id,
1246 "expected": expected,
1247 }));
1248 }
1249 for parent in &rec.op.parents {
1250 let known = match log.get(parent) {
1251 Ok(Some(_)) => true,
1252 Ok(None) => false,
1253 Err(e) => return error_response(500, format!("op log read: {e}")),
1254 };
1255 if !known && !batch_ids.contains(parent) {
1256 return error_with_detail(422, "MissingParent", serde_json::json!({
1257 "op_id": rec.op_id,
1258 "missing_parent": parent,
1259 }));
1260 }
1261 }
1262 batch_ids.insert(rec.op_id.clone());
1263 }
1264
1265 let mut added = 0usize;
1268 let mut added_ids: Vec<&lex_vcs::OpId> = Vec::new();
1269 for rec in &records {
1270 let already_present = matches!(log.get(&rec.op_id), Ok(Some(_)));
1271 match log.put(rec) {
1272 Ok(()) => {
1273 if !already_present {
1274 added += 1;
1275 added_ids.push(&rec.op_id);
1276 }
1277 }
1278 Err(e) => return error_response(500, format!("op log write: {e}")),
1279 }
1280 }
1281
1282 json_response(200, &serde_json::json!({
1283 "received": records.len(),
1284 "added": added,
1285 "skipped": records.len() - added,
1286 "added_ids": added_ids,
1287 }))
1288}
1289
1290pub(crate) fn attestations_batch_handler(state: &State, body: &str)
1312 -> Response<std::io::Cursor<Vec<u8>>>
1313{
1314 let attestations: Vec<lex_vcs::Attestation> = match serde_json::from_str(body) {
1315 Ok(a) => a,
1316 Err(e) => return error_response(400,
1317 format!("body must be a JSON array of Attestation: {e}")),
1318 };
1319 let store = state.store.lock().unwrap();
1320 let log = match store.attestation_log() {
1321 Ok(l) => l,
1322 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1323 };
1324 let op_log = match lex_vcs::OpLog::open(store.root()) {
1325 Ok(l) => l,
1326 Err(e) => return error_response(500, format!("opening op log: {e}")),
1327 };
1328
1329 for att in &attestations {
1331 let expected = lex_vcs::Attestation::with_timestamp(
1334 att.stage_id.clone(),
1335 att.op_id.clone(),
1336 att.intent_id.clone(),
1337 att.kind.clone(),
1338 att.result.clone(),
1339 att.produced_by.clone(),
1340 att.cost.clone(),
1341 att.timestamp,
1342 ).attestation_id;
1343 if expected != att.attestation_id {
1344 return error_with_detail(409, "AttestationIdMismatch", serde_json::json!({
1345 "supplied": att.attestation_id,
1346 "expected": expected,
1347 }));
1348 }
1349 if let Some(op_id) = &att.op_id {
1353 match op_log.get(op_id) {
1354 Ok(Some(_)) => {}
1355 Ok(None) => return error_with_detail(422, "UnknownOp", serde_json::json!({
1356 "attestation_id": att.attestation_id,
1357 "op_id": op_id,
1358 })),
1359 Err(e) => return error_response(500, format!("op log read: {e}")),
1360 }
1361 }
1362 }
1363
1364 let mut added = 0usize;
1368 let mut added_ids: Vec<&lex_vcs::AttestationId> = Vec::new();
1369 for att in &attestations {
1370 let already_present = matches!(log.get(&att.attestation_id), Ok(Some(_)));
1371 match log.put(att) {
1372 Ok(()) => {
1373 if !already_present {
1374 added += 1;
1375 added_ids.push(&att.attestation_id);
1376 }
1377 }
1378 Err(e) => return error_response(500, format!("attestation log write: {e}")),
1379 }
1380 }
1381
1382 json_response(200, &serde_json::json!({
1383 "received": attestations.len(),
1384 "added": added,
1385 "skipped": attestations.len() - added,
1386 "added_ids": added_ids,
1387 }))
1388}
1389
1390pub(crate) fn ops_since_handler(state: &State, query: &str)
1422 -> Response<std::io::Cursor<Vec<u8>>>
1423{
1424 let mut after: Option<String> = None;
1425 let mut branch = String::from("main");
1426 let mut limit: Option<usize> = None;
1427 for kv in query.split('&') {
1428 let Some((k, v)) = kv.split_once('=') else { continue };
1429 match k {
1430 "after" => after = Some(v.to_string()),
1431 "branch" => branch = v.to_string(),
1432 "limit" => {
1433 limit = Some(match v.parse::<usize>() {
1434 Ok(n) => n,
1435 Err(_) => return error_response(400,
1436 format!("limit must be a positive integer, got `{v}`")),
1437 });
1438 }
1439 _ => {}
1440 }
1441 }
1442
1443 let store = state.store.lock().unwrap();
1444 let log = match lex_vcs::OpLog::open(store.root()) {
1445 Ok(l) => l,
1446 Err(e) => return error_response(500, format!("opening op log: {e}")),
1447 };
1448 let head = match store.get_branch(&branch) {
1449 Ok(Some(b)) => b.head_op,
1450 Ok(None) => None,
1451 Err(e) => return error_response(500, format!("get_branch: {e}")),
1452 };
1453 let Some(head) = head else {
1454 return json_response(200, &serde_json::json!([]));
1455 };
1456
1457 let ops_since = match log.ops_since(&head, after.as_ref()) {
1458 Ok(o) => o,
1459 Err(e) => return error_response(500, format!("ops_since: {e}")),
1460 };
1461 let mut ops = ops_since;
1465 ops.reverse();
1466 if let Some(n) = limit {
1467 ops.truncate(n);
1468 }
1469
1470 json_response(200, &serde_json::to_value(&ops).unwrap_or_default())
1471}
1472
1473pub(crate) fn attestations_since_handler(state: &State, query: &str)
1486 -> Response<std::io::Cursor<Vec<u8>>>
1487{
1488 let mut after_op: Option<String> = None;
1489 let mut limit: Option<usize> = None;
1490 for kv in query.split('&') {
1491 let Some((k, v)) = kv.split_once('=') else { continue };
1492 match k {
1493 "after-op" => after_op = Some(v.to_string()),
1494 "limit" => {
1495 limit = Some(match v.parse::<usize>() {
1496 Ok(n) => n,
1497 Err(_) => return error_response(400,
1498 format!("limit must be a positive integer, got `{v}`")),
1499 });
1500 }
1501 _ => {}
1502 }
1503 }
1504
1505 let store = state.store.lock().unwrap();
1506 let log = match store.attestation_log() {
1507 Ok(l) => l,
1508 Err(e) => return error_response(500, format!("opening attestation log: {e}")),
1509 };
1510
1511 let exclude: std::collections::BTreeSet<String> = match &after_op {
1515 None => std::collections::BTreeSet::new(),
1516 Some(cutoff) => {
1517 let op_log = match lex_vcs::OpLog::open(store.root()) {
1518 Ok(l) => l,
1519 Err(e) => return error_response(500, format!("opening op log: {e}")),
1520 };
1521 match op_log.walk_back(cutoff, None) {
1522 Ok(records) => records.into_iter().map(|r| r.op_id).collect(),
1523 Err(_) => {
1524 std::collections::BTreeSet::new()
1528 }
1529 }
1530 }
1531 };
1532
1533 let all = match log.list_all() {
1534 Ok(v) => v,
1535 Err(e) => return error_response(500, format!("listing attestations: {e}")),
1536 };
1537 let mut filtered: Vec<lex_vcs::Attestation> = all
1538 .into_iter()
1539 .filter(|a| match &a.op_id {
1540 Some(op_id) => !exclude.contains(op_id),
1541 None => true,
1545 })
1546 .collect();
1547 filtered.sort_by(|a, b| {
1551 a.timestamp.cmp(&b.timestamp)
1552 .then_with(|| a.attestation_id.cmp(&b.attestation_id))
1553 });
1554 if let Some(n) = limit {
1555 filtered.truncate(n);
1556 }
1557
1558 json_response(200, &serde_json::to_value(&filtered).unwrap_or_default())
1559}
1560
1561#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1565struct PkgRecord {
1566 name: String,
1567 version: String,
1568 head_op: Option<String>,
1569 published_at: u64,
1570 function_names: Vec<String>,
1572 ops: Vec<serde_json::Value>,
1574}
1575
1576#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
1583#[serde(rename_all = "lowercase")]
1584pub enum Visibility {
1585 #[default]
1586 Private,
1587 Public,
1588}
1589
1590#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1595struct PkgIndex {
1596 latest: Option<String>,
1598 versions: Vec<PkgVersionSummary>,
1600 #[serde(default)]
1604 visibility: Visibility,
1605}
1606
1607#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1608struct PkgVersionSummary {
1609 version: String,
1610 head_op: Option<String>,
1611 published_at: u64,
1612}
1613
1614fn pkg_name_dir(root: &std::path::Path, name: &str) -> PathBuf {
1615 root.join("packages").join(name)
1616}
1617
1618fn pkg_index_path(root: &std::path::Path, name: &str) -> PathBuf {
1619 pkg_name_dir(root, name).join("index.json")
1620}
1621
1622fn pkg_version_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1623 pkg_name_dir(root, name).join(format!("{version}.json"))
1624}
1625
1626fn pkg_archive_path(root: &std::path::Path, name: &str, version: &str) -> PathBuf {
1627 pkg_name_dir(root, name).join(format!("{version}.tar.gz"))
1628}
1629
1630fn load_pkg_index(root: &std::path::Path, name: &str) -> Option<PkgIndex> {
1631 let bytes = std::fs::read(pkg_index_path(root, name)).ok()?;
1632 serde_json::from_slice(&bytes).ok()
1633}
1634
1635fn load_pkg_record(root: &std::path::Path, name: &str, version: &str) -> Option<PkgRecord> {
1636 let bytes = std::fs::read(pkg_version_path(root, name, version)).ok()?;
1637 serde_json::from_slice(&bytes).ok()
1638}
1639
1640fn load_latest_pkg_record(root: &std::path::Path, name: &str) -> Option<PkgRecord> {
1641 let index = load_pkg_index(root, name)?;
1642 let latest = index.latest.clone()?;
1643 load_pkg_record(root, name, &latest)
1644}
1645
1646fn pkg_is_public(root: &std::path::Path, name: &str) -> bool {
1650 load_pkg_index(root, name).map(|i| i.visibility) == Some(Visibility::Public)
1651}
1652
1653fn valid_pkg_segment(s: &str) -> bool {
1658 !s.is_empty()
1659 && s.len() <= 128
1660 && s != "."
1661 && s != ".."
1662 && s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
1663}
1664
1665#[derive(Deserialize)]
1666struct VisibilityReq {
1667 visibility: Visibility,
1668}
1669
1670fn pkg_set_visibility_handler(
1677 state: &State,
1678 name: &str,
1679 body: &str,
1680) -> Response<std::io::Cursor<Vec<u8>>> {
1681 if !valid_pkg_segment(name) {
1682 return error_response(400, format!("invalid package name {name:?}"));
1683 }
1684 let req: VisibilityReq = match serde_json::from_str(body) {
1685 Ok(r) => r,
1686 Err(e) => return error_response(400, format!("bad request: {e}")),
1687 };
1688 let mut index = match load_pkg_index(&state.root, name) {
1689 Some(i) => i,
1690 None => return error_response(404, format!("package {name:?} not found")),
1691 };
1692 index.visibility = req.visibility;
1693 let bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1694 match std::fs::write(pkg_index_path(&state.root, name), bytes) {
1695 Ok(()) => json_response(
1696 200,
1697 &serde_json::json!({ "name": name, "visibility": index.visibility }),
1698 ),
1699 Err(e) => error_response(500, format!("write index: {e}")),
1700 }
1701}
1702
1703fn public_pkg_names(root: &std::path::Path) -> Vec<String> {
1706 list_pkg_names(root)
1707 .into_iter()
1708 .filter(|name| pkg_is_public(root, name))
1709 .collect()
1710}
1711
1712fn public_pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
1716 let packages: Vec<serde_json::Value> = public_pkg_names(&state.root)
1717 .iter()
1718 .filter_map(|name| {
1719 let r = load_latest_pkg_record(&state.root, name)?;
1720 Some(serde_json::json!({
1721 "name": r.name,
1722 "version": r.version,
1723 "head_op": r.head_op,
1724 "published_at": r.published_at,
1725 }))
1726 })
1727 .collect();
1728 json_response(200, &serde_json::json!({ "packages": packages }))
1729}
1730
1731#[derive(Debug, PartialEq, Eq)]
1736enum PublicTarget {
1737 List,
1738 Latest(String),
1739 Versions(String),
1740 Head(String),
1741 Version(String, String),
1742 Archive(String, String),
1743}
1744
1745impl PublicTarget {
1746 fn pkg_name(&self) -> Option<&str> {
1748 match self {
1749 PublicTarget::List => None,
1750 PublicTarget::Latest(n)
1751 | PublicTarget::Versions(n)
1752 | PublicTarget::Head(n)
1753 | PublicTarget::Version(n, _)
1754 | PublicTarget::Archive(n, _) => Some(n),
1755 }
1756 }
1757}
1758
1759fn resolve_public(method: &Method, path: &str) -> Result<PublicTarget, u16> {
1764 if !matches!(method, Method::Get) {
1765 return Err(405);
1766 }
1767 let rest = path.trim_matches('/');
1768 if rest.is_empty() {
1769 return Ok(PublicTarget::List);
1770 }
1771 let segs: Vec<&str> = rest.split('/').collect();
1772 if !segs.iter().all(|s| valid_pkg_segment(s)) {
1773 return Err(404);
1774 }
1775 match segs.as_slice() {
1776 [n] => Ok(PublicTarget::Latest(n.to_string())),
1777 [n, "versions"] => Ok(PublicTarget::Versions(n.to_string())),
1778 [n, "head"] => Ok(PublicTarget::Head(n.to_string())),
1779 [n, v, "archive"] => Ok(PublicTarget::Archive(n.to_string(), v.to_string())),
1780 [n, v] => Ok(PublicTarget::Version(n.to_string(), v.to_string())),
1781 _ => Err(404),
1782 }
1783}
1784
1785pub fn route_public(
1796 state: &State,
1797 method: &Method,
1798 path: &str,
1799 _query: &str,
1800) -> Response<std::io::Cursor<Vec<u8>>> {
1801 let target = match resolve_public(method, path) {
1802 Ok(t) => t,
1803 Err(405) => return error_response(405, "public read is GET-only"),
1804 Err(_) => return error_response(404, "not found"),
1805 };
1806 if let PublicTarget::List = target {
1808 return public_pkg_list_handler(state);
1809 }
1810 if let Some(name) = target.pkg_name() {
1812 if !pkg_is_public(&state.root, name) {
1813 return error_response(404, format!("package {name:?} not found"));
1814 }
1815 }
1816 match target {
1817 PublicTarget::List => unreachable!("handled above"),
1818 PublicTarget::Latest(n) => pkg_get_handler(state, &n),
1819 PublicTarget::Versions(n) => pkg_versions_handler(state, &n),
1820 PublicTarget::Head(n) => pkg_head_handler(state, &n),
1821 PublicTarget::Version(n, v) => pkg_get_version_handler(state, &n, &v),
1822 PublicTarget::Archive(n, v) => pkg_archive_handler(state, &n, &v),
1823 }
1824}
1825
1826fn save_pkg_record(
1827 root: &std::path::Path,
1828 record: &PkgRecord,
1829 archive: &[u8],
1830) -> std::io::Result<()> {
1831 let dir = pkg_name_dir(root, &record.name);
1832 std::fs::create_dir_all(&dir)?;
1833
1834 let rec_bytes = serde_json::to_vec_pretty(record).unwrap_or_default();
1836 std::fs::write(pkg_version_path(root, &record.name, &record.version), rec_bytes)?;
1837
1838 std::fs::write(pkg_archive_path(root, &record.name, &record.version), archive)?;
1840
1841 let mut index = load_pkg_index(root, &record.name).unwrap_or_default();
1843 index.latest = Some(record.version.clone());
1844 if !index.versions.iter().any(|v| v.version == record.version) {
1845 index.versions.push(PkgVersionSummary {
1846 version: record.version.clone(),
1847 head_op: record.head_op.clone(),
1848 published_at: record.published_at,
1849 });
1850 }
1851 let idx_bytes = serde_json::to_vec_pretty(&index).unwrap_or_default();
1852 std::fs::write(pkg_index_path(root, &record.name), idx_bytes)
1853}
1854
1855fn list_pkg_names(root: &std::path::Path) -> Vec<String> {
1856 let dir = root.join("packages");
1857 let Ok(entries) = std::fs::read_dir(&dir) else {
1858 return Vec::new();
1859 };
1860 let mut names: Vec<String> = entries
1861 .filter_map(|e| e.ok())
1862 .filter(|e| e.path().is_dir())
1863 .filter_map(|e| e.file_name().into_string().ok())
1864 .collect();
1865 names.sort();
1866 names
1867}
1868
1869fn collect_lex_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
1870 let Ok(entries) = std::fs::read_dir(dir) else { return };
1871 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
1872 entries.sort_by_key(|e| e.path());
1873 for entry in entries {
1874 let path = entry.path();
1875 if path.is_dir() {
1876 collect_lex_files(&path, out);
1877 } else if path.extension().and_then(|x| x.to_str()) == Some("lex") {
1878 out.push(path);
1879 }
1880 }
1881}
1882
1883fn pkg_publish_handler(state: &State, body: &[u8]) -> Response<std::io::Cursor<Vec<u8>>> {
1886 let tmp = match tempfile::TempDir::new() {
1887 Ok(t) => t,
1888 Err(e) => return error_response(500, format!("create temp dir: {e}")),
1889 };
1890 {
1891 let gz = flate2::read::GzDecoder::new(std::io::Cursor::new(body));
1892 let mut ar = tar::Archive::new(gz);
1893 if let Err(e) = ar.unpack(tmp.path()) {
1894 return error_response(400, format!("unpack archive: {e}"));
1895 }
1896 }
1897
1898 let toml_path = tmp.path().join("lex.toml");
1899 if !toml_path.exists() {
1900 return error_response(400, "archive must contain lex.toml at root");
1901 }
1902 let manifest = match Manifest::load(&toml_path) {
1903 Ok(m) => m,
1904 Err(e) => return error_response(400, format!("lex.toml: {e}")),
1905 };
1906 let (pkg_name, pkg_version) = match &manifest.package {
1907 Some(m) => (m.name.clone(), m.version.clone()),
1908 None => return error_response(400, "lex.toml must have a [package] section"),
1909 };
1910
1911 if load_pkg_record(&state.root, &pkg_name, &pkg_version).is_some() {
1915 return error_response(
1916 409,
1917 format!(
1918 "package {pkg_name}@{pkg_version} already published; \
1919 bump the version in lex.toml to publish a new release"
1920 ),
1921 );
1922 }
1923
1924 let src_dir = tmp.path().join("src");
1925 if !src_dir.exists() {
1926 return error_response(400, "archive must contain a src/ directory");
1927 }
1928 let mut lex_files: Vec<PathBuf> = Vec::new();
1929 collect_lex_files(&src_dir, &mut lex_files);
1930 if lex_files.is_empty() {
1931 return error_response(400, "no .lex files found in src/");
1932 }
1933
1934 let store = state.store.lock().unwrap();
1935 let branch = store.current_branch();
1936
1937 let old_head = match store.branch_head(&branch) {
1957 Ok(h) => h,
1958 Err(e) => return error_response(500, format!("branch_head: {e}")),
1959 };
1960 let old_pairs: Vec<(String, String)> =
1967 old_head.iter().map(|(sig, stage)| (sig.clone(), stage.clone())).collect();
1968 let mut old_fns_by_name: BTreeMap<String, Vec<lex_ast::FnDecl>> = BTreeMap::new();
1969 for fd in store.get_asts_for_sigs_bulk(&old_pairs)
1970 .into_iter()
1971 .filter_map(|r| r.ok())
1972 .filter_map(|s| match s { lex_ast::Stage::FnDecl(fd) => Some(fd), _ => None })
1973 {
1974 old_fns_by_name.entry(fd.name.clone()).or_default().push(fd);
1975 }
1976 fn structural_key(fd: &lex_ast::FnDecl) -> Option<String> {
1984 let mut anon = fd.clone();
1985 anon.name = String::new();
1986 lex_ast::sig_id(&lex_ast::Stage::FnDecl(anon))
1987 }
1988
1989 fn take_matching(
1999 map: &mut BTreeMap<String, Vec<lex_ast::FnDecl>>,
2000 name: &str,
2001 new_fd: &lex_ast::FnDecl,
2002 ) -> Option<lex_ast::FnDecl> {
2003 let candidates = map.get_mut(name)?;
2004 let idx = match candidates.len() {
2005 0 => return None,
2006 1 => 0,
2007 _ => {
2008 let want = structural_key(new_fd);
2009 candidates.iter().position(|c| structural_key(c) == want)?
2010 }
2011 };
2012 let matched = candidates.remove(idx);
2013 if candidates.is_empty() {
2014 map.remove(name);
2015 }
2016 Some(matched)
2017 }
2018
2019 let loaded = match load_package(&lex_files, tmp.path(), &pkg_name) {
2039 Ok(p) => p,
2040 Err(e) => return error_response(400, format!("load package: {e}")),
2041 };
2042 let mut stages = canonicalize_program(&loaded.program);
2043 if let Err(errs) = lex_types::check_and_rewrite_program(&mut stages) {
2049 return error_with_detail(
2050 422,
2051 format!("type errors in package {pkg_name}"),
2052 serde_json::to_value(&errs).unwrap(),
2053 );
2054 }
2055 let new_fns: BTreeMap<String, lex_ast::FnDecl> = stages
2056 .iter()
2057 .filter_map(|s| match s {
2058 lex_ast::Stage::FnDecl(fd) => Some((fd.name.clone(), fd.clone())),
2059 _ => None,
2060 })
2061 .collect();
2062 let all_function_names: Vec<String> = new_fns.keys().cloned().collect();
2063
2064 let mut old_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2069 for (name, new_fd) in &new_fns {
2070 if let Some(fd) = take_matching(&mut old_fns_by_name, name, new_fd) {
2071 old_fns.insert(name.clone(), fd);
2072 }
2073 }
2074 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2075
2076 let mut new_imports = lex_vcs::ImportMap::new();
2081 for (file, modules) in &loaded.imports_by_file {
2082 let entry = new_imports.entry(file.clone()).or_default();
2083 for m in modules {
2084 entry.insert(m.clone());
2085 }
2086 }
2087
2088 let outcome = match store.publish_program(&branch, &stages, &report, &new_imports, false) {
2089 Ok(outcome) => outcome,
2090 Err(lex_store::StoreError::TypeError(errs)) => {
2091 return error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap());
2092 }
2093 Err(e) => return write_error_response("publish_program", e),
2094 };
2095 let all_ops: Vec<serde_json::Value> = match serde_json::to_value(&outcome.ops) {
2096 Ok(serde_json::Value::Array(arr)) => arr,
2097 _ => Vec::new(),
2098 };
2099 let final_head_op = outcome.head_op;
2100
2101 let now = SystemTime::now()
2126 .duration_since(UNIX_EPOCH)
2127 .map(|d| d.as_secs())
2128 .unwrap_or(0);
2129 let record = PkgRecord {
2130 name: pkg_name.clone(),
2131 version: pkg_version,
2132 head_op: final_head_op.clone(),
2133 published_at: now,
2134 function_names: all_function_names,
2135 ops: all_ops.clone(),
2136 };
2137 if let Err(e) = save_pkg_record(&state.root, &record, body) {
2138 return error_response(500, format!("save package index: {e}"));
2139 }
2140
2141 json_response(200, &serde_json::json!({
2142 "package": pkg_name,
2143 "ops": all_ops,
2144 "head_op": final_head_op,
2145 }))
2146}
2147
2148fn pkg_list_handler(state: &State) -> Response<std::io::Cursor<Vec<u8>>> {
2150 let names = list_pkg_names(&state.root);
2151 let packages: Vec<serde_json::Value> = names.iter()
2152 .filter_map(|name| {
2153 let idx = load_pkg_index(&state.root, name)?;
2154 let latest = idx.latest.as_deref()?;
2155 let r = load_pkg_record(&state.root, name, latest)?;
2156 Some(serde_json::json!({
2157 "name": r.name,
2158 "version": r.version,
2159 "head_op": r.head_op,
2160 "published_at": r.published_at,
2161 }))
2162 })
2163 .collect();
2164 json_response(200, &serde_json::json!({ "packages": packages }))
2165}
2166
2167fn pkg_get_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2169 match load_latest_pkg_record(&state.root, name) {
2170 Some(r) => json_response(200, &serde_json::json!({
2171 "name": r.name,
2172 "version": r.version,
2173 "head_op": r.head_op,
2174 "published_at": r.published_at,
2175 "function_names": r.function_names,
2176 "ops": r.ops,
2177 })),
2178 None => error_response(404, format!("package {name:?} not found")),
2179 }
2180}
2181
2182fn pkg_versions_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2184 match load_pkg_index(&state.root, name) {
2185 Some(idx) => json_response(200, &serde_json::json!({
2186 "name": name,
2187 "latest": idx.latest,
2188 "versions": idx.versions,
2189 })),
2190 None => error_response(404, format!("package {name:?} not found")),
2191 }
2192}
2193
2194fn pkg_get_version_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2196 match load_pkg_record(&state.root, name, version) {
2197 Some(r) => json_response(200, &serde_json::json!({
2198 "name": r.name,
2199 "version": r.version,
2200 "head_op": r.head_op,
2201 "published_at": r.published_at,
2202 "function_names": r.function_names,
2203 "ops": r.ops,
2204 })),
2205 None => error_response(404, format!("package {name:?}@{version:?} not found")),
2206 }
2207}
2208
2209fn pkg_archive_handler(state: &State, name: &str, version: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2211 let path = pkg_archive_path(&state.root, name, version);
2212 match std::fs::read(&path) {
2213 Ok(bytes) => Response::from_data(bytes)
2214 .with_status_code(200)
2215 .with_header(
2216 tiny_http::Header::from_bytes(
2217 &b"Content-Type"[..],
2218 &b"application/gzip"[..],
2219 )
2220 .unwrap(),
2221 ),
2222 Err(_) => error_response(404, format!("archive for {name:?}@{version:?} not found")),
2223 }
2224}
2225
2226fn pkg_head_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2228 match load_latest_pkg_record(&state.root, name) {
2229 Some(r) => json_response(200, &serde_json::json!({
2230 "name": r.name,
2231 "version": r.version,
2232 "head_op": r.head_op,
2233 })),
2234 None => error_response(404, format!("package {name:?} not found")),
2235 }
2236}
2237
2238fn pkg_delete_handler(state: &State, name: &str) -> Response<std::io::Cursor<Vec<u8>>> {
2240 let record = match load_latest_pkg_record(&state.root, name) {
2241 Some(r) => r,
2242 None => return error_response(404, format!("package {name:?} not found")),
2243 };
2244
2245 let store = state.store.lock().unwrap();
2246 let branch = store.current_branch();
2247
2248 let head = match store.branch_head(&branch) {
2249 Ok(h) => h,
2250 Err(e) => return error_response(500, format!("branch_head: {e}")),
2251 };
2252
2253 let head_pairs: Vec<(String, String)> = head
2260 .iter()
2261 .map(|(sig, stage)| (sig.clone(), stage.clone()))
2262 .collect();
2263 let old_fns: BTreeMap<String, lex_ast::FnDecl> = store
2264 .get_asts_for_sigs_bulk(&head_pairs)
2265 .into_iter()
2266 .filter_map(|r| r.ok())
2267 .filter_map(|s| match s {
2268 lex_ast::Stage::FnDecl(fd)
2269 if record.function_names.contains(&fd.name) => Some((fd.name.clone(), fd)),
2270 _ => None,
2271 })
2272 .collect();
2273
2274 let new_fns: BTreeMap<String, lex_ast::FnDecl> = BTreeMap::new();
2275 let report = lex_vcs::compute_diff(&old_fns, &new_fns, false);
2276 let empty_imports = lex_vcs::ImportMap::new();
2277
2278 match store.publish_program(&branch, &[], &report, &empty_imports, false) {
2279 Ok(outcome) => {
2280 let ver = record.version.clone();
2282 let _ = std::fs::remove_file(pkg_version_path(&state.root, name, &ver));
2283 let _ = std::fs::remove_file(pkg_archive_path(&state.root, name, &ver));
2284 if let Some(mut idx) = load_pkg_index(&state.root, name) {
2286 idx.versions.retain(|v| v.version != ver);
2287 idx.latest = idx.versions.last().map(|v| v.version.clone());
2288 if idx.versions.is_empty() {
2289 let _ = std::fs::remove_dir_all(pkg_name_dir(&state.root, name));
2290 } else {
2291 let bytes = serde_json::to_vec_pretty(&idx).unwrap_or_default();
2292 let _ = std::fs::write(pkg_index_path(&state.root, name), bytes);
2293 }
2294 }
2295 json_response(200, &serde_json::json!({
2296 "deleted": name,
2297 "version": ver,
2298 "ops": outcome.ops,
2299 "head_op": outcome.head_op,
2300 }))
2301 }
2302 Err(lex_store::StoreError::TypeError(errs)) => {
2303 error_with_detail(422, "type errors", serde_json::to_value(&errs).unwrap())
2304 }
2305 Err(e) => write_error_response("retract package", e),
2306 }
2307}
2308
2309#[cfg(test)]
2310mod policy_ceiling_tests {
2311 use super::*;
2312 use lex_runtime::Policy;
2313 use std::path::PathBuf;
2314
2315 fn permissive_request() -> Policy {
2319 Policy {
2320 allow_effects: ["io", "fs_read", "fs_write", "net", "proc"]
2321 .iter()
2322 .map(|s| s.to_string())
2323 .collect(),
2324 allow_fs_read: vec![PathBuf::from("/")],
2325 allow_fs_write: vec![PathBuf::from("/")],
2326 allow_net_host: Vec::new(),
2327 allow_proc: Vec::new(),
2328 allow_approval: Vec::new(),
2329 budget: None,
2330 }
2331 }
2332
2333 #[test]
2334 fn ceiling_drops_effects_the_caller_was_not_granted() {
2335 let ceiling = Policy {
2336 allow_effects: ["io", "time"].iter().map(|s| s.to_string()).collect(),
2337 ..Policy::default()
2338 };
2339 let got = clamp_policy(permissive_request(), &ceiling);
2340 assert!(got.allow_effects.contains("io"));
2341 assert!(!got.allow_effects.contains("proc"), "proc must not survive a ceiling without it");
2342 assert!(!got.allow_effects.contains("fs_write"));
2343 assert!(!got.allow_effects.contains("net"));
2344 assert!(!got.allow_effects.contains("time"));
2346 }
2347
2348 #[test]
2349 fn ceiling_scopes_override_caller_scopes() {
2350 let ceiling = Policy {
2351 allow_effects: ["fs_read"].iter().map(|s| s.to_string()).collect(),
2352 allow_fs_read: vec![PathBuf::from("/srv/tenant")],
2353 ..Policy::default()
2354 };
2355 let got = clamp_policy(permissive_request(), &ceiling);
2356 assert_eq!(got.allow_fs_read, vec![PathBuf::from("/srv/tenant")]);
2359 assert!(got.allow_fs_write.is_empty());
2360 assert!(got.allow_proc.is_empty());
2361 assert!(got.allow_net_host.is_empty());
2362 }
2363
2364 #[test]
2365 fn ceiling_caps_budget_and_prefers_the_smaller() {
2366 let mut req = permissive_request();
2368 req.budget = None;
2369 let ceiling = Policy { budget: Some(1_000), ..Policy::default() };
2370 assert_eq!(clamp_policy(req, &ceiling).budget, Some(1_000));
2371
2372 let mut req2 = permissive_request();
2374 req2.budget = Some(50);
2375 let ceiling2 = Policy { budget: Some(1_000), ..Policy::default() };
2376 assert_eq!(clamp_policy(req2, &ceiling2).budget, Some(50));
2377 }
2378
2379 #[test]
2380 fn empty_ceiling_is_pure_only() {
2381 let got = clamp_policy(permissive_request(), &Policy::default());
2382 assert!(got.allow_effects.is_empty(), "an empty ceiling grants nothing");
2383 assert!(got.allow_proc.is_empty());
2384 assert!(got.allow_fs_write.is_empty());
2385 }
2386}
2387
2388#[cfg(test)]
2389mod public_read_tests {
2390 use super::*;
2391
2392 fn seed_pkg(root: &std::path::Path, name: &str, version: &str) {
2395 let record = PkgRecord {
2396 name: name.to_string(),
2397 version: version.to_string(),
2398 head_op: Some(format!("op-{name}")),
2399 published_at: 1,
2400 function_names: vec![format!("{name}.f")],
2401 ops: vec![],
2402 };
2403 save_pkg_record(root, &record, format!("ARCHIVE:{name}@{version}").as_bytes())
2404 .expect("seed package");
2405 }
2406
2407 #[test]
2408 fn new_package_defaults_to_private() {
2409 let tmp = tempfile::TempDir::new().unwrap();
2410 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2411 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2412 assert!(!pkg_is_public(tmp.path(), "does-not-exist"));
2414 }
2415
2416 #[test]
2417 fn set_visibility_round_trips_and_index_persists() {
2418 let tmp = tempfile::TempDir::new().unwrap();
2419 let state = State::open(tmp.path().to_path_buf()).unwrap();
2420 seed_pkg(tmp.path(), "lex-schema", "0.9.2");
2421
2422 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"public"}"#);
2423 assert!(pkg_is_public(tmp.path(), "lex-schema"));
2424 let idx = load_pkg_index(tmp.path(), "lex-schema").unwrap();
2426 assert_eq!(idx.latest.as_deref(), Some("0.9.2"));
2427 assert_eq!(idx.versions.len(), 1);
2428
2429 let _ = pkg_set_visibility_handler(&state, "lex-schema", r#"{"visibility":"private"}"#);
2430 assert!(!pkg_is_public(tmp.path(), "lex-schema"));
2431 }
2432
2433 #[test]
2434 fn set_visibility_on_unknown_package_is_a_noop() {
2435 let tmp = tempfile::TempDir::new().unwrap();
2436 let state = State::open(tmp.path().to_path_buf()).unwrap();
2437 let _ = pkg_set_visibility_handler(&state, "ghost", r#"{"visibility":"public"}"#);
2439 assert!(load_pkg_index(tmp.path(), "ghost").is_none());
2440 }
2441
2442 #[test]
2443 fn public_listing_omits_private_packages() {
2444 let tmp = tempfile::TempDir::new().unwrap();
2445 let state = State::open(tmp.path().to_path_buf()).unwrap();
2446 seed_pkg(tmp.path(), "pub-pkg", "1.0.0");
2447 seed_pkg(tmp.path(), "priv-pkg", "1.0.0");
2448 let _ = pkg_set_visibility_handler(&state, "pub-pkg", r#"{"visibility":"public"}"#);
2449
2450 let names = public_pkg_names(tmp.path());
2451 assert_eq!(names, vec!["pub-pkg".to_string()]);
2452 }
2453
2454 #[test]
2455 fn resolve_public_maps_routes() {
2456 let get = Method::Get;
2457 assert_eq!(resolve_public(&get, "").unwrap(), PublicTarget::List);
2458 assert_eq!(resolve_public(&get, "/").unwrap(), PublicTarget::List);
2459 assert_eq!(
2460 resolve_public(&get, "/lex-schema").unwrap(),
2461 PublicTarget::Latest("lex-schema".into())
2462 );
2463 assert_eq!(
2464 resolve_public(&get, "/lex-schema/versions").unwrap(),
2465 PublicTarget::Versions("lex-schema".into())
2466 );
2467 assert_eq!(
2468 resolve_public(&get, "/lex-schema/head").unwrap(),
2469 PublicTarget::Head("lex-schema".into())
2470 );
2471 assert_eq!(
2472 resolve_public(&get, "/lex-schema/0.9.2").unwrap(),
2473 PublicTarget::Version("lex-schema".into(), "0.9.2".into())
2474 );
2475 assert_eq!(
2476 resolve_public(&get, "/lex-schema/0.9.2/archive").unwrap(),
2477 PublicTarget::Archive("lex-schema".into(), "0.9.2".into())
2478 );
2479 }
2480
2481 #[test]
2482 fn resolve_public_rejects_bad_method_and_traversal() {
2483 assert_eq!(resolve_public(&Method::Put, "/lex-schema"), Err(405));
2485 assert_eq!(resolve_public(&Method::Post, "").err(), Some(405));
2486 assert_eq!(resolve_public(&Method::Get, "/.."), Err(404));
2488 assert_eq!(resolve_public(&Method::Get, "/lex-schema/../etc"), Err(404));
2489 assert_eq!(resolve_public(&Method::Get, "/a/b/c/d"), Err(404));
2490 assert!(resolve_public(&Method::Get, "/lex schema").is_err());
2492 }
2493}